feat(folder-scoped-st): added batch,create secrets v2 secretpath support and service token

This commit is contained in:
akhilmhdh
2023-06-10 12:10:43 +05:30
parent 7d554f46d5
commit 445afb397c
4 changed files with 147 additions and 84 deletions
+72 -12
View File
@@ -1,6 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { ISecret, Secret } from "../../models"; import { ISecret, Secret, ServiceTokenData } from "../../models";
import { IAction, SecretVersion } from "../../ee/models"; import { IAction, SecretVersion } from "../../ee/models";
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
@@ -29,6 +29,7 @@ import { BatchSecretRequest, BatchSecret } from "../../types/secret";
import Folder from "../../models/folder"; import Folder from "../../models/folder";
import { import {
getFolderByPath, getFolderByPath,
getFolderIdFromServiceToken,
searchByFolderId, searchByFolderId,
} from "../../services/FolderService"; } from "../../services/FolderService";
@@ -45,14 +46,15 @@ export const batchSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
folderId,
requests, requests,
secretPath,
}: { }: {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
folderId: string;
requests: BatchSecretRequest[]; requests: BatchSecretRequest[];
secretPath: string;
} = req.body; } = req.body;
let folderId = req.body.folderId as string;
const createSecrets: BatchSecret[] = []; const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = []; const updateSecrets: BatchSecret[] = [];
@@ -70,6 +72,25 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (!folder) throw BadRequestError({ message: "Folder not found" }); if (!folder) throw BadRequestError({ message: "Folder not found" });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if (
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (secretPath) {
folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
}
for await (const request of requests) { for await (const request of requests) {
// do a validation // do a validation
@@ -152,6 +173,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
numberOfSecrets: createdSecrets.length, numberOfSecrets: createdSecrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
@@ -218,7 +240,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
tags: u.tags, tags: u.tags,
folder: u.folder folder: u.folder,
}) })
); );
@@ -248,6 +270,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
numberOfSecrets: updateSecrets.length, numberOfSecrets: updateSecrets.length,
environment, environment,
workspaceId, workspaceId,
folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
@@ -395,8 +418,13 @@ export const createSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
folderId, secretPath,
}: { workspaceId: string; environment: string; folderId: string } = req.body; }: {
workspaceId: string;
environment: string;
secretPath?: string;
} = req.body;
let folderId = req.body.folderId;
if (req.user) { if (req.user) {
const hasAccess = await userHasWorkspaceAccess( const hasAccess = await userHasWorkspaceAccess(
@@ -421,6 +449,24 @@ export const createSecrets = async (req: Request, res: Response) => {
// case: create 1 secret // case: create 1 secret
listOfSecretsToCreate = [req.body.secrets]; listOfSecretsToCreate = [req.body.secrets];
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if (
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (secretPath) {
folderId = await getFolderIdFromServiceToken(
workspaceId,
environment,
secretPath
);
}
// get secret blind index salt // get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({ const salt = await SecretService.getSecretBlindIndexSalt({
@@ -585,6 +631,7 @@ export const createSecrets = async (req: Request, res: Response) => {
environment, environment,
workspaceId, workspaceId,
channel: channel, channel: channel,
folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
}); });
@@ -660,6 +707,18 @@ export const getSecrets = async (req: Request, res: Response) => {
if (!folder) throw BadRequestError({ message: "Folder not found" }); if (!folder) throw BadRequestError({ message: "Folder not found" });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload;
// in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used
if (
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
if (folders && secretPath) { if (folders && secretPath) {
if (!folders) throw BadRequestError({ message: "Folder not found" }); if (!folders) throw BadRequestError({ message: "Folder not found" });
const folder = getFolderByPath(folders.nodes, secretPath as string); const folder = getFolderByPath(folders.nodes, secretPath as string);
@@ -800,6 +859,7 @@ export const getSecrets = async (req: Request, res: Response) => {
environment, environment,
workspaceId, workspaceId,
channel, channel,
folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"],
}, },
}); });
@@ -910,13 +970,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
tags, tags,
...(secretCommentCiphertext !== undefined && ...(secretCommentCiphertext !== undefined &&
secretCommentIV && secretCommentIV &&
secretCommentTag secretCommentTag
? { ? {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
} }
: {}), : {}),
}, },
}, },
+1 -24
View File
@@ -44,30 +44,7 @@ import {
getAuthDataPayloadIdObj, getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj, getAuthDataPayloadUserObj,
} from "../utils/auth"; } from "../utils/auth";
import Folder from "../models/folder"; import { getFolderIdFromServiceToken } from "../services/FolderService";
import { getFolderByPath } from "../services/FolderService";
export const getFolderIdFromServiceToken = async (
workspaceId: Types.ObjectId | string,
environment: string,
secretPath: string
) => {
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (!folders) {
if (secretPath !== "/") throw new Error("Invalid path. Folders not found");
} else {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw new Error("Folder not found");
}
return folder.id;
}
return "root";
};
/** /**
* Create secret blind index data containing encrypted blind index [salt] * Create secret blind index data containing encrypted blind index [salt]
+50 -47
View File
@@ -1,15 +1,15 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
requireSecretsAuth, requireSecretsAuth,
validateRequest, validateRequest,
} from '../../middleware'; } from "../../middleware";
import { validateClientForSecrets } from '../../validation'; import { validateClientForSecrets } from "../../validation";
import { query, body } from 'express-validator'; import { query, body } from "express-validator";
import { secretsController } from '../../controllers/v2'; import { secretsController } from "../../controllers/v2";
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
@@ -21,11 +21,11 @@ import {
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
} from '../../variables'; } from "../../variables";
import { BatchSecretRequest } from '../../types/secret'; import { BatchSecretRequest } from "../../types/secret";
router.post( router.post(
'/batch', "/batch",
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
@@ -35,12 +35,13 @@ router.post(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
}), }),
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('folderId').default('root').isString().trim(), body("folderId").default("root").isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('requests') body("secretPath").optional().isString().trim(),
body("requests")
.exists() .exists()
.custom(async (requests: BatchSecretRequest[], { req }) => { .custom(async (requests: BatchSecretRequest[], { req }) => {
if (Array.isArray(requests)) { if (Array.isArray(requests)) {
@@ -65,17 +66,18 @@ router.post(
); );
router.post( router.post(
'/', "/",
body('workspaceId').exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body('environment').exists().isString().trim(), body("environment").exists().isString().trim(),
body('folderId').default('root').isString().trim(), body("folderId").default("root").isString().trim(),
body('secrets') body("secretPath").optional().isString().trim(),
body("secrets")
.exists() .exists()
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: create multiple secrets // case: create multiple secrets
if (value.length === 0) if (value.length === 0)
throw new Error('secrets cannot be an empty array'); throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if ( if (
!secret.type || !secret.type ||
@@ -85,16 +87,16 @@ router.post(
!secret.secretKeyCiphertext || !secret.secretKeyCiphertext ||
!secret.secretKeyIV || !secret.secretKeyIV ||
!secret.secretKeyTag || !secret.secretKeyTag ||
typeof secret.secretValueCiphertext !== 'string' || typeof secret.secretValueCiphertext !== "string" ||
!secret.secretValueIV || !secret.secretValueIV ||
!secret.secretValueTag !secret.secretValueTag
) { ) {
throw new Error( throw new Error(
'secrets array must contain objects that have required secret properties' "secrets array must contain objects that have required secret properties"
); );
} }
} }
} else if (typeof value === 'object') { } else if (typeof value === "object") {
// case: update 1 secret // case: update 1 secret
if ( if (
!value.type || !value.type ||
@@ -107,11 +109,11 @@ router.post(
!value.secretValueTag !value.secretValueTag
) { ) {
throw new Error( throw new Error(
'secrets object is missing required secret properties' "secrets object is missing required secret properties"
); );
} }
} else { } else {
throw new Error('secrets must be an object or an array of objects'); throw new Error("secrets must be an object or an array of objects");
} }
return true; return true;
@@ -126,19 +128,20 @@ router.post(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body', locationWorkspaceId: "body",
locationEnvironment: 'body', locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
}), }),
secretsController.createSecrets secretsController.createSecrets
); );
router.get( router.get(
'/', "/",
query('workspaceId').exists().trim(), query("workspaceId").exists().trim(),
query('environment').exists().trim(), query("environment").exists().trim(),
query('tagSlugs'), query("tagSlugs"),
query('folderId').default('root').isString().trim(), query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
@@ -150,34 +153,34 @@ router.get(
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query', locationWorkspaceId: "query",
locationEnvironment: 'query', locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
router.patch( router.patch(
'/', "/",
body('secrets') body("secrets")
.exists() .exists()
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: update multiple secrets // case: update multiple secrets
if (value.length === 0) if (value.length === 0)
throw new Error('secrets cannot be an empty array'); throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if (!secret.id) { if (!secret.id) {
throw new Error('Each secret must contain a ID property'); throw new Error("Each secret must contain a ID property");
} }
} }
} else if (typeof value === 'object') { } else if (typeof value === "object") {
// case: update 1 secret // case: update 1 secret
if (!value.id) { if (!value.id) {
throw new Error('secret must contain a ID property'); throw new Error("secret must contain a ID property");
} }
} else { } else {
throw new Error('secrets must be an object or an array of objects'); throw new Error("secrets must be an object or an array of objects");
} }
return true; return true;
@@ -198,21 +201,21 @@ router.patch(
); );
router.delete( router.delete(
'/', "/",
body('secretIds') body("secretIds")
.exists() .exists()
.custom((value) => { .custom((value) => {
// case: delete 1 secret // case: delete 1 secret
if (typeof value === 'string') return true; if (typeof value === "string") return true;
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: delete multiple secrets // case: delete multiple secrets
if (value.length === 0) if (value.length === 0)
throw new Error('secrets cannot be an empty array'); throw new Error("secrets cannot be an empty array");
return value.every((id: string) => typeof id === 'string'); return value.every((id: string) => typeof id === "string");
} }
throw new Error('secretIds must be a string or an array of strings'); throw new Error("secretIds must be a string or an array of strings");
}) })
.not() .not()
.isEmpty(), .isEmpty(),
+24 -1
View File
@@ -1,5 +1,6 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { TFolderSchema } from "../models/folder"; import { Types } from "mongoose";
import Folder, { TFolderSchema } from "../models/folder";
type TAppendFolderDTO = { type TAppendFolderDTO = {
folderName: string; folderName: string;
@@ -192,3 +193,25 @@ export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
} }
return segment; return segment;
}; };
export const getFolderIdFromServiceToken = async (
workspaceId: Types.ObjectId | string,
environment: string,
secretPath: string
) => {
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (!folders) {
if (secretPath !== "/") throw new Error("Invalid path. Folders not found");
} else {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw new Error("Folder not found");
}
return folder.id;
}
return "root";
};