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

View File

@ -1,6 +1,6 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { ISecret, Secret } from "../../models";
import { ISecret, Secret, ServiceTokenData } from "../../models";
import { IAction, SecretVersion } from "../../ee/models";
import {
SECRET_PERSONAL,
@ -29,6 +29,7 @@ import { BatchSecretRequest, BatchSecret } from "../../types/secret";
import Folder from "../../models/folder";
import {
getFolderByPath,
getFolderIdFromServiceToken,
searchByFolderId,
} from "../../services/FolderService";
@ -45,14 +46,15 @@ export const batchSecrets = async (req: Request, res: Response) => {
const {
workspaceId,
environment,
folderId,
requests,
secretPath,
}: {
workspaceId: string;
environment: string;
folderId: string;
requests: BatchSecretRequest[];
secretPath: string;
} = req.body;
let folderId = req.body.folderId as string;
const createSecrets: 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 (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) {
// do a validation
@ -152,6 +173,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
numberOfSecrets: createdSecrets.length,
environment,
workspaceId,
folderId,
channel,
userAgent: req.headers?.["user-agent"],
},
@ -218,7 +240,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags: u.tags,
folder: u.folder
folder: u.folder,
})
);
@ -248,6 +270,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
numberOfSecrets: updateSecrets.length,
environment,
workspaceId,
folderId,
channel,
userAgent: req.headers?.["user-agent"],
},
@ -395,8 +418,13 @@ export const createSecrets = async (req: Request, res: Response) => {
const {
workspaceId,
environment,
folderId,
}: { workspaceId: string; environment: string; folderId: string } = req.body;
secretPath,
}: {
workspaceId: string;
environment: string;
secretPath?: string;
} = req.body;
let folderId = req.body.folderId;
if (req.user) {
const hasAccess = await userHasWorkspaceAccess(
@ -421,6 +449,24 @@ export const createSecrets = async (req: Request, res: Response) => {
// case: create 1 secret
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
const salt = await SecretService.getSecretBlindIndexSalt({
@ -585,6 +631,7 @@ export const createSecrets = async (req: Request, res: Response) => {
environment,
workspaceId,
channel: channel,
folderId,
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 (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) throw BadRequestError({ message: "Folder not found" });
const folder = getFolderByPath(folders.nodes, secretPath as string);
@ -800,6 +859,7 @@ export const getSecrets = async (req: Request, res: Response) => {
environment,
workspaceId,
channel,
folderId,
userAgent: req.headers?.["user-agent"],
},
});
@ -910,13 +970,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
keyEncoding: ENCODING_SCHEME_UTF8,
tags,
...(secretCommentCiphertext !== undefined &&
secretCommentIV &&
secretCommentTag
secretCommentIV &&
secretCommentTag
? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
}
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
}
: {}),
},
},

View File

@ -44,30 +44,7 @@ import {
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj,
} from "../utils/auth";
import Folder from "../models/folder";
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";
};
import { getFolderIdFromServiceToken } from "../services/FolderService";
/**
* Create secret blind index data containing encrypted blind index [salt]

View File

@ -1,15 +1,15 @@
import express from 'express';
import express from "express";
const router = express.Router();
import { Types } from 'mongoose';
import { Types } from "mongoose";
import {
requireAuth,
requireWorkspaceAuth,
requireSecretsAuth,
validateRequest,
} from '../../middleware';
import { validateClientForSecrets } from '../../validation';
import { query, body } from 'express-validator';
import { secretsController } from '../../controllers/v2';
} from "../../middleware";
import { validateClientForSecrets } from "../../validation";
import { query, body } from "express-validator";
import { secretsController } from "../../controllers/v2";
import {
ADMIN,
MEMBER,
@ -21,11 +21,11 @@ import {
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
} from '../../variables';
import { BatchSecretRequest } from '../../types/secret';
} from "../../variables";
import { BatchSecretRequest } from "../../types/secret";
router.post(
'/batch',
"/batch",
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
@ -35,12 +35,13 @@ router.post(
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationWorkspaceId: "body",
}),
body('workspaceId').exists().isString().trim(),
body('folderId').default('root').isString().trim(),
body('environment').exists().isString().trim(),
body('requests')
body("workspaceId").exists().isString().trim(),
body("folderId").default("root").isString().trim(),
body("environment").exists().isString().trim(),
body("secretPath").optional().isString().trim(),
body("requests")
.exists()
.custom(async (requests: BatchSecretRequest[], { req }) => {
if (Array.isArray(requests)) {
@ -65,17 +66,18 @@ router.post(
);
router.post(
'/',
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('folderId').default('root').isString().trim(),
body('secrets')
"/",
body("workspaceId").exists().isString().trim(),
body("environment").exists().isString().trim(),
body("folderId").default("root").isString().trim(),
body("secretPath").optional().isString().trim(),
body("secrets")
.exists()
.custom((value) => {
if (Array.isArray(value)) {
// case: create multiple secrets
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) {
if (
!secret.type ||
@ -85,16 +87,16 @@ router.post(
!secret.secretKeyCiphertext ||
!secret.secretKeyIV ||
!secret.secretKeyTag ||
typeof secret.secretValueCiphertext !== 'string' ||
typeof secret.secretValueCiphertext !== "string" ||
!secret.secretValueIV ||
!secret.secretValueTag
) {
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
if (
!value.type ||
@ -107,11 +109,11 @@ router.post(
!value.secretValueTag
) {
throw new Error(
'secrets object is missing required secret properties'
"secrets object is missing required secret properties"
);
}
} 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;
@ -126,19 +128,20 @@ router.post(
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'body',
locationEnvironment: 'body',
locationWorkspaceId: "body",
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
}),
secretsController.createSecrets
);
router.get(
'/',
query('workspaceId').exists().trim(),
query('environment').exists().trim(),
query('tagSlugs'),
query('folderId').default('root').isString().trim(),
"/",
query("workspaceId").exists().trim(),
query("environment").exists().trim(),
query("tagSlugs"),
query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [
@ -150,34 +153,34 @@ router.get(
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'query',
locationEnvironment: 'query',
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
}),
secretsController.getSecrets
);
router.patch(
'/',
body('secrets')
"/",
body("secrets")
.exists()
.custom((value) => {
if (Array.isArray(value)) {
// case: update multiple secrets
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) {
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
if (!value.id) {
throw new Error('secret must contain a ID property');
throw new Error("secret must contain a ID property");
}
} 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;
@ -198,21 +201,21 @@ router.patch(
);
router.delete(
'/',
body('secretIds')
"/",
body("secretIds")
.exists()
.custom((value) => {
// case: delete 1 secret
if (typeof value === 'string') return true;
if (typeof value === "string") return true;
if (Array.isArray(value)) {
// case: delete multiple secrets
if (value.length === 0)
throw new Error('secrets cannot be an empty array');
return value.every((id: string) => typeof id === 'string');
throw new Error("secrets cannot be an empty array");
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()
.isEmpty(),

View File

@ -1,5 +1,6 @@
import { nanoid } from "nanoid";
import { TFolderSchema } from "../models/folder";
import { Types } from "mongoose";
import Folder, { TFolderSchema } from "../models/folder";
type TAppendFolderDTO = {
folderName: string;
@ -192,3 +193,25 @@ export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => {
}
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";
};