mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: completed easier changes on other files where permission is needed
This commit is contained in:
@@ -14,7 +14,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
|
||||
const accessApprovalPolicyFindQuery = async (
|
||||
tx: Knex,
|
||||
filter: TFindFilter<TAccessApprovalPolicies>,
|
||||
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
|
||||
customFilter?: {
|
||||
policyId?: string;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -14,7 +14,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
|
||||
const secretApprovalPolicyFindQuery = (
|
||||
tx: Knex,
|
||||
filter: TFindFilter<TSecretApprovalPolicies>,
|
||||
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
|
||||
customFilter?: {
|
||||
sapId?: string;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
: []
|
||||
}
|
||||
: {};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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: []
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -2,32 +2,35 @@ import { Knex } from "knex";
|
||||
|
||||
import { UnauthorizedError } from "../errors";
|
||||
|
||||
type TKnexDynamicPrimitiveOperator = {
|
||||
type TKnexDynamicPrimitiveOperator<T extends object> = {
|
||||
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
||||
value: string;
|
||||
field: string;
|
||||
field: Extract<keyof T, string>;
|
||||
};
|
||||
|
||||
type TKnexDynamicInOperator = {
|
||||
type TKnexDynamicInOperator<T extends object> = {
|
||||
operator: "in";
|
||||
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";
|
||||
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.
|
||||
// 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<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 }];
|
||||
|
||||
while (stack.length) {
|
||||
|
||||
@@ -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 = <K extends object>(db: Knex, dal: K) => ({
|
||||
export type TFindFilter<R extends object = object> = Partial<R> & {
|
||||
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||
$complex?: TKnexDynamicOperator<R>;
|
||||
};
|
||||
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>) => {
|
||||
void bd.where(filter);
|
||||
if ($in) {
|
||||
@@ -39,6 +41,9 @@ export const buildFindFilter =
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($complex) {
|
||||
buildDynamicKnexQuery($complex, bd);
|
||||
}
|
||||
return bd;
|
||||
};
|
||||
|
||||
|
||||
@@ -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: []
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<typeof secretV2BridgeDALFacto
|
||||
export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
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) => {
|
||||
try {
|
||||
const sec = await (tx || db)(TableName.SecretV2)
|
||||
@@ -484,6 +582,8 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
upsertSecretReferences,
|
||||
findReferencedSecretReferences,
|
||||
findAllProjectSecretValues,
|
||||
countByFolderIds
|
||||
countByFolderIds,
|
||||
findOne,
|
||||
find
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<TSecretV2BridgeDALFactory, "findByFolderId">;
|
||||
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;
|
||||
@@ -390,29 +385,29 @@ export const expandSecretReferencesFactory = ({
|
||||
folderDAL,
|
||||
canExpandValue
|
||||
}: 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 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<Record<string, string>>((prev, secret) => {
|
||||
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
secretTagDAL: TSecretTagDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
||||
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<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 ({
|
||||
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<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 } =
|
||||
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<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 } =
|
||||
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,
|
||||
|
||||
@@ -15,6 +15,12 @@ type TPartialSecret = Pick<TSecretsV2, "id" | "reminderRepeatDays" | "reminderNo
|
||||
|
||||
type TPartialInputSecret = Pick<TSecretsV2, "type" | "reminderNote" | "reminderRepeatDays" | "id">;
|
||||
|
||||
export type TSecretReferenceDTO = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretKey: string;
|
||||
};
|
||||
|
||||
export type TGetSecretsDTO = {
|
||||
expandSecretReferences?: boolean;
|
||||
path: string;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [] })
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user