From 17567ebd0fea309456e6fc2e70554babceff7751 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 3 Oct 2024 23:23:27 +0530 Subject: [PATCH] feat: completed easier changes on other files where permission is needed --- .../access-approval-policy-dal.ts | 2 +- .../access-approval-policy-fns.ts | 2 +- .../access-approval-policy-service.ts | 68 -- .../access-approval-request-service.ts | 18 - .../secret-approval-policy-dal.ts | 2 +- .../secret-approval-policy-service.ts | 2 +- .../secret-approval-request-service.ts | 6 +- .../secret-replication-service.ts | 16 +- .../secret-rotation-service.ts | 3 +- .../secret-snapshot-service.ts | 8 +- backend/src/lib/casl/knex.ts | 111 --- backend/src/lib/knex/dynamic.ts | 21 +- backend/src/lib/knex/index.ts | 7 +- .../integration/integration-service.ts | 14 +- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 104 ++- .../secret-v2-bridge/secret-v2-bridge-fns.ts | 57 +- .../secret-v2-bridge-service.ts | 674 ++++++++++++------ .../secret-v2-bridge-types.ts | 6 + backend/src/services/secret/secret-fns.ts | 10 +- backend/src/services/secret/secret-queue.ts | 4 +- backend/src/services/secret/secret-service.ts | 49 +- .../service-token/service-token-service.ts | 2 +- 22 files changed, 676 insertions(+), 510 deletions(-) delete mode 100644 backend/src/lib/casl/knex.ts diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index 569580507..220701410 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -14,7 +14,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { const accessApprovalPolicyFindQuery = async ( tx: Knex, - filter: TFindFilter, + filter: TFindFilter, customFilter?: { policyId?: string; } diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts index 0e242f04c..92b5c1449 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts @@ -26,7 +26,7 @@ export const isApproversValid = async ({ ForbiddenError.from(approverPermission).throwUnlessCan( ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath, secretName: "", secretTags: [] }) ); } } catch { diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 2633a026f..f5151b0b7 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -11,7 +11,6 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TGroupDALFactory } from "../group/group-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; -import { isApproversValid } from "./access-approval-policy-fns"; import { ApproverType, TCreateAccessApprovalPolicy, @@ -132,22 +131,6 @@ export const accessApprovalPolicyServiceFactory = ({ .map((user) => user.id); verifyAllApprovers.push(...verifyGroupApprovers); - const approversValid = await isApproversValid({ - projectId: project.id, - orgId: actorOrgId, - envSlug: environment, - secretPath, - actorAuthMethod, - permissionService, - userIds: verifyAllApprovers - }); - - if (!approversValid) { - throw new BadRequestError({ - message: "One or more approvers doesn't have access to be specified secret path" - }); - } - const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const doc = await accessApprovalPolicyDAL.create( { @@ -289,22 +272,6 @@ export const accessApprovalPolicyServiceFactory = ({ userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id)); } - const approversValid = await isApproversValid({ - projectId: accessApprovalPolicy.projectId, - orgId: actorOrgId, - envSlug: accessApprovalPolicy.environment.slug, - secretPath: doc.secretPath!, - actorAuthMethod, - permissionService, - userIds: userApproverIds - }); - - if (!approversValid) { - throw new BadRequestError({ - message: "One or more approvers doesn't have access to be specified secret path" - }); - } - await accessApprovalPolicyApproverDAL.insertMany( userApproverIds.map((userId) => ({ approverUserId: userId, @@ -315,41 +282,6 @@ export const accessApprovalPolicyServiceFactory = ({ } if (groupApprovers) { - const usersPromises: Promise< - { - id: string; - email: string | null | undefined; - username: string; - firstName: string | null | undefined; - lastName: string | null | undefined; - isPartOfGroup: boolean; - }[] - >[] = []; - - for (const groupId of groupApprovers) { - usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 })); - } - const verifyGroupApprovers = (await Promise.all(usersPromises)) - .flat() - .filter((user) => user.isPartOfGroup) - .map((user) => user.id); - - const approversValid = await isApproversValid({ - projectId: accessApprovalPolicy.projectId, - orgId: actorOrgId, - envSlug: accessApprovalPolicy.environment.slug, - secretPath: doc.secretPath!, - actorAuthMethod, - permissionService, - userIds: verifyGroupApprovers - }); - - if (!approversValid) { - throw new BadRequestError({ - message: "One or more approvers doesn't have access to be specified secret path" - }); - } - await accessApprovalPolicyApproverDAL.insertMany( groupApprovers.map((groupId) => ({ approverGroupId: groupId, diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 7c1f00a37..1e2c67f02 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -17,7 +17,6 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; -import { isApproversValid } from "../access-approval-policy/access-approval-policy-fns"; import { TGroupDALFactory } from "../group/group-dal"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; @@ -78,7 +77,6 @@ export const accessApprovalRequestServiceFactory = ({ permissionService, accessApprovalRequestDAL, accessApprovalRequestReviewerDAL, - projectMembershipDAL, accessApprovalPolicyDAL, accessApprovalPolicyApproverDAL, additionalPrivilegeDAL, @@ -323,22 +321,6 @@ export const accessApprovalRequestServiceFactory = ({ throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" }); } - const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id); - - const approversValid = await isApproversValid({ - projectId: accessApprovalRequest.projectId, - orgId: actorOrgId, - envSlug: accessApprovalRequest.environment, - secretPath: accessApprovalRequest.policy.secretPath!, - actorAuthMethod, - permissionService, - userIds: [reviewerProjectMembership.userId] - }); - - if (!approversValid) { - throw new ForbiddenRequestError({ message: "You don't have access to approve this request" }); - } - const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts index e3526b0e3..bb77660aa 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts @@ -14,7 +14,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { const secretApprovalPolicyFindQuery = ( tx: Knex, - filter: TFindFilter, + filter: TFindFilter, customFilter?: { sapId?: string; } diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 1908e75af..bcff5bd3a 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -353,7 +353,7 @@ export const secretApprovalPolicyServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { secretPath, environment }) + subject(ProjectPermissionSub.Secrets, { secretPath, environment, secretName: "", secretTags: [] }) ); return getSecretApprovalPolicy(projectId, environment, secretPath); }; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index bbc099956..19ecee99d 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -43,7 +43,7 @@ import { fnSecretBulkDelete as fnSecretV2BridgeBulkDelete, fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, - getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge + getAllSecretReferences as getAllNestedSecretReferencesV2Bridge } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; @@ -527,7 +527,7 @@ export const secretApprovalRequestServiceFactory = ({ secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - ) + ).nestedReferences : [], type: SecretType.Shared })), @@ -551,7 +551,7 @@ export const secretApprovalRequestServiceFactory = ({ secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() - ) + ).nestedReferences : [] } : {}; diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 8f74079f7..e76cfe785 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -28,8 +28,8 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret import { fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, - getAllNestedSecretReferences, - getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge + getAllSecretReferences as getAllNestedSecretReferencesV2Bridge, + getAllSecretReferences } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; @@ -416,7 +416,9 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, - references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : [] + references: doc.secretValue + ? getAllNestedSecretReferencesV2Bridge(doc.secretValue).nestedReferences + : [] }; }) }); @@ -442,7 +444,9 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue as Buffer, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, - references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : [] + references: doc.secretValue + ? getAllNestedSecretReferencesV2Bridge(doc.secretValue).nestedReferences + : [] } }; }) @@ -687,7 +691,7 @@ export const secretReplicationServiceFactory = ({ secretCommentTag: doc.secretCommentTag, secretCommentCiphertext: doc.secretCommentCiphertext, skipMultilineEncoding: doc.skipMultilineEncoding, - references: getAllNestedSecretReferences(doc.secretValue) + references: getAllSecretReferences(doc.secretValue).nestedReferences }; }) }); @@ -723,7 +727,7 @@ export const secretReplicationServiceFactory = ({ secretCommentTag: doc.secretCommentTag, secretCommentCiphertext: doc.secretCommentCiphertext, skipMultilineEncoding: doc.skipMultilineEncoding, - references: getAllNestedSecretReferences(doc.secretValue) + references: getAllSecretReferences(doc.secretValue).nestedReferences } }; }) diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 8408463ef..4c3c61b0c 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -97,8 +97,9 @@ export const secretRotationServiceFactory = ({ if (!folder) throw new NotFoundError({ message: "Secret path not found" }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "", secretTags: [] }) ); + const project = await projectDAL.findById(projectId); const shouldUseBridge = project.version === ProjectVersion.V3; diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index de285a0f2..f4795447f 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -95,7 +95,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -127,7 +127,7 @@ export const secretSnapshotServiceFactory = ({ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -214,7 +214,9 @@ export const secretSnapshotServiceFactory = ({ ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: snapshotDetails.environment.slug, - secretPath: fullFolderPath + secretPath: fullFolderPath, + secretName: "", + secretTags: [] }) ); diff --git a/backend/src/lib/casl/knex.ts b/backend/src/lib/casl/knex.ts deleted file mode 100644 index cc9718fe0..000000000 --- a/backend/src/lib/casl/knex.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { AnyAbility, ExtractSubjectType } from "@casl/ability"; -import { AbilityQuery, rulesToQuery } from "@casl/ability/extra"; -import { Tables } from "knex/types/tables"; - -import { BadRequestError, UnauthorizedError } from "../errors"; -import { TKnexDynamicOperator } from "../knex/dynamic"; - -type TBuildKnexQueryFromCaslDTO = { - ability: K; - subject: ExtractSubjectType[1]>; - action: Parameters[0]; -}; - -export const buildKnexQueryFromCaslOperators = ({ - ability, - subject, - action -}: TBuildKnexQueryFromCaslDTO) => { - const query = rulesToQuery(ability, action, subject, (rule) => { - if (!rule.ast) throw new Error("Ast not defined"); - return rule.ast; - }); - - if (query === null) throw new UnauthorizedError({ message: `You don't have permission to do ${action} ${subject}` }); - return query; -}; - -type TFieldMapper = { - [K in T]: `${K}.${Exclude}`; -}[T]; - -type TFormatCaslFieldsWithTableNames = { - // handle if any missing operator else throw error let the app break because this is executing again the db - missingOperatorCallback?: (operator: string) => void; - fieldMapping: (arg: string) => TFieldMapper | null; - dynamicQuery: TKnexDynamicOperator; -}; - -export const formatCaslOperatorFieldsWithTableNames = ({ - missingOperatorCallback = (arg) => { - throw new BadRequestError({ message: `Unknown permission operator: ${arg}` }); - }, - dynamicQuery: dynamicQueryAst, - fieldMapping -}: TFormatCaslFieldsWithTableNames) => { - const stack: [TKnexDynamicOperator, TKnexDynamicOperator | null][] = [[dynamicQueryAst, null]]; - - while (stack.length) { - const [filterAst, parentAst] = stack.pop()!; - - if (filterAst.operator === "and" || filterAst.operator === "or" || filterAst.operator === "not") { - filterAst.value.forEach((el) => { - stack.push([el, filterAst]); - }); - - // eslint-disable-next-line no-continue - continue; - } - - if ( - filterAst.operator === "eq" || - filterAst.operator === "ne" || - filterAst.operator === "in" || - filterAst.operator === "endsWith" || - filterAst.operator === "startsWith" - ) { - const attrPath = fieldMapping(filterAst.field); - if (attrPath) { - filterAst.field = attrPath; - } else if (parentAst && Array.isArray(parentAst.value)) { - parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[]; - } else throw new Error("Unknown casl field"); - // eslint-disable-next-line no-continue - continue; - } - - if (parentAst && Array.isArray(parentAst.value)) { - parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[]; - } else { - missingOperatorCallback?.(filterAst.operator); - } - } - return dynamicQueryAst; -}; - -export const convertCaslOperatorToKnexOperator = ( - caslKnexOperators: AbilityQuery, - fieldMapping: (arg: string) => TFieldMapper | null -) => { - const value = []; - if (caslKnexOperators.$and) { - value.push({ - operator: "not" as const, - value: caslKnexOperators.$and as TKnexDynamicOperator[] - }); - } - if (caslKnexOperators.$or) { - value.push({ - operator: "or" as const, - value: caslKnexOperators.$or as TKnexDynamicOperator[] - }); - } - - return formatCaslOperatorFieldsWithTableNames({ - dynamicQuery: { - operator: "and", - value - }, - fieldMapping - }); -}; diff --git a/backend/src/lib/knex/dynamic.ts b/backend/src/lib/knex/dynamic.ts index c336d7a9e..90530b979 100644 --- a/backend/src/lib/knex/dynamic.ts +++ b/backend/src/lib/knex/dynamic.ts @@ -2,32 +2,35 @@ import { Knex } from "knex"; import { UnauthorizedError } from "../errors"; -type TKnexDynamicPrimitiveOperator = { +type TKnexDynamicPrimitiveOperator = { operator: "eq" | "ne" | "startsWith" | "endsWith"; value: string; - field: string; + field: Extract; }; -type TKnexDynamicInOperator = { +type TKnexDynamicInOperator = { operator: "in"; value: string[] | number[]; - field: string; + field: Extract; }; -type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator; +type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator; -type TKnexGroupOperator = { +type TKnexGroupOperator = { operator: "and" | "or" | "not"; - value: (TKnexNonGroupOperator | TKnexGroupOperator)[]; + value: (TKnexNonGroupOperator | TKnexGroupOperator)[]; }; // akhilmhdh: This is still in pending state and not yet ready. If you want to use it ping me. // used when you need to write a complex query with the orm // use it when you need complex or and and condition - most of the time not needed // majorly used with casl permission to filter data based on permission -export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator; +export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator; -export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => { +export const buildDynamicKnexQuery = ( + dynamicQuery: TKnexDynamicOperator, + rootQueryBuild: Knex.QueryBuilder +) => { const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }]; while (stack.length) { diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index 36d81ae34..226b0b802 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -3,6 +3,7 @@ import { Knex } from "knex"; import { Tables } from "knex/types/tables"; import { DatabaseError } from "../errors"; +import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic"; export * from "./connection"; export * from "./join"; @@ -20,9 +21,10 @@ export const withTransaction = (db: Knex, dal: K) => ({ export type TFindFilter = Partial & { $in?: Partial<{ [k in keyof R]: R[k][] }>; $search?: Partial<{ [k in keyof R]: R[k] }>; + $complex?: TKnexDynamicOperator; }; export const buildFindFilter = - ({ $in, $search, ...filter }: TFindFilter) => + ({ $in, $search, $complex, ...filter }: TFindFilter) => (bd: Knex.QueryBuilder) => { void bd.where(filter); if ($in) { @@ -39,6 +41,9 @@ export const buildFindFilter = } }); } + if ($complex) { + buildDynamicKnexQuery($complex, bd); + } return bd; }; diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 47a92c384..16b5f9936 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -89,7 +89,12 @@ export const integrationServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath, + secretName: "", + secretTags: [] + }) ); const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath); @@ -162,7 +167,12 @@ export const integrationServiceFactory = ({ if (environment || secretPath) { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: newEnvironment, + secretPath: newSecretPath, + secretName: "", + secretTags: [] + }) ); } diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 1a9397e91..a8180c8c6 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -4,7 +4,14 @@ import { validate as uuidValidate } from "uuid"; import { TDbClient } from "@app/db"; import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { + buildFindFilter, + ormify, + selectAllTableCols, + sqlNestRelationships, + TFindFilter, + TFindOpt +} from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -13,6 +20,97 @@ export type TSecretV2BridgeDALFactory = ReturnType { const secretOrm = ormify(db, TableName.SecretV2); + const findOne = async (filter: Partial, tx?: Knex) => { + try { + const docs = await (tx || db)(TableName.SecretV2) + .where(filter) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .select(selectAllTableCols(TableName.SecretV2)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + return data?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretV2}: FindOne` }); + } + }; + + const find = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db)(TableName.SecretV2) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter(filter)) + .leftJoin( + TableName.SecretV2JnTag, + `${TableName.SecretV2}.id`, + `${TableName.SecretV2JnTag}.${TableName.SecretV2}Id` + ) + .leftJoin( + TableName.SecretTag, + `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, + `${TableName.SecretTag}.id` + ) + .select(selectAllTableCols(TableName.SecretV2)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({ + id, + color, + slug, + name: slug + }) + } + ] + }); + return data; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.SecretV2}: FindOne` }); + } + }; + const update = async (filter: Partial, data: Omit, tx?: Knex) => { try { const sec = await (tx || db)(TableName.SecretV2) @@ -484,6 +582,8 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { upsertSecretReferences, findReferencedSecretReferences, findAllProjectSecretValues, - countByFolderIds + countByFolderIds, + findOne, + find }; }; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 1ae7ce6dc..1b471c985 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -30,9 +30,10 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3; * // { environment: 'prod', secretPath: '/anotherFolder' } * // ] */ -export const getAllNestedSecretReferences = (maybeSecretReference: string) => { +export const getAllSecretReferences = (maybeSecretReference: string) => { const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]); - return references + + const nestedReferences = references .filter((el) => el.includes(".")) .map((el) => { const [environment, ...secretPathList] = el.split("."); @@ -42,6 +43,8 @@ export const getAllNestedSecretReferences = (maybeSecretReference: string) => { secretKey: secretPathList[secretPathList.length - 1] }; }); + const localReferences = references.filter((el) => !el.includes(".")); + return { nestedReferences, localReferences }; }; // these functions are special functions shared by a couple of resources @@ -325,16 +328,13 @@ type TRecursivelyFetchSecretsFromFoldersArg = { projectId: string; environment: string; currentPath: string; - hasAccess: (environment: string, secretPath: string) => boolean; }; export const recursivelyGetSecretPaths = async ({ folderDAL, projectEnvDAL, projectId, - environment, - currentPath, - hasAccess + environment }: TRecursivelyFetchSecretsFromFoldersArg) => { const env = await projectEnvDAL.findOne({ projectId, @@ -360,12 +360,7 @@ export const recursivelyGetSecretPaths = async ({ folderId: p.folderId })); - // Filter out paths that the user does not have permission to access, and paths that are not in the current path - const allowedPaths = paths.filter( - (folder) => hasAccess(environment, folder.path) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) - ); - - return allowedPaths; + return paths; }; // used to convert multi line ones to quotes ones with \n const formatMultiValueEnv = (val?: string) => { @@ -379,7 +374,7 @@ type TInterpolateSecretArg = { decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined; secretDAL: Pick; folderDAL: Pick; - canExpandValue: (environment: string, secretPath: string) => boolean; + canExpandValue: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; }; const MAX_SECRET_REFERENCE_DEPTH = 10; @@ -390,29 +385,29 @@ export const expandSecretReferencesFactory = ({ folderDAL, canExpandValue }: TInterpolateSecretArg) => { - const secretCache: Record> = {}; + const secretCache: Record> = {}; const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`; const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => { const cacheKey = getCacheUniqueKey(environment, secretPath); if (secretCache?.[cacheKey]) { - return secretCache[cacheKey][secretKey] || ""; + return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; } const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) return ""; + if (!folder) return { value: "", tags: [] }; const secrets = await secretDAL.findByFolderId(folder.id); - const decryptedSecret = secrets.reduce>((prev, secret) => { + const decryptedSecret = secrets.reduce>((prev, secret) => { // eslint-disable-next-line no-param-reassign - prev[secret.key] = decryptSecret(secret.encryptedValue) || ""; + prev[secret.key] = { value: decryptSecret(secret.encryptedValue) || "", tags: secret.tags?.map((el) => el.slug) }; return prev; }, {}); secretCache[cacheKey] = decryptedSecret; - return secretCache[cacheKey][secretKey] || ""; + return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; }; const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => { @@ -438,43 +433,43 @@ export const expandSecretReferencesFactory = ({ if (entities.length === 1) { const [secretKey] = entities; - if (!canExpandValue(environment, secretPath)) + // eslint-disable-next-line no-continue,no-await-in-loop + const referedValue = await fetchSecret(environment, secretPath, secretKey); + if (!canExpandValue(environment, secretPath, secretKey, referedValue.tags)) throw new ForbiddenRequestError({ message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.` }); - // eslint-disable-next-line no-continue,no-await-in-loop - const referedValue = await fetchSecret(environment, secretPath, secretKey); const cacheKey = getCacheUniqueKey(environment, secretPath); secretCache[cacheKey][secretKey] = referedValue; - if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { + if (INTERPOLATION_SYNTAX_REG.test(referedValue.value)) { stack.push({ - value: referedValue, + value: referedValue.value, secretPath, environment, depth: depth + 1 }); } if (referedValue) { - expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); + expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value); } } else { const secretReferenceEnvironment = entities[0]; const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1)); const secretReferenceKey = entities[entities.length - 1]; - if (!canExpandValue(secretReferenceEnvironment, secretReferencePath)) + // eslint-disable-next-line no-await-in-loop + const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); + if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) throw new ForbiddenRequestError({ message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.` }); - // eslint-disable-next-line no-await-in-loop - const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); secretCache[cacheKey][secretReferenceKey] = referedValue; - if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { + if (INTERPOLATION_SYNTAX_REG.test(referedValue.value)) { stack.push({ - value: referedValue, + value: referedValue.value, secretPath: secretReferencePath, environment: secretReferenceEnvironment, depth: depth + 1 @@ -482,7 +477,7 @@ export const expandSecretReferencesFactory = ({ } if (referedValue) { - expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); + expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value); } } } diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 0d2797800..79c62f825 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1,4 +1,5 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError, PureAbility, subject } from "@casl/ability"; +import { z } from "zod"; import { ProjectMembershipRole, SecretsV2Schema, SecretType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -28,7 +29,7 @@ import { fnSecretBulkDelete, fnSecretBulkInsert, fnSecretBulkUpdate, - getAllNestedSecretReferences, + getAllSecretReferences, recursivelyGetSecretPaths, reshapeBridgeSecret } from "./secret-v2-bridge-fns"; @@ -43,6 +44,7 @@ import { TGetSecretsDTO, TGetSecretVersionsDTO, TMoveSecretsDTO, + TSecretReference, TUpdateManySecretDTO, TUpdateSecretDTO } from "./secret-v2-bridge-types"; @@ -56,7 +58,7 @@ type TSecretV2BridgeServiceFactoryDep = { secretVersionTagDAL: Pick; secretTagDAL: TSecretTagDALFactory; permissionService: Pick; - projectEnvDAL: Pick; + projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv" @@ -93,6 +95,70 @@ export const secretV2BridgeServiceFactory = ({ secretApprovalRequestSecretDAL, kmsService }: TSecretV2BridgeServiceFactoryDep) => { + const $validateSecretReferences = async ( + projectId: string, + permission: PureAbility, + references: ReturnType["nestedReferences"] + ) => { + if (!references.length) return; + + const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment))); + const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs); + if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length) + throw new BadRequestError({ message: "Referred environment not found" }); + + const referencesEnvironmentGroupBySlug = groupBy(referencesEnvironments, (i) => i.slug); + const referredFolders = await folderDAL.findByManySecretPath( + references.map((el) => ({ + secretPath: el.secretPath, + envId: referencesEnvironmentGroupBySlug[el.environment][0].id + })) + ); + const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => i?.path as string); + const referredSecrets = await secretDAL.find({ + $complex: { + operator: "or", + value: references.map((el) => { + const folderId = referencesFolderGroupByPath[el.secretPath][0]?.id; + if (!folderId) throw new BadRequestError({ message: `Reference path ${el.secretPath} doesn't exist` }); + + return { + operator: "and", + value: [ + { + operator: "eq", + field: "folderId", + value: folderId + }, + { + operator: "eq", + field: "key", + value: el.secretKey + } + ] + }; + }) + } + }); + if (referredSecrets.length !== references.length) + throw new BadRequestError({ message: "Reference secret not found" }); + + const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); + references.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: el.environment, + secretPath: el.secretPath, + secretName: el.secretKey, + tags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug) + }) + ); + }); + + return referredSecrets; + }; + const createSecret = async ({ actor, actorId, @@ -110,10 +176,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -134,6 +196,7 @@ export const secretV2BridgeServiceFactory = ({ }); if (inputSecret.type === SecretType.Shared && doesSecretExist) throw new BadRequestError({ message: "Secret already exist" }); + // if user creating personal check its shared also exist if (inputSecret.type === SecretType.Personal && !doesSecretExist) { throw new BadRequestError({ @@ -146,22 +209,28 @@ export const secretV2BridgeServiceFactory = ({ const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; if ((inputSecret.tagIds || []).length !== tags.length) throw new NotFoundError({ message: "Tag not found" }); - const { secretName, type, ...el } = inputSecret; - const references = getAllNestedSecretReferences(inputSecret.secretValue); + const { secretName, type, ...inputSecretData } = inputSecret; + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName, + secretTags: tags?.map((el) => el.slug) + }) + ); + + const { nestedReferences, localReferences } = getAllSecretReferences(inputSecret.secretValue); + const allSecretReferences = nestedReferences.concat( + localReferences.map((el) => ({ secretKey: el, secretPath, environment })) + ); + await $validateSecretReferences(projectId, permission, allSecretReferences); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - references.forEach((referredSecret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: referredSecret.environment, - secretPath: referredSecret.secretPath - }) - ); - }); - const secret = await secretDAL.transaction((tx) => fnSecretBulkInsert({ folderId, @@ -169,20 +238,20 @@ export const secretV2BridgeServiceFactory = ({ { version: 1, type, - reminderRepeatDays: el.secretReminderRepeatDays, + reminderRepeatDays: inputSecretData.secretReminderRepeatDays, encryptedComment: setKnexStringValue( - el.secretComment, + inputSecretData.secretComment, (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob ), - encryptedValue: el.secretValue - ? secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob + encryptedValue: inputSecretData.secretValue + ? secretManagerEncryptor({ plainText: Buffer.from(inputSecretData.secretValue) }).cipherTextBlob : undefined, - reminderNote: el.secretReminderNote, - skipMultilineEncoding: el.skipMultilineEncoding, + reminderNote: inputSecretData.secretReminderNote, + skipMultilineEncoding: inputSecretData.skipMultilineEncoding, key: secretName, userId: inputSecret.type === SecretType.Personal ? actorId : null, tagIds: inputSecret.tagIds, - references + references: nestedReferences } ], secretDAL, @@ -228,10 +297,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); if (inputSecret.newSecretName === "") { throw new BadRequestError({ message: "New secret name cannot be empty" }); @@ -276,6 +341,21 @@ export const secretV2BridgeServiceFactory = ({ secret = sharedSecretToModify; } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.secretName, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + + // validate tags + // fetch all tags and if not same count throw error meaning one was invalid tags + const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; + if ((inputSecret.tagIds || []).length !== tags.length) throw new NotFoundError({ message: "Tag not found" }); + if (inputSecret.newSecretName) { const doesNewNameSecretExist = await secretDAL.findOne({ key: inputSecret.newSecretName, @@ -283,36 +363,34 @@ export const secretV2BridgeServiceFactory = ({ folderId }); if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: inputSecret.secretName, + secretTags: tags?.map((el) => el.slug) + }) + ); } - // validate tags - // fetch all tags and if not same count throw error meaning one was invalid tags - const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; - if ((inputSecret.tagIds || []).length !== tags.length) throw new NotFoundError({ message: "Tag not found" }); - const { secretName, secretValue } = inputSecret; const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); - const encryptedValue = - typeof secretValue !== "undefined" - ? { - encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob, - references: getAllNestedSecretReferences(secretValue) - } - : {}; - if (encryptedValue.references) { - encryptedValue.references.forEach((referredSecret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: referredSecret.environment, - secretPath: referredSecret.secretPath - }) - ); - }); + const encryptedValue = secretValue + ? secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob + : undefined; + const secretReferences = secretValue ? getAllSecretReferences(secretValue) : undefined; + + if (secretReferences) { + const { nestedReferences, localReferences } = secretReferences; + const allSecretReferences = nestedReferences.concat( + localReferences.map((el) => ({ secretKey: el, secretPath, environment })) + ); + await $validateSecretReferences(projectId, permission, allSecretReferences); } const updatedSecret = await secretDAL.transaction(async (tx) => @@ -331,7 +409,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: inputSecret.skipMultilineEncoding, key: inputSecret.newSecretName || secretName, tags: inputSecret.tagIds, - ...encryptedValue + ...(encryptedValue ? { encryptedValue, references: secretReferences?.nestedReferences || [] } : {}) } } ], @@ -386,10 +464,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -414,6 +488,15 @@ export const secretV2BridgeServiceFactory = ({ }) }); if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: secretToDelete.key, + secretTags: secretToDelete.tags?.map((el) => el.slug) + }) + ); const deletedSecret = await secretDAL.transaction(async (tx) => fnSecretBulkDelete({ @@ -482,13 +565,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); - // verify user has access to all environments - environments.forEach((environment) => - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ) - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); } const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); @@ -534,10 +611,7 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return 0; @@ -562,22 +636,15 @@ export const secretV2BridgeServiceFactory = ({ environments: string[]; isInternal?: boolean; }) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); if (!isInternal) { - const { permission } = await permissionService.getProjectPermission( - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId - ); - - // verify user has access to all environments - environments.forEach((environment) => - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ) - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); } let paths: { folderId: string; path: string; environment: string }[] = []; @@ -645,6 +712,8 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + let paths: { folderId: string; path: string }[] = []; if (recursive) { @@ -653,26 +722,13 @@ export const secretV2BridgeServiceFactory = ({ projectEnvDAL, projectId, environment, - currentPath: path, - hasAccess: (permissionEnvironment, permissionSecretPath) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: permissionEnvironment, - secretPath: permissionSecretPath - }) - ) + currentPath: path }); if (!deepPaths) return { secrets: [], imports: [] }; paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); } else { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); - const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) return { secrets: [], imports: [] }; @@ -693,27 +749,44 @@ export const secretV2BridgeServiceFactory = ({ projectId }); - const decryptedSecrets = secrets.map((secret) => - reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { - ...secret, - value: secret.encryptedValue - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() - : "", - comment: secret.encryptedComment - ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() - : "" - }) - ); + const decryptedSecrets = secrets + .filter((el) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: groupedPaths[el.folderId][0].path, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) + ) + ) + .map((secret) => + reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { + ...secret, + value: secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : "", + comment: secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : "" + }) + ); const expandSecretReferences = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath) => + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) ) }); @@ -744,19 +817,7 @@ export const secretV2BridgeServiceFactory = ({ } const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); - const allowedImports = secretImports.filter(({ importEnv, importPath, isReplication }) => - !isReplication && - // if its service token allow full access over imported one - actor === ActorType.SERVICE - ? true - : permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) - ); + const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); const importedSecrets = await fnSecretsV2FromImports({ allowedImports, secretDAL, @@ -793,10 +854,7 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) - ); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new NotFoundError({ @@ -832,17 +890,43 @@ export const secretV2BridgeServiceFactory = ({ userId: secretType === SecretType.Personal ? actorId : null, key: secretName }) - .then((el) => SecretsV2Schema.parse({ ...el, id: el.secretId }))); + .then((el) => + SecretsV2Schema.extend({ + tags: z + .object({ slug: z.string(), name: z.string(), id: z.string(), color: z.string() }) + .array() + .default([]) + .optional() + }).parse({ + ...el, + id: el.secretId + }) + )); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: path, + secretName, + secretTags: (secret?.tags || []).map((el) => el.slug) + }) + ); const expandSecretReferences = expandSecretReferencesFactory({ projectId, folderDAL, secretDAL, decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), - canExpandValue: (expandEnvironment, expandSecretPath) => + canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => permission.can( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: expandEnvironment, secretPath: expandSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: expandEnvironment, + secretPath: expandSecretPath, + secretName: expandSecretKey, + secretTags: expandSecretTags + }) ) }); @@ -851,20 +935,8 @@ export const secretV2BridgeServiceFactory = ({ // here we consider the import order also thus starting from bottom if (!secret && includeImports) { const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); - const allowedImports = secretImports.filter(({ importEnv, importPath }) => - // if its service token allow full access over imported one - actor === ActorType.SERVICE - ? true - : permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: importEnv.slug, - secretPath: importPath - }) - ) - ); const importedSecrets = await fnSecretsV2FromImports({ - allowedImports, + allowedImports: secretImports, secretDAL, folderDAL, secretImportDAL, @@ -927,10 +999,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -940,13 +1008,32 @@ export const secretV2BridgeServiceFactory = ({ }); const folderId = folder.id; - const secrets = await secretDAL.findBySecretKeys( + const secrets = await secretDAL.find({ folderId, - inputSecrets.map((el) => ({ - key: el.secretKey, - type: SecretType.Shared - })) - ); + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: inputSecrets.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }); if (secrets.length) throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` }); @@ -954,6 +1041,34 @@ export const secretV2BridgeServiceFactory = ({ const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" }); + const tagsGroupByID = groupBy(tags, (i) => i.id); + + inputSecrets.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.secretKey, + secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) + }) + ); + }); + + // now get all secret references made and validate the permission + const secretReferencesGroupByInputSecretKey: Record> = {}; + const secretReferences: TSecretReference[] = []; + inputSecrets.forEach((el) => { + if (el.secretValue) { + const references = getAllSecretReferences(el.secretValue); + secretReferencesGroupByInputSecretKey[el.secretKey] = references; + secretReferences.push(...references.nestedReferences); + references.localReferences.forEach((localRefKey) => { + secretReferences.push({ secretKey: localRefKey, secretPath, environment }); + }); + } + }); + await $validateSecretReferences(projectId, permission, secretReferences); const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); @@ -961,16 +1076,7 @@ export const secretV2BridgeServiceFactory = ({ const newSecrets = await secretDAL.transaction(async (tx) => fnSecretBulkInsert({ inputSecrets: inputSecrets.map((el) => { - const references = getAllNestedSecretReferences(el.secretValue); - references.forEach((referredSecret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: referredSecret.environment, - secretPath: referredSecret.secretPath - }) - ); - }); + const references = secretReferencesGroupByInputSecretKey[el.secretKey].nestedReferences; return { version: 1, @@ -1032,10 +1138,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1045,38 +1147,115 @@ export const secretV2BridgeServiceFactory = ({ }); const folderId = folder.id; - const secretsToUpdate = await secretDAL.findBySecretKeys( + const secretsToUpdate = await secretDAL.find({ folderId, - inputSecrets.map((el) => ({ - key: el.secretKey, - type: SecretType.Shared - })) - ); + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: inputSecrets.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }); if (secretsToUpdate.length !== inputSecrets.length) throw new NotFoundError({ message: `Secret does not exist: ${secretsToUpdate.map((el) => el.key).join(",")}` }); const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key); - // now find any secret that needs to update its name - // same process as above - const secretsWithNewName = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName)); - if (secretsWithNewName.length) { - const secrets = await secretDAL.findBySecretKeys( - folderId, - secretsWithNewName.map((el) => ({ - key: el.newSecretName as string, - type: SecretType.Shared - })) + secretsToUpdate.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags.map((i) => i.slug) + }) ); - if (secrets.length) - throw new BadRequestError({ - message: `Secret with new name already exists: ${secretsWithNewName.map((el) => el.newSecretName).join(",")}` - }); - } + }); // get all tags const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" }); + const tagsGroupByID = groupBy(tags, (i) => i.id); + + // now find any secret that needs to update its name + // same process as above + const secretsWithNewName = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName)); + if (secretsWithNewName.length) { + const secrets = await secretDAL.find({ + folderId, + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: secretsWithNewName.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }); + if (secrets.length) + throw new BadRequestError({ + message: `Secret with new name already exists: ${secretsWithNewName.map((el) => el.newSecretName).join(",")}` + }); + + secretsWithNewName.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.newSecretName as string, + secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug) + }) + ); + }); + } + // now get all secret references made and validate the permission + const secretReferencesGroupByInputSecretKey: Record> = {}; + const secretReferences: TSecretReference[] = []; + inputSecrets.forEach((el) => { + if (el.secretValue) { + const references = getAllSecretReferences(el.secretValue); + secretReferencesGroupByInputSecretKey[el.secretKey] = references; + secretReferences.push(...references.nestedReferences); + references.localReferences.forEach((localRefKey) => { + secretReferences.push({ secretKey: localRefKey, secretPath, environment }); + }); + } + }); + await $validateSecretReferences(projectId, permission, secretReferences); const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); @@ -1091,22 +1270,10 @@ export const secretV2BridgeServiceFactory = ({ typeof el.secretValue !== "undefined" ? { encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob, - references: getAllNestedSecretReferences(el.secretValue) + references: secretReferencesGroupByInputSecretKey[el.secretKey].nestedReferences } : {}; - if (encryptedValue.references) { - encryptedValue.references.forEach((referredSecret) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { - environment: referredSecret.environment, - secretPath: referredSecret.secretPath - }) - ); - }); - } - return { filter: { id: originalSecret.id, type: SecretType.Shared }, data: { @@ -1164,10 +1331,6 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1177,17 +1340,47 @@ export const secretV2BridgeServiceFactory = ({ }); const folderId = folder.id; - const secretsToDelete = await secretDAL.findBySecretKeys( + const secretsToDelete = await secretDAL.find({ folderId, - inputSecrets.map((el) => ({ - key: el.secretKey, - type: SecretType.Shared - })) - ); + $complex: { + operator: "and", + value: [ + { + operator: "or", + value: inputSecrets.map((el) => ({ + operator: "and", + value: [ + { + operator: "eq", + field: "key", + value: el.secretKey + }, + { + operator: "eq", + field: "type", + value: SecretType.Shared + } + ] + })) + } + ] + } + }); if (secretsToDelete.length !== inputSecrets.length) throw new NotFoundError({ message: `One or more secrets does not exist: ${secretsToDelete.map((el) => el.key).join(",")}` }); + secretsToDelete.forEach((el) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName: el.key, + secretTags: el.tags?.map((i) => i.slug) + }) + ); + }); const secretsDeleted = await secretDAL.transaction(async (tx) => fnSecretBulkDelete({ @@ -1296,7 +1489,8 @@ export const secretV2BridgeServiceFactory = ({ .map(({ id, encryptedValue }) => ({ secretId: id, references: encryptedValue - ? getAllNestedSecretReferences(secretManagerDecryptor({ cipherTextBlob: encryptedValue }).toString()) + ? getAllSecretReferences(secretManagerDecryptor({ cipherTextBlob: encryptedValue }).toString()) + .nestedReferences : [] })), tx @@ -1327,21 +1521,6 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath: sourceSecretPath }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) - ); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) - ); - const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath); if (!sourceFolder) { throw new NotFoundError({ @@ -1367,6 +1546,17 @@ export const secretV2BridgeServiceFactory = ({ id: secretIds } }); + sourceSecrets.forEach((secret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + }); if (sourceSecrets.length !== secretIds.length) { throw new BadRequestError({ @@ -1437,6 +1627,32 @@ export const secretV2BridgeServiceFactory = ({ message: "Selected secrets already exist in the destination." }); } + + // permission check whether can create or edit the ones in the destination folder + locallyCreatedSecrets.forEach((secret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationEnvironment, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + }); + + locallyUpdatedSecrets.forEach((secret) => { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationEnvironment, + secretName: secret.key, + secretTags: secret.tags.map((el) => el.slug) + }) + ); + }); + const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( projectId, destinationFolder.environment.slug, @@ -1503,7 +1719,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, reminderRepeatDays: doc.reminderRepeatDays, - references: doc.value ? getAllNestedSecretReferences(doc.value) : [] + references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] }; }) }); @@ -1532,7 +1748,7 @@ export const secretV2BridgeServiceFactory = ({ ...(doc.encryptedValue ? { encryptedValue: doc.encryptedValue, - references: doc.value ? getAllNestedSecretReferences(doc.value) : [] + references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] } : { encryptedValue: undefined, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index a76c57561..0bb93198d 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -15,6 +15,12 @@ type TPartialSecret = Pick; +export type TSecretReferenceDTO = { + environment: string; + secretPath: string; + secretKey: string; +}; + export type TGetSecretsDTO = { expandSecretReferences?: boolean; path: string; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 1d0b89b46..5d3a6ef46 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -25,7 +25,7 @@ import { logger } from "@app/lib/logger"; import { fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, - getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge + getAllSecretReferences } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; @@ -185,7 +185,9 @@ export const recursivelyGetSecretPaths = ({ ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, - secretPath: folder.path + secretPath: folder.path, + secretName: "", + secretTags: [] }) ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) ); @@ -791,7 +793,7 @@ export const createManySecretsRawFnFactory = ({ : null, skipMultilineEncoding: secret.skipMultilineEncoding, tags: secret.tags, - references: getAllNestedSecretReferencesV2Bridge(secret.secretValue) + references: getAllSecretReferences(secret.secretValue).nestedReferences }; }); @@ -971,7 +973,7 @@ export const updateManySecretsRawFnFactory = ({ : null, skipMultilineEncoding: secret.skipMultilineEncoding, tags: secret.tags, - references: getAllNestedSecretReferencesV2Bridge(secret.secretValue) + references: getAllSecretReferences(secret.secretValue).nestedReferences }; }); diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 4076b179f..00678102c 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -50,7 +50,7 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; -import { expandSecretReferencesFactory, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; +import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; @@ -1147,7 +1147,7 @@ export const secretQueueFactory = ({ : ""; const encryptedValue = secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob; // create references - const references = getAllNestedSecretReferences(value); + const references = getAllSecretReferences(value).nestedReferences; secretReferences.push({ secretId: el.id, references }); const encryptedComment = comment diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 9d3037ec0..e2636a0a9 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -187,7 +187,7 @@ export const secretServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -296,7 +296,7 @@ export const secretServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -433,7 +433,7 @@ export const secretServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -538,7 +538,7 @@ export const secretServiceFactory = ({ } else { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -565,7 +565,9 @@ export const secretServiceFactory = ({ ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, - secretPath: importPath + secretPath: importPath, + secretName: "", + secretTags: [] }) ) ); @@ -619,7 +621,7 @@ export const secretServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) @@ -671,7 +673,9 @@ export const secretServiceFactory = ({ ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, - secretPath: importPath + secretPath: importPath, + secretName: "", + secretTags: [] }) ) ); @@ -718,7 +722,7 @@ export const secretServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -803,7 +807,7 @@ export const secretServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -909,7 +913,7 @@ export const secretServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] }) ); await projectDAL.checkProjectUpgradeStatus(projectId); @@ -2118,7 +2122,7 @@ export const secretServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "", secretTags: [] }) ); await projectDAL.checkProjectUpgradeStatus(project.id); @@ -2220,7 +2224,7 @@ export const secretServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "", secretTags: [] }) ); await projectDAL.checkProjectUpgradeStatus(project.id); @@ -2407,17 +2411,32 @@ export const secretServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath: sourceSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: sourceEnvironment, + secretPath: sourceSecretPath, + secretName: "", + secretTags: [] + }) ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath, + secretName: "", + secretTags: [] + }) ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) + subject(ProjectPermissionSub.Secrets, { + environment: destinationEnvironment, + secretPath: destinationSecretPath, + secretName: "", + secretTags: [] + }) ); const { botKey } = await projectBotService.getBotKey(project.id); diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 5ecbcf54d..480cb438a 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -66,7 +66,7 @@ export const serviceTokenServiceFactory = ({ scopes.forEach(({ environment, secretPath }) => { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "", secretTags: [] }) ); });