Begin refactoring middleware for service accounts

This commit is contained in:
Tuan Dang
2023-04-04 11:08:03 +03:00
parent c797901778
commit aa53de9070
39 changed files with 943 additions and 577 deletions

View File

@@ -12,8 +12,8 @@
"@aws-sdk/client-secrets-manager": "^3.281.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",
@@ -2988,24 +2988,6 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@sentry/core": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.38.0.tgz",
"integrity": "sha512-+hXh/SO3Ie6WC2b+wi01xLhyVREdkRXS5QBmCiv3z2ks2HvYXp7PoKSXJvNKiwCP+pBD+enOnM1YEzM2yEy5yw==",
"dependencies": {
"@sentry/types": "7.38.0",
"@sentry/utils": "7.38.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/core/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/node": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.40.0.tgz",
@@ -3113,31 +3095,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/types": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.38.0.tgz",
"integrity": "sha512-NKOALR6pNUMzUrsk2m+dkPrO8uGNvNh1LD0BCPswKNjC2qHo1h1mDGCgBmF9+EWyii8ZoACTIsxvsda+MBf97Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.38.0.tgz",
"integrity": "sha512-MgbI3YmYuyyhUtvcXkgGBqjOW+nuLLNGUdWCK+C4kObf8VbLt3dSE/7SEMT6TSHLYQmxs2BxFgx5Agn97m68kQ==",
"dependencies": {
"@sentry/types": "7.38.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sinclair/typebox": {
"version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -14804,23 +14761,6 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@sentry/core": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.38.0.tgz",
"integrity": "sha512-+hXh/SO3Ie6WC2b+wi01xLhyVREdkRXS5QBmCiv3z2ks2HvYXp7PoKSXJvNKiwCP+pBD+enOnM1YEzM2yEy5yw==",
"requires": {
"@sentry/types": "7.38.0",
"@sentry/utils": "7.38.0",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sentry/node": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.40.0.tgz",
@@ -14908,27 +14848,6 @@
}
}
},
"@sentry/types": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.38.0.tgz",
"integrity": "sha512-NKOALR6pNUMzUrsk2m+dkPrO8uGNvNh1LD0BCPswKNjC2qHo1h1mDGCgBmF9+EWyii8ZoACTIsxvsda+MBf97Q=="
},
"@sentry/utils": {
"version": "7.38.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.38.0.tgz",
"integrity": "sha512-MgbI3YmYuyyhUtvcXkgGBqjOW+nuLLNGUdWCK+C4kObf8VbLt3dSE/7SEMT6TSHLYQmxs2BxFgx5Agn97m68kQ==",
"requires": {
"@sentry/types": "7.38.0",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@sinclair/typebox": {
"version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",

View File

@@ -9,7 +9,7 @@ import {
import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events';
import { EventService } from '../../services';
import { getPostHogClient } from '../../services';
import { TelemetryService } from '../../services';
interface PushSecret {
ciphertextKey: string;
@@ -38,7 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
@@ -112,7 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
@@ -181,7 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;

View File

@@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { getPostHogClient } from '../../services';
import { TelemetryService } from '../../services';
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
@@ -15,7 +15,7 @@ import { getPostHogClient } from '../../services';
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = {
@@ -68,7 +68,7 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
@@ -130,7 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds
@@ -184,7 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) {
@@ -213,7 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
* @returns
*/
export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
@@ -281,7 +281,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
@@ -335,7 +335,7 @@ export const updateSecret = async (req: Request, res: Response) => {
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const { environment } = req.query;
const { workspaceId } = req.params;

View File

@@ -15,12 +15,12 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services';
import { getPostHogClient } from '../../services';
import { TelemetryService } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag';
import _ from 'lodash';
import _, { eq } from 'lodash';
import {
BatchSecretRequest,
BatchSecret
@@ -28,12 +28,13 @@ import {
/**
* Peform a batch of any specified CUD secret operations
* (used by dashboard)
* @param req
* @param res
*/
export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']);
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const {
workspaceId,
@@ -91,7 +92,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user._id,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: createdSecrets.map((n) => n._id)
}) as IAction;
@@ -328,14 +331,15 @@ export const createSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
if (req.user) {
const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, ABILITY_WRITE)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
let listOfSecretsToCreate;
@@ -378,7 +382,7 @@ export const createSecrets = async (req: Request, res: Response) => {
version: 1,
workspace: new Types.ObjectId(workspaceId),
type,
user: type === SECRET_PERSONAL ? req.user : undefined,
user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined,
environment,
secretKeyCiphertext,
secretKeyIV,
@@ -391,7 +395,7 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentTag,
tags
});
})
});
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
@@ -447,14 +451,18 @@ export const createSecrets = async (req: Request, res: Response) => {
const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user._id,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: newlyCreatedSecrets.map((n) => n._id)
});
// (EE) create (audit) log
addAction && await EELogService.createLog({
userId: req.user._id.toString(),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction],
channel,
@@ -466,10 +474,15 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: req.user.email,
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: {
numberOfSecrets: listOfSecretsToCreate.length,
environment,
@@ -533,91 +546,118 @@ export const getSecrets = async (req: Request, res: Response) => {
}
*/
const postHogClient = getPostHogClient();
const { tagSlugs } = req.query;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const { workspaceId, environment, tagSlugs } = req.query;
// tags logic
let tagIds = [];
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped to env and project
let hasWriteOnlyAccess
if (!req.serviceTokenData) {
hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment)
const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment)
if (hasNoAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
let secrets: any
let secretQuery: any
if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
const tagIds = _.map(tagNamesList, (tagName) => {
const workspaceFromDB = await Tag.find({ workspace: workspaceId });
tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null;
});
}
let secrets: ISecret[] = [];
if (req.user) {
// case: client authorization is via JWT
secretQuery = {
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
tags: { $in: tagIds },
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
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" })
}
}
} else {
secretQuery = {
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
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] }
}
}
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")
} else {
secrets = await Secret.find(secretQuery).populate("tags")
}
}
if (hasWriteOnlyAccess) {
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
} 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),
environment,
user: {
$exists: false
},
...(tagIds.length > 0 ? { tags: { $in: tagIds } } : {}),
type: SECRET_SHARED
});
}
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS,
userId: new Types.ObjectId(userId),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId: new Types.ObjectId(userId),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction],
channel,
ipAddress: req.ip
});
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: userEmail,
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
@@ -633,59 +673,6 @@ export const getSecrets = async (req: Request, res: Response) => {
});
}
export const getOnlySecretKeys = async (req: Request, res: Response) => {
const { workspaceId, environment } = req.query;
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped
if (!req.serviceTokenData) {
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
const [err, secretKeys] = await to(Secret.find(
{
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
)
.select("secretKeyIV secretKeyTag secretKeyCiphertext")
.then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
// readAction && await EELogService.createLog({
// userId: new Types.ObjectId(userId),
// workspaceId: new Types.ObjectId(workspaceId as string),
// actions: [readAction],
// channel,
// ipAddress: req.ip
// });
return res.status(200).send({
secretKeys
});
}
/**
* Update secret(s)
* @param req
@@ -736,10 +723,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type
interface PatchSecret {
id: string;
secretKeyCiphertext: string;
@@ -865,14 +850,18 @@ export const updateSecrets = async (req: Request, res: Response) => {
const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
userId: req.user._id,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
updateAction && await EELogService.createLog({
userId: req.user._id.toString(),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key),
actions: [updateAction],
channel,
@@ -884,10 +873,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: key
})
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: req.user.email,
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,
@@ -909,7 +903,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
}
/**
* Delete secret(s) with id [workspaceId] and environment [environment]
* Delete secret(s)
* @param req
* @param res
*/
@@ -958,7 +952,11 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
return res.status(200).send({
message: 'delete secrets!!'
});
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id);
@@ -992,14 +990,18 @@ export const deleteSecrets = async (req: Request, res: Response) => {
});
const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
userId: req.user._id,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
deleteAction && await EELogService.createLog({
userId: req.user._id.toString(),
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key),
actions: [deleteAction],
channel,
@@ -1011,10 +1013,15 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: key
})
const postHogClient = TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: req.user.email,
distinctId: TelemetryService.getDistinctId({
user: req.user,
serviceAccount: req.serviceAccount,
serviceTokenData: req.serviceTokenData
}),
properties: {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,

View File

@@ -195,10 +195,8 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re
const {
environment,
workspaceId,
canRead = false,
canWrite = false,
canUpdate = false,
canDelete = false,
read = false,
write = false,
encryptedKey,
nonce
} = req.body;
@@ -221,10 +219,8 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment,
canRead,
canWrite,
canUpdate,
canDelete
read,
write
}).save();
const existingServiceAccountKey = await ServiceAccountKey.findOne({

View File

@@ -19,7 +19,7 @@ import {
reformatPullSecrets
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { getPostHogClient, EventService } from '../../services';
import { TelemetryService, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
interface V2PushSecret {
@@ -48,7 +48,7 @@ interface V2PushSecret {
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
@@ -122,7 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
try {
const postHogClient = getPostHogClient();
const postHogClient = TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;

View File

@@ -24,11 +24,15 @@ import {
const createActionUpdateSecret = async ({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
@@ -46,6 +50,8 @@ const createActionUpdateSecret = async ({
action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
@@ -72,11 +78,15 @@ const createActionUpdateSecret = async ({
const createActionSecret = async ({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
@@ -94,6 +104,8 @@ const createActionSecret = async ({
action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
@@ -110,29 +122,36 @@ const createActionSecret = async ({
}
/**
* Create an (audit) action for user with id [userId]
* Create an (audit) action for client with id [userId],
* [serviceAccountId], or [serviceTokenDataId]
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {String} obj.userId - id of user associated with action
* @returns
*/
const createActionUser = ({
const createActionClient = ({
name,
userId
userId,
serviceAccountId,
serviceTokenDataId
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
}) => {
let action;
try {
action = new Action({
name,
user: userId
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create user action');
throw new Error('Failed to create client action');
}
return action;
@@ -149,11 +168,15 @@ const createActionUser = ({
const createActionHelper = async ({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds,
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[];
}) => {
@@ -162,7 +185,7 @@ const createActionHelper = async ({
switch (name) {
case ACTION_LOGIN:
case ACTION_LOGOUT:
action = await createActionUser({
action = await createActionClient({
name,
userId
});

View File

@@ -1,8 +1,9 @@
import { Types } from 'mongoose';
import _ from "lodash";
import { Membership } from "../../models";
import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization";
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) {
return false
@@ -18,7 +19,7 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
return true
}
export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => {
export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) {
return false
@@ -36,7 +37,7 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env
return false
}
export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => {
export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) {
return true

View File

@@ -16,12 +16,16 @@ import {
*/
const createLogHelper = async ({
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
@@ -31,6 +35,8 @@ const createLogHelper = async ({
try {
log = await new Log({
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name),
actions,

View File

@@ -11,6 +11,8 @@ import {
export interface IAction {
name: string;
user?: Types.ObjectId,
serviceAccount?: Types.ObjectId,
serviceTokenData?: Types.ObjectId,
workspace?: Types.ObjectId,
payload?: {
secretVersions?: Types.ObjectId[]
@@ -33,8 +35,15 @@ const actionSchema = new Schema<IAction>(
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
ref: 'User'
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
},
workspace: {
type: Schema.Types.ObjectId,

View File

@@ -11,6 +11,8 @@ import {
export interface ILog {
_id: Types.ObjectId;
user?: Types.ObjectId;
serviceAccount?: Types.ObjectId;
serviceTokenData?: Types.ObjectId;
workspace?: Types.ObjectId;
actionNames: string[];
actions: Types.ObjectId[];
@@ -24,6 +26,14 @@ const logSchema = new Schema<ILog>(
type: Schema.Types.ObjectId,
ref: 'User'
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'

View File

@@ -26,12 +26,16 @@ class EELogService {
*/
static async createLog({
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
@@ -40,6 +44,8 @@ class EELogService {
if (!EELicenseService.isLicenseValid) return null;
return await createLogHelper({
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
actions,
channel,
@@ -59,17 +65,23 @@ class EELogService {
static async createAction({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
}: {
name: string;
userId: Types.ObjectId;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[];
}) {
return await createActionHelper({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
});

View File

@@ -22,6 +22,12 @@ import {
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
*
@@ -39,7 +45,7 @@ const validateAuthMode = ({
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];
let authTokenType, authTokenValue;
let authMode, authTokenValue;
if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
@@ -47,7 +53,7 @@ const validateAuthMode = ({
if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
authTokenType = 'apiKey';
authMode = AUTH_MODE_API_KEY;
authTokenValue = apiKey;
}
@@ -63,24 +69,24 @@ const validateAuthMode = ({
switch (tokenValue.split('.', 1)[0]) {
case 'st':
authTokenType = 'serviceToken';
authMode = AUTH_MODE_SERVICE_TOKEN;
break;
case 'sa':
authTokenType = 'serviceAccount';
authMode = AUTH_MODE_SERVICE_ACCOUNT;
break;
default:
authTokenType = 'jwt';
authMode = AUTH_MODE_JWT;
}
authTokenValue = tokenValue;
}
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
return ({
authTokenType,
authMode,
authTokenValue
});
}
@@ -155,8 +161,7 @@ const getAuthSTDPayload = async ({
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag')
.populate<{user: IUser}>('user');
.select('+encryptedKey +iv +tag');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
@@ -216,7 +221,7 @@ const getAuthAPIKeyPayload = async ({
const apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('user', '+publicKey');
.populate<{user: IUser}>('user', '+publicKey');
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });

View File

@@ -24,57 +24,6 @@ import _ from 'lodash';
import { ABILITY_WRITE } from '../variables/organization';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
/**
* Validate that user with id [userId] can modify secrets with ids [secretIds]
* @param {Object} obj
* @param {Object} obj.userId - id of user to validate
* @param {Object} obj.secretIds - secret ids
* @returns {Secret[]} secrets
*/
const validateSecrets = async ({
userId,
secretIds
}: {
userId: string;
secretIds: string[];
}) => {
let secrets;
try {
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Unable to validate some secrets' })
}
const userMemberships = await Membership.find({ user: userId })
const userMembershipById = _.keyBy(userMemberships, 'workspace');
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
// for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => {
if (workspaceIdsSet.has(secret.workspace.toString())) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
if (isDisallowed) {
throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' });
}
} else {
throw BadRequestError({ message: 'You cannot edit secrets of a workspace you are not a member of' });
}
});
} catch (err) {
throw BadRequestError({ message: 'Unable to validate secrets' })
}
return secrets;
}
interface V1PushSecret {
ciphertextKey: string;
ivKey: string;
@@ -714,7 +663,6 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
};
export {
validateSecrets,
v1PushSecrets,
v2PushSecrets,
pullSecrets,

View File

@@ -0,0 +1,102 @@
import { Types } from 'mongoose';
import {
User,
IUser,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
Secret,
ISecret
} from '../models';
import {
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets
} from '../helpers/serviceAccount';
import { BadRequestError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate accepted clients for secrets with ids [secretIds]
* @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
*/
const validateClientForSecrets = async ({
authData,
secretIds,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
secretIds: string[];
requiredPermissions: string[];
}) => {
let secrets: ISecret[] = [];
secrets = await Secret.find({
_id: {
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
// TODO
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
// TODO
await validateServiceAccountClientForSecrets({
serviceAccount: authData.authPayload,
secrets,
requiredPermissions
});
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForSecrets({
serviceTokenData: authData.authPayload,
secrets,
requiredPermissions
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
// TODO
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
}
return secrets;
}
export {
validateClientForSecrets
}

View File

@@ -0,0 +1,63 @@
import _ from 'lodash';
import { Types } from 'mongoose';
import {
IServiceAccount,
ISecret,
ServiceAccountWorkspacePermission
} from '../models';
/**
* Validate that serviceAccount (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {Object} obj.
*/
const validateServiceAccountClientForWorkspace = async ({
serviceAccount,
workspaceId,
environment,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
workspaceId: Types.ObjectId;
environment: string;
requiredPermissions: string[];
}) => {
// TODO
return [];
}
/**
* Validate that service account (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceAccount - service account client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForSecrets = async ({
serviceAccount,
secrets,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
const permissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: serviceAccount._id
});
const permissionsObj = _.keyBy(permissions, (p) => {
return `${p.workspace.toString()}-${p.environment}`
});
// TODO
return [];
}
export {
validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets
}

View File

@@ -0,0 +1,99 @@
import { Types } from 'mongoose';
import {
ISecret,
IServiceTokenData
} from '../models';
import { UnauthorizedRequestError } from '../utils/errors';
/**
* Validate that service token (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForWorkspace = async ({
serviceTokenData,
workspaceId,
environment,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (serviceTokenData.environment !== environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
}
/**
* Validate that service token (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForSecrets = async ({
serviceTokenData,
secrets,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
secrets.forEach((secret: ISecret) => {
if (!serviceTokenData.workspace.equals(secret.workspace)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (serviceTokenData.environment !== secret.environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
});
}
export {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataClientForSecrets
}

View File

View File

@@ -1,6 +1,18 @@
import * as Sentry from '@sentry/node';
import { IUser, User } from '../models';
import { Types } from 'mongoose';
import {
IUser,
ISecret,
User,
Membership
} from '../models';
import { sendMail } from './nodemailer';
import { validateMembership } from './membership';
import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import {
ABILITY_WRITE
} from '../variables/organization';
/**
* Initialize a user under email [email]
@@ -146,4 +158,90 @@ const checkUserDevice = async ({
}
}
export { setupAccount, completeAccount, checkUserDevice };
/**
* Validate that user (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForWorkspace = async ({
user,
workspaceId,
environment,
requiredPermissions
}: {
user: IUser;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
// org-level and workspace-level permissions? - workspace-level env scoped?
// validate user membership in workspace
const membership = await validateMembership({
userId: user._id,
workspaceId
});
// validate user permission
// TODO: validate that user can perform action on environment in workspace
return membership;
}
/**
* Validate that user (client) can access secrets with ids [secretIds]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Secret[]} obj.secrets - secrets to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForSecrets = async ({
user,
secrets,
requiredPermissions
}: {
user: IUser;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
// TODO: consider refactor
const userMemberships = await Membership.find({ user: user._id })
const userMembershipById = _.keyBy(userMemberships, 'workspace');
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
// for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => {
if (workspaceIdsSet.has(secret.workspace.toString())) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
} else {
throw BadRequestError({
message: 'You cannot edit secrets of a workspace you are not a member of'
});
}
});
}
export {
setupAccount,
completeAccount,
checkUserDevice,
validateUserClientForWorkspace,
validateUserClientForSecrets
};

View File

@@ -5,47 +5,98 @@ import {
Bot,
Membership,
Key,
Secret
Secret,
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
} from '../models';
import { createBot } from '../helpers/bot';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate accepted clients by id including [userId], [serviceAccountId],
* and [serviceTokenDataId] for workspace with id [workspaceId] based
* Validate accepted clients for workspace with id [workspaceId] based
* on any known permissions.
* @param {Object} obj
* @param {Types.ObjectId} obj.userId - id of user
* @param {User} obj.user - user client
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForWorkspace = async ({
userId,
serviceAccountId,
serviceTokenDataId,
authData,
workspaceId,
environment
environment,
requiredPermissions
}: {
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
let membership;
if (userId) {
membership = await validateMembership({
userId,
workspaceId
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
// TODO: validate user against [requiredPermissions]
}
if (serviceAccountId) {
// TODO
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
const permission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount: authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId),
environment
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace environment'
});
// TODO: validate [requiredPermissions] against [permission]
}
if (serviceTokenDataId) {
// TODO
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
// TODO: validate [requiredPermissions] against [permission]
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
}
return ({

View File

@@ -9,7 +9,7 @@ import * as Sentry from '@sentry/node';
import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { logTelemetryMessage } from './services';
import { TelemetryService } from './services';
import { setTransporter } from './helpers/nodemailer';
import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -80,7 +80,7 @@ const main = async () => {
});
}
logTelemetryMessage();
TelemetryService.logTelemetryMessage();
setTransporter(initSmtp());
await DatabaseService.initDatabase(getMongoURL());

View File

@@ -10,6 +10,17 @@ import {
import {
UnauthorizedRequestError
} from '../utils/errors';
import {
IUser,
IServiceAccount,
IServiceTokenData
} from '../models';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -28,59 +39,51 @@ declare module 'jsonwebtoken' {
* @returns
*/
const requireAuth = ({
acceptedAuthModes = ['jwt'],
requiredServiceTokenPermissions = []
acceptedAuthModes = [AUTH_MODE_JWT],
}: {
acceptedAuthModes: string[];
requiredServiceTokenPermissions?: 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 { authTokenType, authTokenValue } = validateAuthMode({
const { authMode, authTokenValue } = validateAuthMode({
headers: req.headers,
acceptedAuthModes
});
req.authTokenType = authTokenType;
// attach auth payloads
let serviceTokenData: any;
switch (authTokenType) {
case 'serviceAccount':
req.serviceAccount = await getAuthSAAKPayload({
let authPayload: IUser | IServiceAccount | IServiceTokenData;
switch (authMode) {
case AUTH_MODE_SERVICE_ACCOUNT:
authPayload = await getAuthSAAKPayload({
authTokenValue
});
req.serviceAccount = authPayload;
break;
case 'serviceToken':
serviceTokenData = await getAuthSTDPayload({
case AUTH_MODE_SERVICE_TOKEN:
authPayload = await getAuthSTDPayload({
authTokenValue
});
// TODO: bring this into a separate collection
requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => {
if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) {
return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' }));
}
});
req.serviceTokenData = serviceTokenData;
req.user = serviceTokenData?.user;
req.serviceTokenData = authPayload;
break;
case 'apiKey':
// TODO: deprecate API key
req.user = await getAuthAPIKeyPayload({
case AUTH_MODE_API_KEY:
authPayload = await getAuthAPIKeyPayload({
authTokenValue
});
req.user = authPayload;
break;
default:
req.user = await getAuthUserPayload({
authPayload = await getAuthUserPayload({
authTokenValue
});
req.user = authPayload;
break;
}
req.authData = {
authMode,
authPayload
}
return next();
}
}

View File

@@ -1,48 +1,34 @@
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import { Secret, Membership } from '../models';
import { validateSecrets } from '../helpers/secret';
// TODO: make this work for delete route
import { validateClientForSecrets } from '../helpers/secrets';
const requireSecretsAuth = ({
acceptedRoles
acceptedRoles,
requiredPermissions = []
}: {
acceptedRoles: string[];
requiredPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
let secrets;
try {
if (Array.isArray(req.body.secrets)) {
// case: validate multiple secrets
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secrets.map((s: any) => s.id)
});
} else if (typeof req.body.secrets === 'object') { // change this to check for object
// case: validate 1 secret
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secrets.id]
});
} else if (Array.isArray(req.body.secretIds)) {
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secretIds
});
} else if (typeof req.body.secretIds === 'string') {
// case: validate secretIds
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: [req.body.secretIds]
});
}
req.secrets = secrets;
return next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' }));
let secretIds = [];
if (Array.isArray(req.body.secrets)) {
secretIds = req.body.secrets.map((s: any) => s.id);
} else if (typeof req.body.secrets === 'object') {
secretIds = [req.body.secrets.id];
} else if (Array.isArray(req.body.secretIds)) {
secretIds = req.body.secretIds;
} else if (typeof req.body.secretIds === 'string') {
secretIds = [req.body.secretIds];
}
req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds: [req.body.secretIds],
requiredPermissions
});
return next();
}
}

View File

@@ -16,44 +16,32 @@ type req = 'params' | 'body' | 'query';
const requireWorkspaceAuth = ({
acceptedRoles,
locationWorkspaceId,
locationEnvironment = undefined
locationEnvironment = undefined,
requiredPermissions = []
}: {
acceptedRoles: string[];
locationWorkspaceId: req;
locationEnvironment?: req | undefined;
requiredPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
// TODO: throw errors if workspaceId or environemnt are not present
const workspaceId = req[locationWorkspaceId]?.workspaceId;
const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
// validate clients
const { membership } = await validateClientForWorkspace({
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId),
environment
});
if (membership) {
req.membership = membership;
}
if (
req.serviceTokenData
&& req.serviceTokenData.workspace.toString() !== workspaceId
&& req.serviceTokenData.environment !== req.body.environment
) {
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}
return next();
} catch (err) {
return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
const workspaceId = req[locationWorkspaceId]?.workspaceId;
const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
// validate clients
const { membership } = await validateClientForWorkspace({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
requiredPermissions
});
if (membership) {
req.membership = membership;
}
return next();
};
};

View File

@@ -5,10 +5,8 @@ export interface IServiceAccountWorkspacePermission extends Document {
serviceAccount: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
canRead: boolean;
canWrite: boolean;
canUpdate: boolean;
canDelete: boolean;
read: boolean;
write: boolean;
}
const serviceAccountWorkspacePermissionSchema = new Schema<IServiceAccountWorkspacePermission>(
@@ -27,19 +25,11 @@ const serviceAccountWorkspacePermissionSchema = new Schema<IServiceAccountWorksp
type: String,
required: true
},
canRead: {
read: {
type: Boolean,
default: false
},
canWrite: {
type: Boolean,
default: false
},
canUpdate: {
type: Boolean,
default: false
},
canDelete: {
write: {
type: Boolean,
default: false
}

View File

@@ -1,6 +1,7 @@
import { Schema, model, Types } from 'mongoose';
import { Schema, model, Types, Document } from 'mongoose';
export interface IServiceTokenData {
export interface IServiceTokenData extends Document {
_id: Types.ObjectId;
name: string;
workspace: Types.ObjectId;
environment: string;

View File

@@ -16,6 +16,7 @@ router.post(
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
@@ -31,6 +32,7 @@ router.put(
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
@@ -47,6 +49,7 @@ router.delete(
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
@@ -61,6 +64,7 @@ router.get(
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,

View File

@@ -8,12 +8,14 @@ import {
} from '../../middleware';
import { query, body } from 'express-validator';
import { secretsController } from '../../controllers/v2';
import { validateSecrets } from '../../helpers/secret';
import { validateClientForSecrets } from '../../helpers/secrets';
import {
ADMIN,
MEMBER,
SECRET_PERSONAL,
SECRET_SHARED
SECRET_SHARED,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../variables';
import {
BatchSecretRequest
@@ -22,8 +24,7 @@ import {
router.post(
'/batch',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read', 'write']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -40,12 +41,11 @@ router.post(
.filter((secretId) => secretId !== undefined)
if (secretIds.length > 0) {
const relevantSecrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds
req.secrets = await validateClientForSecrets({
authData: req.authData,
secretIds,
requiredPermissions: []
});
req.secrets = relevantSecrets;
}
}
return true;
@@ -100,12 +100,13 @@ router.post(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body'
locationWorkspaceId: 'body',
locationEnvironment: 'body',
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
secretsController.createSecrets
);
@@ -117,12 +118,13 @@ router.get(
query('tagSlugs'),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query'
locationWorkspaceId: 'query',
locationEnvironment: 'query',
requiredPermissions: [PERMISSION_READ_SECRETS]
}),
secretsController.getSecrets
);
@@ -157,11 +159,11 @@ router.patch(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
secretsController.updateSecrets
);
@@ -186,11 +188,11 @@ router.delete(
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken', 'serviceAccount']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS]
}),
secretsController.deleteSecrets
);

View File

@@ -125,10 +125,8 @@ router.post(
param('serviceAccountId').exists().isString().trim(),
body('workspaceId').exists().isString().notEmpty(),
body('environment').exists().isString().notEmpty(),
body('canRead').isBoolean().optional(),
body('canWrite').isBoolean().optional(),
body('canUpdate').isBoolean().optional(),
body('canDelete').isBoolean().optional(),
body('read').isBoolean().optional(),
body('write').isBoolean().optional(),
body('encryptedKey').exists().isString().notEmpty(),
body('nonce').exists().isString().notEmpty(),
validateRequest,

View File

@@ -5,7 +5,7 @@ import { tagController } from '../../controllers/v2';
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
validateRequest
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
@@ -16,6 +16,7 @@ router.get(
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
validateRequest,
@@ -39,6 +40,7 @@ router.post(
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
locationWorkspaceId: 'params'
}),
param('workspaceId').exists().trim(),
body('name').exists().trim(),

View File

@@ -1,44 +0,0 @@
import { PostHog } from 'posthog-node';
import { getLogger } from '../utils/logger';
import {
getNodeEnv,
getTelemetryEnabled,
getPostHogProjectApiKey,
getPostHogHost
} from '../config';
/**
* Logs telemetry enable/disable notice.
*/
const logTelemetryMessage = () => {
if(!getTelemetryEnabled()){
getLogger("backend-main").info([
"",
"To improve, Infisical collects telemetry data about general usage.",
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n'))
}
}
/**
* Return an instance of the PostHog client initialized.
* @returns
*/
const getPostHogClient = () => {
let postHogClient: any;
if (getNodeEnv() === 'production' && getTelemetryEnabled()) {
// case: enable opt-out telemetry in production
postHogClient = new PostHog(getPostHogProjectApiKey(), {
host: getPostHogHost()
});
}
return postHogClient;
}
export {
logTelemetryMessage,
getPostHogClient
}

View File

@@ -0,0 +1,85 @@
import { PostHog } from 'posthog-node';
import { getLogger } from '../utils/logger';
import {
getNodeEnv,
getTelemetryEnabled,
getPostHogProjectApiKey,
getPostHogHost
} from '../config';
import {
IUser,
IServiceAccount,
IServiceTokenData
} from '../models';
import {
BadRequestError
} from '../utils/errors';
class Telemetry {
/**
* Logs telemetry enable/disable notice.
*/
static logTelemetryMessage = () => {
if(!getTelemetryEnabled()){
getLogger("backend-main").info([
"",
"To improve, Infisical collects telemetry data about general usage.",
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
].join('\n'))
}
}
/**
* Return an instance of the PostHog client initialized.
* @returns
*/
static getPostHogClient = () => {
let postHogClient: any;
if (getNodeEnv() === 'production' && getTelemetryEnabled()) {
// case: enable opt-out telemetry in production
postHogClient = new PostHog(getPostHogProjectApiKey(), {
host: getPostHogHost()
});
}
return postHogClient;
}
/**
* Return a distinct id for client to be used for logging telemetry
*/
static getDistinctId = ({
user,
serviceAccount,
serviceTokenData
}: {
user?: IUser;
serviceAccount?: IServiceAccount;
serviceTokenData?: IServiceTokenData;
}) => {
let distinctId = '';
if (user) {
distinctId = user.email;
}
if (serviceAccount) {
distinctId = `sa.${serviceAccount._id}`;
}
if (serviceTokenData) {
distinctId = `st.${serviceTokenData._id}`;
}
if (distinctId === '') {
throw BadRequestError({
message: 'Failed to obtain distinct id for logging telemetry'
});
}
return distinctId;
}
}
export default Telemetry;

View File

@@ -1,13 +1,15 @@
import DatabaseService from './DatabaseService';
import { logTelemetryMessage, getPostHogClient } from './PostHogClient';
// import { logTelemetryMessage, getPostHogClient } from './TelemetryService';
import TelemetryService from './TelemetryService';
import BotService from './BotService';
import EventService from './EventService';
import IntegrationService from './IntegrationService';
import TokenService from './TokenService';
export {
logTelemetryMessage,
getPostHogClient,
TelemetryService,
// logTelemetryMessage,
// getPostHogClient,
DatabaseService,
BotService,
EventService,

View File

@@ -24,7 +24,7 @@ declare global {
serviceTokenData: any;
apiKeyData: any;
query?: any;
authTokenType: string;
authData: any;
}
}
}

View File

@@ -0,0 +1,11 @@
const AUTH_MODE_JWT = 'jwt';
const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount';
const AUTH_MODE_SERVICE_TOKEN = 'serviceToken';
const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate
export {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
}

View File

@@ -63,6 +63,16 @@ import {
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
} from './token';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from './permission';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from './authentication';
export {
OWNER,
@@ -113,6 +123,8 @@ export {
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
getIntegrationOptions,
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
@@ -124,5 +136,9 @@ export {
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
TOKEN_EMAIL_PASSWORD_RESET,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
};

View File

@@ -0,0 +1,7 @@
const PERMISSION_READ_SECRETS = 'read';
const PERMISSION_WRITE_SECRETS = 'write';
export {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
}

View File

@@ -31,20 +31,16 @@ export type ServiceAccountWorkspacePermission = {
serviceAccount: string;
workspace: Workspace;
environment: string;
canRead: boolean;
canWrite: boolean;
canUpdate: boolean;
canDelete: boolean;
read: boolean;
write: boolean;
}
export type CreateServiceAccountWorkspacePermissionDTO = {
serviceAccountId: string;
workspaceId: string;
environment: string;
canRead: boolean;
canWrite: boolean;
canUpdate: boolean;
canDelete: boolean;
read: boolean;
write: boolean;
encryptedKey: string;
nonce: string;
}

View File

@@ -48,10 +48,8 @@ const createProjectLevelPermissionSchema = yup.object({
workspace: yup.string().required().label('Workspace'),
environment: yup.string().required().label('Environment'),
permissions: yup.object().shape({
canRead: yup.boolean().required(),
canWrite: yup.boolean().required(),
canUpdate: yup.boolean().required(),
canDelete: yup.boolean().required(),
read: yup.boolean().required(),
write: yup.boolean().required()
}).defined().required()
});
@@ -91,7 +89,7 @@ export const SAProjectLevelPermissionsTable = ({
privateKey,
workspace,
environment,
permissions: { canRead, canWrite, canUpdate, canDelete }
permissions: { read, write }
}: CreateProjectLevelPermissionForm) => {
// TODO: clean up / modularize this function
@@ -126,10 +124,8 @@ export const SAProjectLevelPermissionsTable = ({
serviceAccountId,
workspaceId: workspace,
environment,
canRead,
canWrite,
canUpdate,
canDelete,
read,
write,
encryptedKey: ciphertext,
nonce
});
@@ -187,10 +183,8 @@ export const SAProjectLevelPermissionsTable = ({
_id,
workspace,
environment,
canRead,
canWrite,
canUpdate,
canDelete
read,
write
}) => {
const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name;
return (
@@ -200,28 +194,14 @@ export const SAProjectLevelPermissionsTable = ({
<Td>
<Checkbox
id="isReadPermissionEnabled"
isChecked={canRead}
isChecked={read}
isDisabled
>{/**/}</Checkbox>
</Td>
<Td>
<Checkbox
id="isWritePermissionEnabled"
isChecked={canWrite}
isDisabled
>{/**/}</Checkbox>
</Td>
<Td>
<Checkbox
id="isUpdatePermissionEnabled"
isChecked={canUpdate}
isDisabled
>{/**/}</Checkbox>
</Td>
<Td>
<Checkbox
id="isDeletePermissionEnabled"
isChecked={canDelete}
isChecked={write}
isDisabled
>{/**/}</Checkbox>
</Td>
@@ -352,28 +332,18 @@ export const SAProjectLevelPermissionsTable = ({
control={control}
name="permissions"
defaultValue={{
canRead: true,
canWrite: false,
canUpdate: false,
canDelete: false
read: true,
write: false
}}
render={({ field: { onChange, value }, fieldState: { error }}) => {
const options = [
{
label: 'Read (default)',
value: 'canRead'
value: 'read'
},
{
label: 'Write',
value: 'canWrite'
},
{
label: 'Update',
value: 'canUpdate'
},
{
label: 'Delete',
value: 'canDelete'
value: 'write'
}
];