Compare commits

...

4 Commits

Author SHA1 Message Date
caa0dc6e3f Comment out v4/environments v4/folders items 2023-11-18 14:03:26 +02:00
4d28a66572 Checkpoint folders v4 2023-11-17 22:45:30 +02:00
7346b2ff34 Finish preliminary v4/secret without approvals 2023-11-17 16:50:23 +02:00
ab361b1315 Init scaffolding for v4/secrets 2023-11-17 12:52:53 +02:00
17 changed files with 979 additions and 211 deletions

View File

@ -3,130 +3,24 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets";
import {
checkSecretsPermission,
containsGlobPatterns,
repackageSecretV3ToRaw
} from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { Folder, IMembership, IServiceTokenData, IServiceTokenDataV3 } from "../../models";
import { Permission } from "../../models/serviceTokenDataV3";
import { Folder, IServiceTokenData } from "../../models";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secrets";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataV3ClientForWorkspace
} from "../../validation";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
import { ActorType } from "../../ee/models";
import { UnauthorizedRequestError } from "../../utils/errors";
import { AuthData } from "../../interfaces/middleware";
import { ProjectPermissionActions } from "../../ee/services/ProjectRoleService";
import {
generateSecretApprovalRequest,
getSecretPolicyOfBoard
} from "../../ee/services/SecretApprovalService";
import { CommitType } from "../../ee/models/secretApprovalRequest";
import { IRole } from "../../ee/models/role";
const checkSecretsPermission = async ({
authData,
workspaceId,
environment,
secretPath,
secretAction
}: {
authData: AuthData;
workspaceId: string;
environment: string;
secretPath: string;
secretAction: ProjectPermissionActions; // CRUD
}): Promise<{
authVerifier: (env: string, secPath: string) => boolean;
membership?: Omit<IMembership, "customRole"> & { customRole: IRole };
}> => {
let STV2RequiredPermissions = [];
let STV3RequiredPermissions: Permission[] = [];
switch (secretAction) {
case ProjectPermissionActions.Create:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Read:
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
STV3RequiredPermissions = [Permission.READ];
break;
case ProjectPermissionActions.Edit:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Delete:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
}
switch (authData.actor.type) {
case ActorType.USER: {
const { permission, membership } = await getUserProjectPermissions(
authData.actor.metadata.userId,
workspaceId
);
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
return {
authVerifier: (env: string, secPath: string) =>
permission.can(
secretAction,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
),
membership
};
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV2RequiredPermissions
});
return { authVerifier: () => true };
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData,
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV3RequiredPermissions
});
return {
authVerifier: (env: string, secPath: string) =>
isValidScopeV3({
authPayload: authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: STV3RequiredPermissions
})
};
}
default: {
throw UnauthorizedRequestError();
}
}
};
/**
* Return secrets for workspace with id [workspaceId] and environment
@ -253,21 +147,21 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
);
return res.status(200).send({
secrets: secrets.map((secret) =>
repackageSecretToRaw({
repackageSecretV3ToRaw({
secret,
key
})
),
imports: importedSecrets.map((el) => ({
...el,
secrets: el.secrets.map((secret) => repackageSecretToRaw({ secret, key }))
secrets: el.secrets.map((secret) => repackageSecretV3ToRaw({ secret, key }))
}))
});
}
return res.status(200).send({
secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({
const rep = repackageSecretV3ToRaw({
secret,
key
});
@ -376,7 +270,7 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret: repackageSecretV3ToRaw({
secret,
key
})
@ -522,19 +416,11 @@ export const createSecretRaw = async (req: Request, res: Response) => {
skipMultilineEncoding
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: repackageSecretToRaw({
secret: repackageSecretV3ToRaw({
secret: secretWithoutBlindIndex,
key
})
@ -651,16 +537,8 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
skipMultilineEncoding
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret: repackageSecretV3ToRaw({
secret,
key
})
@ -773,7 +651,7 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret: repackageSecretV3ToRaw({
secret,
key
})
@ -960,14 +838,6 @@ export const createSecret = async (req: Request, res: Response) => {
skipMultilineEncoding
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
@ -1073,14 +943,6 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretKeyIV
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return res.status(200).send({
secret
});
@ -1137,14 +999,6 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secretPath
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return res.status(200).send({
secret
});
@ -1189,14 +1043,6 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
authData: req.authData
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return res.status(200).send({
secrets: createdSecrets
});
@ -1241,14 +1087,6 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
authData: req.authData
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return res.status(200).send({
secrets: updatedSecrets
});
@ -1293,14 +1131,6 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
authData: req.authData
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return res.status(200).send({
secrets: deletedSecrets
});

View File

@ -0,0 +1,42 @@
import { Request, Response } from "express";
// import { validateRequest } from "../../helpers/validation";
// import * as reqValidator from "../../validation/environments";
/**
* Create environment with name [environmentName] and slug [environmentSlug]
* in project with id [projectId]
* @param req
* @param res
*/
export const createEnvironment = async (req: Request, res: Response) => {
// TODO
}
/**
* Return list of environments in project with id [projectId]
* @param req
* @param res
*/
export const getEnvironments = async (req: Request, res: Response) => {
// TODO
}
/**
* Delete environment with name [environmentName] and slug [environmentSlug]
* in project with id [projectId]
* @param req
* @param res
*/
export const updateEnvironment = async (req: Request, res: Response) => {
// TODO
}
/**
* Delete environment with name [environmentName] and slug [environmentSlug]
* in project with id [projectId]
* @param req
* @param res
*/
export const deleteEnvironment = async (req: Request, res: Response) => {
// TODO
}

View File

@ -0,0 +1,87 @@
import { Request, Response } from "express";
// import { Types } from "mongoose";
// import { FolderVersion } from "../../ee/models";
// import { Folder } from "../../models";
// import { appendFolder } from "../../services/FolderService";
// import { validateRequest } from "../../helpers/validation";
// import * as reqValidator from "../../validation/folders";
// import { ResourceNotFoundError } from "../../utils/errors";
/**
*
* Return list of folders in project with id [projectId]
* @param req
* @param res
* @returns
*/
export const getFolders = async (req: Request, res: Response) => {
// TODO
}
/**
*
* Create folder with name [folderName] in project with id [projectId]
* under path [path]
* @param req
* @param res
* @returns
*/
export const createFolder = async (req: Request, res: Response) => {
// TODO
// const {
// body: {
// folderName,
// projectId,
// environmentSlug,
// path
// }
// } = await validateRequest(reqValidator.CreateFolderV2, req);
// // get folder
// const folders = await Folder.findOne({
// workspace: new Types.ObjectId(projectId),
// environment: environmentSlug
// });
// if (!folders) throw ResourceNotFoundError();
// const { parent, child: folder, hasCreated } = appendFolder(folders.nodes, { folderName, directory: path });
// if (!hasCreated) return res.json({ folder });
// await Folder.findByIdAndUpdate(folders._id, folders);
// await new FolderVersion({
// project: new Types.ObjectId(projectId),
// nodes: parent
// }).save();
// return res.status(200).send({
// folder
// });
}
/**
*
* Update folder with name [folderName] in project with id [projectId]
* under path [path]
* @param req
* @param res
* @returns
*/
export const updateFolder = async (req: Request, res: Response) => {
// TODO
}
/**
* Delete folder with name [folderName] in project with id [projectId]
* under path [path]
* @param req
* @param res
* @returns
*/
export const deleteFolder = async (req: Request, res: Response) => {
// TODO
}

View File

@ -0,0 +1,9 @@
import * as secretsController from "./secretsController";
import * as environmentsController from "./environmentsController";
import * as foldersController from "./foldersController";
export {
secretsController,
environmentsController,
foldersController
}

View File

@ -0,0 +1,353 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { BotService, SecretService } from "../../services";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { ProjectPermissionActions } from "../../ee/services/ProjectRoleService";
import { checkSecretsPermission, packageSecretV4 } from "../../helpers";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secrets";
import { getFolderIdFromServiceToken } from "../../services/FolderService";
/**
* Get secrets in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const {
query: {
projectId,
environmentSlug,
path,
includeImports
}
} = await validateRequest(reqValidator.GetSecretsV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Read
});
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
secretPath: path,
authData: req.authData
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
let packagedSecrets = secrets.map((secret) => packageSecretV4({
secret,
key
}));
const folderId = await getFolderIdFromServiceToken(projectId, environmentSlug, path);
if (includeImports) {
const importGroups = await getAllImportedSecrets(
projectId,
environmentSlug,
folderId,
() => true
);
importGroups.forEach((importGroup) => {
packagedSecrets = packagedSecrets.concat(
importGroup.secrets.map((secret) => packageSecretV4({ secret, key }))
);
});
}
return res.status(200).send({
secrets: packagedSecrets
});
}
/**
* Get secret named [secretName] in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const getSecret = async (req: Request, res: Response) => {
const {
params: {
secretName
},
query: {
projectId,
environmentSlug,
path,
type,
includeImports
}
} = await validateRequest(reqValidator.GetSecretV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Read
});
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
type,
secretPath: path,
authData: req.authData,
include_imports: includeImports
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
const packagedSecret = packageSecretV4({
secret,
key
});
return res.status(200).send({
secret: packagedSecret
});
}
/**
* Create secret named [secretName] in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const createSecret = async (req: Request, res: Response) =>{
const {
params: {
secretName
},
body: {
projectId,
environmentSlug,
path,
type,
secretValue,
secretComment,
skipMultilineEncoding
}
} = await validateRequest(reqValidator.CreateSecretV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Create
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretName,
key
});
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue,
key
});
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment,
key
});
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath: path,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
skipMultilineEncoding
});
const packagedSecret = packageSecretV4({
secret,
key
});
return res.status(200).send({
secret: packagedSecret
});
}
/**
* Create secret named [secretName] in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const {
params: {
secretName
},
body: {
projectId,
environmentSlug,
path,
type,
secretValue,
secretComment,
skipMultilineEncoding
}
} = await validateRequest(reqValidator.UpdateSecretV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Edit
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
let secretValueCiphertext, secretValueIV, secretValueTag;
if (secretValue) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue,
key
});
secretValueCiphertext = ciphertext;
secretValueIV = iv;
secretValueTag = tag;
}
let secretCommentCiphertext, secretCommentIV, secretCommentTag;
if (secretComment) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment,
key
});
secretCommentCiphertext = ciphertext;
secretCommentIV = iv;
secretCommentTag = tag;
}
const secret = await SecretService.updateSecret({
secretName,
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretPath: path,
skipMultilineEncoding
});
const packagedSecret = packageSecretV4({
secret,
key
});
return res.status(200).send({
secret: packagedSecret
});
}
/**
* Delete secret named [secretName] in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const deleteSecret = async (req: Request, res: Response) => {
const {
params: {
secretName
},
body: {
projectId,
environmentSlug,
path,
type
}
} = await validateRequest(reqValidator.DeleteSecretV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Delete
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
type,
authData: req.authData,
secretPath: path
});
const packagedSecret = packageSecretV4({
secret,
key
});
return res.status(200).send({
secret: packagedSecret
});
}

View File

@ -1,5 +1,5 @@
import { Types } from "mongoose";
import { EVENT_PULL_SECRETS, EVENT_PUSH_SECRETS } from "../variables";
import { EVENT_PUSH_SECRETS } from "../variables";
interface PushSecret {
ciphertextKey: string;
@ -37,18 +37,4 @@ const eventPushSecrets = ({
};
};
/**
* Return event for pulling secrets
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to pull secrets from
* @returns
*/
const eventPullSecrets = ({ workspaceId }: { workspaceId: string }) => {
return {
name: EVENT_PULL_SECRETS,
workspaceId,
payload: {}
};
};
export { eventPushSecrets };

View File

@ -11,6 +11,7 @@ import {
} from "../interfaces/services/SecretService";
import {
Folder,
IMembership,
ISecret,
IServiceTokenData,
IServiceTokenDataV3,
@ -20,7 +21,7 @@ import {
TFolderRootSchema
} from "../models";
import { Permission } from "../models/serviceTokenDataV3";
import { EventType, SecretVersion } from "../ee/models";
import { ActorType, EventType, IRole, SecretVersion } from "../ee/models";
import {
BadRequestError,
InternalServerError,
@ -33,6 +34,8 @@ import {
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
K8_USER_AGENT_NAME,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL,
SECRET_SHARED
} from "../variables";
@ -42,7 +45,8 @@ import {
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8
} from "../utils/crypto";
import { TelemetryService } from "../services";
import { EventService, TelemetryService } from "../services";
import { eventPushSecrets } from "../events";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { EEAuditLogService, EESecretService } from "../ee/services";
import { getAuthDataPayloadUserObj } from "../utils/authn/helpers";
@ -50,6 +54,112 @@ import { getFolderByPath, getFolderIdFromServiceToken } from "../services/Folder
import picomatch from "picomatch";
import path from "path";
import { getAnImportedSecret } from "../services/SecretImportService";
import { AuthData } from "../interfaces/middleware";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataV3ClientForWorkspace
} from "../validation";
export const checkSecretsPermission = async ({
authData,
workspaceId,
environment,
secretPath,
secretAction
}: {
authData: AuthData;
workspaceId: string;
environment: string;
secretPath: string;
secretAction: ProjectPermissionActions; // CRUD
}): Promise<{
authVerifier: (env: string, secPath: string) => boolean;
membership?: Omit<IMembership, "customRole"> & { customRole: IRole };
}> => {
let STV2RequiredPermissions = [];
let STV3RequiredPermissions: Permission[] = [];
switch (secretAction) {
case ProjectPermissionActions.Create:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Read:
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
STV3RequiredPermissions = [Permission.READ];
break;
case ProjectPermissionActions.Edit:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Delete:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
}
switch (authData.actor.type) {
case ActorType.USER: {
const { permission, membership } = await getUserProjectPermissions(
authData.actor.metadata.userId,
workspaceId
);
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
return {
authVerifier: (env: string, secPath: string) =>
permission.can(
secretAction,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
),
membership
};
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV2RequiredPermissions
});
return { authVerifier: () => true };
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData,
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV3RequiredPermissions
});
return {
authVerifier: (env: string, secPath: string) =>
isValidScopeV3({
authPayload: authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: STV3RequiredPermissions
})
};
}
default: {
throw UnauthorizedRequestError();
}
}
};
/**
* Validate scope for service token v3
@ -119,13 +229,12 @@ const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "Folder not found" });
/**
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
*
* Precondition: the workspace for secret [secret] must have E2EE disabled
* @param {ISecret} secret - secret to repackage to raw
* @param {String} key - symmetric key to use to decrypt secret
* @returns
*/
export const repackageSecretToRaw = ({ secret, key }: { secret: ISecret; key: string }) => {
export const repackageSecretV3ToRaw = ({ secret, key }: { secret: ISecret; key: string }) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
@ -160,7 +269,8 @@ export const repackageSecretToRaw = ({ secret, key }: { secret: ISecret; key: st
user: secret.user,
secretKey,
secretValue,
secretComment
secretComment,
skipMultilineEncoding: secret.skipMultilineEncoding
};
};
@ -517,6 +627,14 @@ export const createSecretHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return secret;
};
@ -755,7 +873,7 @@ export const updateSecretHelper = async ({
secretValueIV,
secretValueTag,
secretPath,
tags,
tags, // maybe this can accept just a secretComment?
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
@ -850,6 +968,9 @@ export const updateSecretHelper = async ({
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
@ -877,9 +998,12 @@ export const updateSecretHelper = async ({
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
secretCommentCiphertext: secret.secretCommentCiphertext,
secretCommentIV: secret.secretCommentIV,
secretCommentTag: secret.secretCommentTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
@ -933,6 +1057,14 @@ export const updateSecretHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return secret;
};
@ -1060,6 +1192,14 @@ export const deleteSecretHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return {
secrets,
secret
@ -1407,6 +1547,14 @@ export const createSecretBatchHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return newlyCreatedSecrets;
};
@ -1602,6 +1750,14 @@ export const updateSecretBatchHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return;
};
@ -1725,7 +1881,54 @@ export const deleteSecretBatchHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return {
secrets: deletedSecrets
};
};
// --- v4/secrets helpers
export const packageSecretV4 = ({
secret,
key
}: {
secret: ISecret;
key: string;
}) => {
const {
_id,
version,
workspace,
type,
environment,
user,
secretKey,
secretValue,
secretComment,
skipMultilineEncoding
} = repackageSecretV3ToRaw({
secret,
key
});
return ({
_id,
version,
projectId: workspace,
environmentSlug: environment,
type,
user,
secretName: secretKey,
secretValue,
secretComment,
skipMultilineEncoding
});
}

View File

@ -77,6 +77,9 @@ import {
users as v3UsersRouter,
workspaces as v3WorkspacesRouter
} from "./routes/v3";
import {
secrets as v4SecretsRouter
} from "./routes/v4";
import { healthCheck } from "./routes/status";
// import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors";
@ -252,6 +255,9 @@ const main = async () => {
app.use("/api/v3/workspaces", v3WorkspacesRouter);
app.use("/api/v3/signup", v3SignupRouter);
app.use("/api/v3/us", v3UsersRouter);
// v4 routes (user-facing)
app.use("/api/v4/secrets", v4SecretsRouter);
// api docs
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile));

View File

@ -51,9 +51,9 @@ export interface UpdateSecretParams {
environment: string;
type: "shared" | "personal";
authData: AuthData;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueCiphertext?: string;
secretValueIV?: string;
secretValueTag?: string;
secretPath: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;

View File

@ -120,6 +120,7 @@ const secretSchema = new Schema<ISecret>(
},
skipMultilineEncoding: {
type: Boolean,
default: false,
required: false
},
algorithm: {

View File

@ -0,0 +1,39 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../middleware";
import { environmentsController } from "../../controllers/v4";
// import { AuthMode } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: []
}),
environmentsController.createEnvironment
);
router.get(
"/",
requireAuth({
acceptedAuthModes: []
}),
environmentsController.getEnvironments
);
router.patch(
"/:environmentSlug",
requireAuth({
acceptedAuthModes: []
}),
environmentsController.updateEnvironment
);
router.delete(
"/:environmentSlug",
requireAuth({
acceptedAuthModes: []
}),
environmentsController.deleteEnvironment
)
export default router;

View File

@ -0,0 +1,39 @@
import express from "express";
const router = express.Router();
import { requireAuth} from "../../middleware";
import { foldersController } from "../../controllers/v4";
// import { AuthMode } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: []
}),
foldersController.createFolder
);
router.get(
"/",
requireAuth({
acceptedAuthModes: []
}),
foldersController.getFolders
);
router.patch(
":/folderName",
requireAuth({
acceptedAuthModes: []
}),
foldersController.updateFolder
);
router.delete(
"/:folderName",
requireAuth({
acceptedAuthModes: []
}),
foldersController.deleteFolder
);
export default router;

View File

@ -0,0 +1,5 @@
import secrets from "./secrets";
export {
secrets
}

View File

@ -0,0 +1,46 @@
import express from "express";
const router = express.Router();
import { requireAuth} from "../../middleware";
import { secretsController } from "../../controllers/v4";
router.get(
"/",
requireAuth({
acceptedAuthModes: []
}),
secretsController.getSecrets
);
router.get(
"/:secretName",
requireAuth({
acceptedAuthModes: []
}),
secretsController.getSecret
);
router.post(
"/:secretName",
requireAuth({
acceptedAuthModes: []
}),
secretsController.createSecret
);
router.patch(
"/:secretName",
requireAuth({
acceptedAuthModes: []
}),
secretsController.updateSecret
);
router.delete(
"/:secretName",
requireAuth({
acceptedAuthModes: []
}),
secretsController.deleteSecret
);
export default router;

View File

@ -47,3 +47,39 @@ export const ReorderWorkspaceEnvironmentsV2 = z.object({
otherEnvironmentName: z.string().trim()
})
});
export const CreateEnvironmentV4 = z.object({
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
environmentName: z.string().trim()
})
});
export const GetEnvironmentsV4 = z.object({
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
environmentName: z.string().trim()
})
});
export const UpdateEnvironmentV4 = z.object({
params: z.object({
environmentSlug: z.string().trim(),
}),
body: z.object({
projectId: z.string().trim(),
environmentName: z.string().trim().optional(),
newEnvironmentSlug: z.string().trim().optional()
})
});
export const DeleteEnvironmentV4 = z.object({
params: z.object({
environmentSlug: z.string().trim()
}),
body: z.object({
projectId: z.string().trim()
})
});

View File

@ -39,3 +39,12 @@ export const GetFoldersV1 = z.object({
directory: z.string().trim().default("/")
})
});
export const CreateFolderV2 = z.object({
body: z.object({
folderName: z.string().trim(),
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/")
})
});

View File

@ -449,3 +449,80 @@ export const DeleteSecretByNameBatchV3 = z.object({
.min(1)
})
});
export const GetSecretsV4 = z.object({
query: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
includeImports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
})
});
export const GetSecretV4 = z.object({
params: z.object({
secretName: z.string().trim()
}),
query: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_PERSONAL),
includeImports: z
.enum(["true", "false"])
.default("true")
.transform((value) => value === "true")
})
});
export const CreateSecretV4 = z.object({
params: z.object({
secretName: z.string().trim()
}),
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
secretComment: z.string().trim().optional().default(""),
skipMultilineEncoding: z.boolean().optional().default(false),
})
});
export const UpdateSecretV4 = z.object({
params: z.object({
secretName: z.string().trim()
}),
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.optional(),
secretComment: z.string().trim().optional(),
skipMultilineEncoding: z.boolean().optional()
})
});
export const DeleteSecretV4 = z.object({
params: z.object({
secretName: z.string().trim()
}),
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED)
})
});