feat: completed easier changes on other files where permission is needed

This commit is contained in:
=
2024-10-03 23:23:27 +05:30
parent 7ed0818279
commit 17567ebd0f
22 changed files with 676 additions and 510 deletions

View File

@@ -14,7 +14,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const accessApprovalPolicyFindQuery = async ( const accessApprovalPolicyFindQuery = async (
tx: Knex, tx: Knex,
filter: TFindFilter<TAccessApprovalPolicies>, filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
customFilter?: { customFilter?: {
policyId?: string; policyId?: string;
} }

View File

@@ -26,7 +26,7 @@ export const isApproversValid = async ({
ForbiddenError.from(approverPermission).throwUnlessCan( ForbiddenError.from(approverPermission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath }) subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath, secretName: "", secretTags: [] })
); );
} }
} catch { } catch {

View File

@@ -11,7 +11,6 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGroupDALFactory } from "../group/group-dal"; import { TGroupDALFactory } from "../group/group-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { isApproversValid } from "./access-approval-policy-fns";
import { import {
ApproverType, ApproverType,
TCreateAccessApprovalPolicy, TCreateAccessApprovalPolicy,
@@ -132,22 +131,6 @@ export const accessApprovalPolicyServiceFactory = ({
.map((user) => user.id); .map((user) => user.id);
verifyAllApprovers.push(...verifyGroupApprovers); 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 accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create( const doc = await accessApprovalPolicyDAL.create(
{ {
@@ -289,22 +272,6 @@ export const accessApprovalPolicyServiceFactory = ({
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id)); 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( await accessApprovalPolicyApproverDAL.insertMany(
userApproverIds.map((userId) => ({ userApproverIds.map((userId) => ({
approverUserId: userId, approverUserId: userId,
@@ -315,41 +282,6 @@ export const accessApprovalPolicyServiceFactory = ({
} }
if (groupApprovers) { 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( await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({ groupApprovers.map((groupId) => ({
approverGroupId: groupId, approverGroupId: groupId,

View File

@@ -17,7 +17,6 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-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 { TGroupDALFactory } from "../group/group-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
@@ -78,7 +77,6 @@ export const accessApprovalRequestServiceFactory = ({
permissionService, permissionService,
accessApprovalRequestDAL, accessApprovalRequestDAL,
accessApprovalRequestReviewerDAL, accessApprovalRequestReviewerDAL,
projectMembershipDAL,
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
additionalPrivilegeDAL, additionalPrivilegeDAL,
@@ -323,22 +321,6 @@ export const accessApprovalRequestServiceFactory = ({
throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" }); 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 }); const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });

View File

@@ -14,7 +14,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
const secretApprovalPolicyFindQuery = ( const secretApprovalPolicyFindQuery = (
tx: Knex, tx: Knex,
filter: TFindFilter<TSecretApprovalPolicies>, filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
customFilter?: { customFilter?: {
sapId?: string; sapId?: string;
} }

View File

@@ -353,7 +353,7 @@ export const secretApprovalPolicyServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { secretPath, environment }) subject(ProjectPermissionSub.Secrets, { secretPath, environment, secretName: "", secretTags: [] })
); );
return getSecretApprovalPolicy(projectId, environment, secretPath); return getSecretApprovalPolicy(projectId, environment, secretPath);
}; };

View File

@@ -43,7 +43,7 @@ import {
fnSecretBulkDelete as fnSecretV2BridgeBulkDelete, fnSecretBulkDelete as fnSecretV2BridgeBulkDelete,
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge getAllSecretReferences as getAllNestedSecretReferencesV2Bridge
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
@@ -527,7 +527,7 @@ export const secretApprovalRequestServiceFactory = ({
secretManagerDecryptor({ secretManagerDecryptor({
cipherTextBlob: el.encryptedValue cipherTextBlob: el.encryptedValue
}).toString() }).toString()
) ).nestedReferences
: [], : [],
type: SecretType.Shared type: SecretType.Shared
})), })),
@@ -551,7 +551,7 @@ export const secretApprovalRequestServiceFactory = ({
secretManagerDecryptor({ secretManagerDecryptor({
cipherTextBlob: el.encryptedValue cipherTextBlob: el.encryptedValue
}).toString() }).toString()
) ).nestedReferences
: [] : []
} }
: {}; : {};

View File

@@ -28,8 +28,8 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
import { import {
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllNestedSecretReferences, getAllSecretReferences as getAllNestedSecretReferencesV2Bridge,
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge getAllSecretReferences
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
@@ -416,7 +416,9 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue, encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment, encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding, 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, encryptedValue: doc.encryptedValue as Buffer,
encryptedComment: doc.encryptedComment, encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding, 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, secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext, secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding, skipMultilineEncoding: doc.skipMultilineEncoding,
references: getAllNestedSecretReferences(doc.secretValue) references: getAllSecretReferences(doc.secretValue).nestedReferences
}; };
}) })
}); });
@@ -723,7 +727,7 @@ export const secretReplicationServiceFactory = ({
secretCommentTag: doc.secretCommentTag, secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext, secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding, skipMultilineEncoding: doc.skipMultilineEncoding,
references: getAllNestedSecretReferences(doc.secretValue) references: getAllSecretReferences(doc.secretValue).nestedReferences
} }
}; };
}) })

View File

@@ -97,8 +97,9 @@ export const secretRotationServiceFactory = ({
if (!folder) throw new NotFoundError({ message: "Secret path not found" }); if (!folder) throw new NotFoundError({ message: "Secret path not found" });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "", secretTags: [] })
); );
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
const shouldUseBridge = project.version === ProjectVersion.V3; const shouldUseBridge = project.version === ProjectVersion.V3;

View File

@@ -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. // 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); 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. // 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
@@ -214,7 +214,9 @@ export const secretSnapshotServiceFactory = ({
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: snapshotDetails.environment.slug, environment: snapshotDetails.environment.slug,
secretPath: fullFolderPath secretPath: fullFolderPath,
secretName: "",
secretTags: []
}) })
); );

View File

@@ -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<K extends AnyAbility> = {
ability: K;
subject: ExtractSubjectType<Parameters<K["rulesFor"]>[1]>;
action: Parameters<K["rulesFor"]>[0];
};
export const buildKnexQueryFromCaslOperators = <K extends AnyAbility>({
ability,
subject,
action
}: TBuildKnexQueryFromCaslDTO<K>) => {
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<T extends keyof Tables> = {
[K in T]: `${K}.${Exclude<keyof Tables[K]["base"], symbol>}`;
}[T];
type TFormatCaslFieldsWithTableNames<T extends keyof Tables> = {
// 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<T> | null;
dynamicQuery: TKnexDynamicOperator;
};
export const formatCaslOperatorFieldsWithTableNames = <T extends keyof Tables>({
missingOperatorCallback = (arg) => {
throw new BadRequestError({ message: `Unknown permission operator: ${arg}` });
},
dynamicQuery: dynamicQueryAst,
fieldMapping
}: TFormatCaslFieldsWithTableNames<T>) => {
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 = <T extends keyof Tables>(
caslKnexOperators: AbilityQuery,
fieldMapping: (arg: string) => TFieldMapper<T> | 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
});
};

View File

@@ -2,32 +2,35 @@ import { Knex } from "knex";
import { UnauthorizedError } from "../errors"; import { UnauthorizedError } from "../errors";
type TKnexDynamicPrimitiveOperator = { type TKnexDynamicPrimitiveOperator<T extends object> = {
operator: "eq" | "ne" | "startsWith" | "endsWith"; operator: "eq" | "ne" | "startsWith" | "endsWith";
value: string; value: string;
field: string; field: Extract<keyof T, string>;
}; };
type TKnexDynamicInOperator = { type TKnexDynamicInOperator<T extends object> = {
operator: "in"; operator: "in";
value: string[] | number[]; value: string[] | number[];
field: string; field: Extract<keyof T, string>;
}; };
type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator; type TKnexNonGroupOperator<T extends object> = TKnexDynamicInOperator<T> | TKnexDynamicPrimitiveOperator<T>;
type TKnexGroupOperator = { type TKnexGroupOperator<T extends object> = {
operator: "and" | "or" | "not"; operator: "and" | "or" | "not";
value: (TKnexNonGroupOperator | TKnexGroupOperator)[]; value: (TKnexNonGroupOperator<T> | TKnexGroupOperator<T>)[];
}; };
// akhilmhdh: This is still in pending state and not yet ready. If you want to use it ping me. // 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 // 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 // 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 // majorly used with casl permission to filter data based on permission
export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator; export type TKnexDynamicOperator<T extends object> = TKnexGroupOperator<T> | TKnexNonGroupOperator<T>;
export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => { export const buildDynamicKnexQuery = <T extends object>(
dynamicQuery: TKnexDynamicOperator<T>,
rootQueryBuild: Knex.QueryBuilder
) => {
const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }]; const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }];
while (stack.length) { while (stack.length) {

View File

@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { Tables } from "knex/types/tables"; import { Tables } from "knex/types/tables";
import { DatabaseError } from "../errors"; import { DatabaseError } from "../errors";
import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic";
export * from "./connection"; export * from "./connection";
export * from "./join"; export * from "./join";
@@ -20,9 +21,10 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
export type TFindFilter<R extends object = object> = Partial<R> & { export type TFindFilter<R extends object = object> = Partial<R> & {
$in?: Partial<{ [k in keyof R]: R[k][] }>; $in?: Partial<{ [k in keyof R]: R[k][] }>;
$search?: Partial<{ [k in keyof R]: R[k] }>; $search?: Partial<{ [k in keyof R]: R[k] }>;
$complex?: TKnexDynamicOperator<R>;
}; };
export const buildFindFilter = export const buildFindFilter =
<R extends object = object>({ $in, $search, ...filter }: TFindFilter<R>) => <R extends object = object>({ $in, $search, $complex, ...filter }: TFindFilter<R>) =>
(bd: Knex.QueryBuilder<R, R>) => { (bd: Knex.QueryBuilder<R, R>) => {
void bd.where(filter); void bd.where(filter);
if ($in) { if ($in) {
@@ -39,6 +41,9 @@ export const buildFindFilter =
} }
}); });
} }
if ($complex) {
buildDynamicKnexQuery($complex, bd);
}
return bd; return bd;
}; };

View File

@@ -89,7 +89,12 @@ export const integrationServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, 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); const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
@@ -162,7 +167,12 @@ export const integrationServiceFactory = ({
if (environment || secretPath) { if (environment || secretPath) {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath }) subject(ProjectPermissionSub.Secrets, {
environment: newEnvironment,
secretPath: newSecretPath,
secretName: "",
secretTags: []
})
); );
} }

View File

@@ -4,7 +4,14 @@ import { validate as uuidValidate } from "uuid";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas"; import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; 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 { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@@ -13,6 +20,97 @@ export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFacto
export const secretV2BridgeDALFactory = (db: TDbClient) => { export const secretV2BridgeDALFactory = (db: TDbClient) => {
const secretOrm = ormify(db, TableName.SecretV2); const secretOrm = ormify(db, TableName.SecretV2);
const findOne = async (filter: Partial<TSecretsV2>, 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<TSecretsV2>, { offset, limit, sort, tx }: TFindOpt<TSecretsV2> = {}) => {
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<TSecretsV2>, data: Omit<TSecretsV2Update, "version">, tx?: Knex) => { const update = async (filter: Partial<TSecretsV2>, data: Omit<TSecretsV2Update, "version">, tx?: Knex) => {
try { try {
const sec = await (tx || db)(TableName.SecretV2) const sec = await (tx || db)(TableName.SecretV2)
@@ -484,6 +582,8 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
upsertSecretReferences, upsertSecretReferences,
findReferencedSecretReferences, findReferencedSecretReferences,
findAllProjectSecretValues, findAllProjectSecretValues,
countByFolderIds countByFolderIds,
findOne,
find
}; };
}; };

View File

@@ -30,9 +30,10 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3;
* // { environment: 'prod', secretPath: '/anotherFolder' } * // { 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]); const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
return references
const nestedReferences = references
.filter((el) => el.includes(".")) .filter((el) => el.includes("."))
.map((el) => { .map((el) => {
const [environment, ...secretPathList] = el.split("."); const [environment, ...secretPathList] = el.split(".");
@@ -42,6 +43,8 @@ export const getAllNestedSecretReferences = (maybeSecretReference: string) => {
secretKey: secretPathList[secretPathList.length - 1] 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 // these functions are special functions shared by a couple of resources
@@ -325,16 +328,13 @@ type TRecursivelyFetchSecretsFromFoldersArg = {
projectId: string; projectId: string;
environment: string; environment: string;
currentPath: string; currentPath: string;
hasAccess: (environment: string, secretPath: string) => boolean;
}; };
export const recursivelyGetSecretPaths = async ({ export const recursivelyGetSecretPaths = async ({
folderDAL, folderDAL,
projectEnvDAL, projectEnvDAL,
projectId, projectId,
environment, environment
currentPath,
hasAccess
}: TRecursivelyFetchSecretsFromFoldersArg) => { }: TRecursivelyFetchSecretsFromFoldersArg) => {
const env = await projectEnvDAL.findOne({ const env = await projectEnvDAL.findOne({
projectId, projectId,
@@ -360,12 +360,7 @@ export const recursivelyGetSecretPaths = async ({
folderId: p.folderId folderId: p.folderId
})); }));
// Filter out paths that the user does not have permission to access, and paths that are not in the current path return paths;
const allowedPaths = paths.filter(
(folder) => hasAccess(environment, folder.path) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
);
return allowedPaths;
}; };
// used to convert multi line ones to quotes ones with \n // used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => { const formatMultiValueEnv = (val?: string) => {
@@ -379,7 +374,7 @@ type TInterpolateSecretArg = {
decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined; decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined;
secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">; secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
canExpandValue: (environment: string, secretPath: string) => boolean; canExpandValue: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}; };
const MAX_SECRET_REFERENCE_DEPTH = 10; const MAX_SECRET_REFERENCE_DEPTH = 10;
@@ -390,29 +385,29 @@ export const expandSecretReferencesFactory = ({
folderDAL, folderDAL,
canExpandValue canExpandValue
}: TInterpolateSecretArg) => { }: TInterpolateSecretArg) => {
const secretCache: Record<string, Record<string, string>> = {}; const secretCache: Record<string, Record<string, { value: string; tags: string[] }>> = {};
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`; const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => { const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => {
const cacheKey = getCacheUniqueKey(environment, secretPath); const cacheKey = getCacheUniqueKey(environment, secretPath);
if (secretCache?.[cacheKey]) { if (secretCache?.[cacheKey]) {
return secretCache[cacheKey][secretKey] || ""; return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
} }
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return ""; if (!folder) return { value: "", tags: [] };
const secrets = await secretDAL.findByFolderId(folder.id); const secrets = await secretDAL.findByFolderId(folder.id);
const decryptedSecret = secrets.reduce<Record<string, string>>((prev, secret) => { const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
// eslint-disable-next-line no-param-reassign // 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; return prev;
}, {}); }, {});
secretCache[cacheKey] = decryptedSecret; secretCache[cacheKey] = decryptedSecret;
return secretCache[cacheKey][secretKey] || ""; return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
}; };
const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => { const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => {
@@ -438,43 +433,43 @@ export const expandSecretReferencesFactory = ({
if (entities.length === 1) { if (entities.length === 1) {
const [secretKey] = entities; 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({ 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.` 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); const cacheKey = getCacheUniqueKey(environment, secretPath);
secretCache[cacheKey][secretKey] = referedValue; secretCache[cacheKey][secretKey] = referedValue;
if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { if (INTERPOLATION_SYNTAX_REG.test(referedValue.value)) {
stack.push({ stack.push({
value: referedValue, value: referedValue.value,
secretPath, secretPath,
environment, environment,
depth: depth + 1 depth: depth + 1
}); });
} }
if (referedValue) { if (referedValue) {
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value);
} }
} else { } else {
const secretReferenceEnvironment = entities[0]; const secretReferenceEnvironment = entities[0];
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1)); const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
const secretReferenceKey = entities[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({ 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.` 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); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
secretCache[cacheKey][secretReferenceKey] = referedValue; secretCache[cacheKey][secretReferenceKey] = referedValue;
if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { if (INTERPOLATION_SYNTAX_REG.test(referedValue.value)) {
stack.push({ stack.push({
value: referedValue, value: referedValue.value,
secretPath: secretReferencePath, secretPath: secretReferencePath,
environment: secretReferenceEnvironment, environment: secretReferenceEnvironment,
depth: depth + 1 depth: depth + 1
@@ -482,7 +477,7 @@ export const expandSecretReferencesFactory = ({
} }
if (referedValue) { if (referedValue) {
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value);
} }
} }
} }

View File

@@ -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 { ProjectMembershipRole, SecretsV2Schema, SecretType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -28,7 +29,7 @@ import {
fnSecretBulkDelete, fnSecretBulkDelete,
fnSecretBulkInsert, fnSecretBulkInsert,
fnSecretBulkUpdate, fnSecretBulkUpdate,
getAllNestedSecretReferences, getAllSecretReferences,
recursivelyGetSecretPaths, recursivelyGetSecretPaths,
reshapeBridgeSecret reshapeBridgeSecret
} from "./secret-v2-bridge-fns"; } from "./secret-v2-bridge-fns";
@@ -43,6 +44,7 @@ import {
TGetSecretsDTO, TGetSecretsDTO,
TGetSecretVersionsDTO, TGetSecretVersionsDTO,
TMoveSecretsDTO, TMoveSecretsDTO,
TSecretReference,
TUpdateManySecretDTO, TUpdateManySecretDTO,
TUpdateSecretDTO TUpdateSecretDTO
} from "./secret-v2-bridge-types"; } from "./secret-v2-bridge-types";
@@ -56,7 +58,7 @@ type TSecretV2BridgeServiceFactoryDep = {
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
secretTagDAL: TSecretTagDALFactory; secretTagDAL: TSecretTagDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
folderDAL: Pick< folderDAL: Pick<
TSecretFolderDALFactory, TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv" "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv"
@@ -93,6 +95,70 @@ export const secretV2BridgeServiceFactory = ({
secretApprovalRequestSecretDAL, secretApprovalRequestSecretDAL,
kmsService kmsService
}: TSecretV2BridgeServiceFactoryDep) => { }: TSecretV2BridgeServiceFactoryDep) => {
const $validateSecretReferences = async (
projectId: string,
permission: PureAbility,
references: ReturnType<typeof getAllSecretReferences>["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 ({ const createSecret = async ({
actor, actor,
actorId, actorId,
@@ -110,10 +176,6 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) if (!folder)
@@ -134,6 +196,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
if (inputSecret.type === SecretType.Shared && doesSecretExist) if (inputSecret.type === SecretType.Shared && doesSecretExist)
throw new BadRequestError({ message: "Secret already exist" }); throw new BadRequestError({ message: "Secret already exist" });
// if user creating personal check its shared also exist // if user creating personal check its shared also exist
if (inputSecret.type === SecretType.Personal && !doesSecretExist) { if (inputSecret.type === SecretType.Personal && !doesSecretExist) {
throw new BadRequestError({ throw new BadRequestError({
@@ -146,22 +209,28 @@ export const secretV2BridgeServiceFactory = ({
const tags = inputSecret.tagIds ? await secretTagDAL.find({ projectId, $in: { id: inputSecret.tagIds } }) : []; 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.tagIds || []).length !== tags.length) throw new NotFoundError({ message: "Tag not found" });
const { secretName, type, ...el } = inputSecret; const { secretName, type, ...inputSecretData } = inputSecret;
const references = getAllNestedSecretReferences(inputSecret.secretValue);
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({ const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId 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) => const secret = await secretDAL.transaction((tx) =>
fnSecretBulkInsert({ fnSecretBulkInsert({
folderId, folderId,
@@ -169,20 +238,20 @@ export const secretV2BridgeServiceFactory = ({
{ {
version: 1, version: 1,
type, type,
reminderRepeatDays: el.secretReminderRepeatDays, reminderRepeatDays: inputSecretData.secretReminderRepeatDays,
encryptedComment: setKnexStringValue( encryptedComment: setKnexStringValue(
el.secretComment, inputSecretData.secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
), ),
encryptedValue: el.secretValue encryptedValue: inputSecretData.secretValue
? secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob ? secretManagerEncryptor({ plainText: Buffer.from(inputSecretData.secretValue) }).cipherTextBlob
: undefined, : undefined,
reminderNote: el.secretReminderNote, reminderNote: inputSecretData.secretReminderNote,
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: inputSecretData.skipMultilineEncoding,
key: secretName, key: secretName,
userId: inputSecret.type === SecretType.Personal ? actorId : null, userId: inputSecret.type === SecretType.Personal ? actorId : null,
tagIds: inputSecret.tagIds, tagIds: inputSecret.tagIds,
references references: nestedReferences
} }
], ],
secretDAL, secretDAL,
@@ -228,10 +297,6 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
if (inputSecret.newSecretName === "") { if (inputSecret.newSecretName === "") {
throw new BadRequestError({ message: "New secret name cannot be empty" }); throw new BadRequestError({ message: "New secret name cannot be empty" });
@@ -276,6 +341,21 @@ export const secretV2BridgeServiceFactory = ({
secret = sharedSecretToModify; 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) { if (inputSecret.newSecretName) {
const doesNewNameSecretExist = await secretDAL.findOne({ const doesNewNameSecretExist = await secretDAL.findOne({
key: inputSecret.newSecretName, key: inputSecret.newSecretName,
@@ -283,36 +363,34 @@ export const secretV2BridgeServiceFactory = ({
folderId folderId
}); });
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" }); 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 { secretName, secretValue } = inputSecret;
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId projectId
}); });
const encryptedValue = const encryptedValue = secretValue
typeof secretValue !== "undefined" ? secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob
? { : undefined;
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob, const secretReferences = secretValue ? getAllSecretReferences(secretValue) : undefined;
references: getAllNestedSecretReferences(secretValue)
} if (secretReferences) {
: {}; const { nestedReferences, localReferences } = secretReferences;
if (encryptedValue.references) { const allSecretReferences = nestedReferences.concat(
encryptedValue.references.forEach((referredSecret) => { localReferences.map((el) => ({ secretKey: el, secretPath, environment }))
ForbiddenError.from(permission).throwUnlessCan( );
ProjectPermissionActions.Read, await $validateSecretReferences(projectId, permission, allSecretReferences);
subject(ProjectPermissionSub.Secrets, {
environment: referredSecret.environment,
secretPath: referredSecret.secretPath
})
);
});
} }
const updatedSecret = await secretDAL.transaction(async (tx) => const updatedSecret = await secretDAL.transaction(async (tx) =>
@@ -331,7 +409,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: inputSecret.skipMultilineEncoding, skipMultilineEncoding: inputSecret.skipMultilineEncoding,
key: inputSecret.newSecretName || secretName, key: inputSecret.newSecretName || secretName,
tags: inputSecret.tagIds, tags: inputSecret.tagIds,
...encryptedValue ...(encryptedValue ? { encryptedValue, references: secretReferences?.nestedReferences || [] } : {})
} }
} }
], ],
@@ -386,10 +464,6 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) if (!folder)
@@ -414,6 +488,15 @@ export const secretV2BridgeServiceFactory = ({
}) })
}); });
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" }); 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) => const deletedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({ fnSecretBulkDelete({
@@ -482,13 +565,7 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId actorOrgId
); );
// verify user has access to all environments ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
environments.forEach((environment) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
)
);
} }
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
@@ -534,10 +611,7 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return 0; if (!folder) return 0;
@@ -562,22 +636,15 @@ export const secretV2BridgeServiceFactory = ({
environments: string[]; environments: string[];
isInternal?: boolean; isInternal?: boolean;
}) => { }) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
if (!isInternal) { if (!isInternal) {
const { permission } = await permissionService.getProjectPermission( ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
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 })
)
);
} }
let paths: { folderId: string; path: string; environment: string }[] = []; let paths: { folderId: string; path: string; environment: string }[] = [];
@@ -645,6 +712,8 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
let paths: { folderId: string; path: string }[] = []; let paths: { folderId: string; path: string }[] = [];
if (recursive) { if (recursive) {
@@ -653,26 +722,13 @@ export const secretV2BridgeServiceFactory = ({
projectEnvDAL, projectEnvDAL,
projectId, projectId,
environment, environment,
currentPath: path, currentPath: path
hasAccess: (permissionEnvironment, permissionSecretPath) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: permissionEnvironment,
secretPath: permissionSecretPath
})
)
}); });
if (!deepPaths) return { secrets: [], imports: [] }; if (!deepPaths) return { secrets: [], imports: [] };
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
} else { } else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return { secrets: [], imports: [] }; if (!folder) return { secrets: [], imports: [] };
@@ -693,27 +749,44 @@ export const secretV2BridgeServiceFactory = ({
projectId projectId
}); });
const decryptedSecrets = secrets.map((secret) => const decryptedSecrets = secrets
reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, { .filter((el) =>
...secret, permission.can(
value: secret.encryptedValue ProjectPermissionActions.Read,
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() subject(ProjectPermissionSub.Secrets, {
: "", environment,
comment: secret.encryptedComment secretPath: groupedPaths[el.folderId][0].path,
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() 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({ const expandSecretReferences = expandSecretReferencesFactory({
projectId, projectId,
folderDAL, folderDAL,
secretDAL, secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath) => canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can( permission.can(
ProjectPermissionActions.Read, 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 secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
const allowedImports = secretImports.filter(({ importEnv, importPath, isReplication }) => const allowedImports = secretImports.filter(({ isReplication }) => !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 importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
allowedImports, allowedImports,
secretDAL, secretDAL,
@@ -793,10 +854,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) if (!folder)
throw new NotFoundError({ throw new NotFoundError({
@@ -832,17 +890,43 @@ export const secretV2BridgeServiceFactory = ({
userId: secretType === SecretType.Personal ? actorId : null, userId: secretType === SecretType.Personal ? actorId : null,
key: secretName 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({ const expandSecretReferences = expandSecretReferencesFactory({
projectId, projectId,
folderDAL, folderDAL,
secretDAL, secretDAL,
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined), decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
canExpandValue: (expandEnvironment, expandSecretPath) => canExpandValue: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can( permission.can(
ProjectPermissionActions.Read, 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 // here we consider the import order also thus starting from bottom
if (!secret && includeImports) { if (!secret && includeImports) {
const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); 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({ const importedSecrets = await fnSecretsV2FromImports({
allowedImports, allowedImports: secretImports,
secretDAL, secretDAL,
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
@@ -927,10 +999,6 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) if (!folder)
@@ -940,13 +1008,32 @@ export const secretV2BridgeServiceFactory = ({
}); });
const folderId = folder.id; const folderId = folder.id;
const secrets = await secretDAL.findBySecretKeys( const secrets = await secretDAL.find({
folderId, folderId,
inputSecrets.map((el) => ({ $complex: {
key: el.secretKey, operator: "and",
type: SecretType.Shared 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) if (secrets.length)
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` }); 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 sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds);
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : [];
if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" }); 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<string, ReturnType<typeof getAllSecretReferences>> = {};
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 } = const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId });
@@ -961,16 +1076,7 @@ export const secretV2BridgeServiceFactory = ({
const newSecrets = await secretDAL.transaction(async (tx) => const newSecrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkInsert({ fnSecretBulkInsert({
inputSecrets: inputSecrets.map((el) => { inputSecrets: inputSecrets.map((el) => {
const references = getAllNestedSecretReferences(el.secretValue); const references = secretReferencesGroupByInputSecretKey[el.secretKey].nestedReferences;
references.forEach((referredSecret) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: referredSecret.environment,
secretPath: referredSecret.secretPath
})
);
});
return { return {
version: 1, version: 1,
@@ -1032,10 +1138,6 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) if (!folder)
@@ -1045,38 +1147,115 @@ export const secretV2BridgeServiceFactory = ({
}); });
const folderId = folder.id; const folderId = folder.id;
const secretsToUpdate = await secretDAL.findBySecretKeys( const secretsToUpdate = await secretDAL.find({
folderId, folderId,
inputSecrets.map((el) => ({ $complex: {
key: el.secretKey, operator: "and",
type: SecretType.Shared 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) if (secretsToUpdate.length !== inputSecrets.length)
throw new NotFoundError({ message: `Secret does not exist: ${secretsToUpdate.map((el) => el.key).join(",")}` }); throw new NotFoundError({ message: `Secret does not exist: ${secretsToUpdate.map((el) => el.key).join(",")}` });
const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key); const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key);
// now find any secret that needs to update its name secretsToUpdate.forEach((el) => {
// same process as above ForbiddenError.from(permission).throwUnlessCan(
const secretsWithNewName = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName)); ProjectPermissionActions.Edit,
if (secretsWithNewName.length) { subject(ProjectPermissionSub.Secrets, {
const secrets = await secretDAL.findBySecretKeys( environment,
folderId, secretPath,
secretsWithNewName.map((el) => ({ secretName: el.key,
key: el.newSecretName as string, secretTags: el.tags.map((i) => i.slug)
type: SecretType.Shared })
}))
); );
if (secrets.length) });
throw new BadRequestError({
message: `Secret with new name already exists: ${secretsWithNewName.map((el) => el.newSecretName).join(",")}`
});
}
// get all tags // get all tags
const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds);
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : [];
if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" }); 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<string, ReturnType<typeof getAllSecretReferences>> = {};
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 } = const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId }); await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId });
@@ -1091,22 +1270,10 @@ export const secretV2BridgeServiceFactory = ({
typeof el.secretValue !== "undefined" typeof el.secretValue !== "undefined"
? { ? {
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob, 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 { return {
filter: { id: originalSecret.id, type: SecretType.Shared }, filter: { id: originalSecret.id, type: SecretType.Shared },
data: { data: {
@@ -1164,10 +1331,6 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) if (!folder)
@@ -1177,17 +1340,47 @@ export const secretV2BridgeServiceFactory = ({
}); });
const folderId = folder.id; const folderId = folder.id;
const secretsToDelete = await secretDAL.findBySecretKeys( const secretsToDelete = await secretDAL.find({
folderId, folderId,
inputSecrets.map((el) => ({ $complex: {
key: el.secretKey, operator: "and",
type: SecretType.Shared 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) if (secretsToDelete.length !== inputSecrets.length)
throw new NotFoundError({ throw new NotFoundError({
message: `One or more secrets does not exist: ${secretsToDelete.map((el) => el.key).join(",")}` 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) => const secretsDeleted = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({ fnSecretBulkDelete({
@@ -1296,7 +1489,8 @@ export const secretV2BridgeServiceFactory = ({
.map(({ id, encryptedValue }) => ({ .map(({ id, encryptedValue }) => ({
secretId: id, secretId: id,
references: encryptedValue references: encryptedValue
? getAllNestedSecretReferences(secretManagerDecryptor({ cipherTextBlob: encryptedValue }).toString()) ? getAllSecretReferences(secretManagerDecryptor({ cipherTextBlob: encryptedValue }).toString())
.nestedReferences
: [] : []
})), })),
tx tx
@@ -1327,21 +1521,6 @@ export const secretV2BridgeServiceFactory = ({
actorOrgId 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); const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath);
if (!sourceFolder) { if (!sourceFolder) {
throw new NotFoundError({ throw new NotFoundError({
@@ -1367,6 +1546,17 @@ export const secretV2BridgeServiceFactory = ({
id: secretIds 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) { if (sourceSecrets.length !== secretIds.length) {
throw new BadRequestError({ throw new BadRequestError({
@@ -1437,6 +1627,32 @@ export const secretV2BridgeServiceFactory = ({
message: "Selected secrets already exist in the destination." 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( const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy(
projectId, projectId,
destinationFolder.environment.slug, destinationFolder.environment.slug,
@@ -1503,7 +1719,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: doc.skipMultilineEncoding, skipMultilineEncoding: doc.skipMultilineEncoding,
reminderNote: doc.reminderNote, reminderNote: doc.reminderNote,
reminderRepeatDays: doc.reminderRepeatDays, 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 ...(doc.encryptedValue
? { ? {
encryptedValue: doc.encryptedValue, encryptedValue: doc.encryptedValue,
references: doc.value ? getAllNestedSecretReferences(doc.value) : [] references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : []
} }
: { : {
encryptedValue: undefined, encryptedValue: undefined,

View File

@@ -15,6 +15,12 @@ type TPartialSecret = Pick<TSecretsV2, "id" | "reminderRepeatDays" | "reminderNo
type TPartialInputSecret = Pick<TSecretsV2, "type" | "reminderNote" | "reminderRepeatDays" | "id">; type TPartialInputSecret = Pick<TSecretsV2, "type" | "reminderNote" | "reminderRepeatDays" | "id">;
export type TSecretReferenceDTO = {
environment: string;
secretPath: string;
secretKey: string;
};
export type TGetSecretsDTO = { export type TGetSecretsDTO = {
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
path: string; path: string;

View File

@@ -25,7 +25,7 @@ import { logger } from "@app/lib/logger";
import { import {
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert, fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate, fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge getAllSecretReferences
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
@@ -185,7 +185,9 @@ export const recursivelyGetSecretPaths = ({
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
secretPath: folder.path secretPath: folder.path,
secretName: "",
secretTags: []
}) })
) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
); );
@@ -791,7 +793,7 @@ export const createManySecretsRawFnFactory = ({
: null, : null,
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags, tags: secret.tags,
references: getAllNestedSecretReferencesV2Bridge(secret.secretValue) references: getAllSecretReferences(secret.secretValue).nestedReferences
}; };
}); });
@@ -971,7 +973,7 @@ export const updateManySecretsRawFnFactory = ({
: null, : null,
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags, tags: secret.tags,
references: getAllNestedSecretReferencesV2Bridge(secret.secretValue) references: getAllSecretReferences(secret.secretValue).nestedReferences
}; };
}); });

View File

@@ -50,7 +50,7 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; 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 { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
@@ -1147,7 +1147,7 @@ export const secretQueueFactory = ({
: ""; : "";
const encryptedValue = secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob; const encryptedValue = secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob;
// create references // create references
const references = getAllNestedSecretReferences(value); const references = getAllSecretReferences(value).nestedReferences;
secretReferences.push({ secretId: el.id, references }); secretReferences.push({ secretId: el.id, references });
const encryptedComment = comment const encryptedComment = comment

View File

@@ -187,7 +187,7 @@ export const secretServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -296,7 +296,7 @@ export const secretServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -433,7 +433,7 @@ export const secretServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -538,7 +538,7 @@ export const secretServiceFactory = ({
} else { } else {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
@@ -565,7 +565,9 @@ export const secretServiceFactory = ({
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug, environment: importEnv.slug,
secretPath: importPath secretPath: importPath,
secretName: "",
secretTags: []
}) })
) )
); );
@@ -619,7 +621,7 @@ export const secretServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) if (!folder)
@@ -671,7 +673,9 @@ export const secretServiceFactory = ({
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug, environment: importEnv.slug,
secretPath: importPath secretPath: importPath,
secretName: "",
secretTags: []
}) })
) )
); );
@@ -718,7 +722,7 @@ export const secretServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -803,7 +807,7 @@ export const secretServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -909,7 +913,7 @@ export const secretServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path, secretName: "", secretTags: [] })
); );
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -2118,7 +2122,7 @@ export const secretServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "", secretTags: [] })
); );
await projectDAL.checkProjectUpgradeStatus(project.id); await projectDAL.checkProjectUpgradeStatus(project.id);
@@ -2220,7 +2224,7 @@ export const secretServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "", secretTags: [] })
); );
await projectDAL.checkProjectUpgradeStatus(project.id); await projectDAL.checkProjectUpgradeStatus(project.id);
@@ -2407,17 +2411,32 @@ export const secretServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath: sourceSecretPath }) subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath: sourceSecretPath,
secretName: "",
secretTags: []
})
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath,
secretName: "",
secretTags: []
})
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, 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); const { botKey } = await projectBotService.getBotKey(project.id);

View File

@@ -66,7 +66,7 @@ export const serviceTokenServiceFactory = ({
scopes.forEach(({ environment, secretPath }) => { scopes.forEach(({ environment, secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "", secretTags: [] })
); );
}); });