feat: updated all services with permission changes

This commit is contained in:
=
2024-10-05 17:17:53 +05:30
parent 17567ebd0f
commit 77740d2c86
9 changed files with 550 additions and 392 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -853,7 +853,7 @@ export const secretApprovalRequestServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "", secretTags: [] })
);
await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -1125,10 +1125,6 @@ export const secretApprovalRequestServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@@ -1292,6 +1288,23 @@ export const secretApprovalRequestServiceFactory = ({
const tagIds = unique(Object.values(commitTagIds).flat());
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "Tag not found" });
const tagsGroupById = groupBy(tags, (i) => i.id);
commits.forEach((commit) => {
let action = ProjectPermissionActions.Create;
if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit;
if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete;
ForbiddenError.from(permission).throwUnlessCan(
action,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: commit.key,
secretTags: commitTagIds[commit.key].map((secretTagId) => tagsGroupById[secretTagId][0].slug)
})
);
});
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
const doc = await secretApprovalRequestDAL.create(

View File

@@ -253,11 +253,13 @@ export const secretReplicationServiceFactory = ({
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
const sourceImportedSecrets = await fnSecretsV2FromImports({
allowedImports: sourceSecretImports,
secretImports: sourceSecretImports,
secretDAL: secretV2BridgeDAL,
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
// TODO(casl): check with team
hasSecretAccess: () => true
});
// secrets that gets replicated across imports
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({

View File

@@ -192,15 +192,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
req.permission.orgId
);
const permissiveEnvs = // filter envs user has access to
const allowedDynamicSecretEnviroments = // filter envs user has access to
environments.filter((environment) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
)
);
if (includeDynamicSecrets && permissiveEnvs.length) {
if (includeDynamicSecrets && allowedDynamicSecretEnviroments.length) {
// this is the unique count, ie duplicate secrets across envs only count as 1
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
actor: req.permission.type,
@@ -209,7 +209,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
projectId,
search,
environmentSlugs: permissiveEnvs,
environmentSlugs: allowedDynamicSecretEnviroments,
path: secretPath,
isInternal: true
});
@@ -224,7 +224,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search,
orderBy,
orderDirection,
environmentSlugs: permissiveEnvs,
environmentSlugs: allowedDynamicSecretEnviroments,
path: secretPath,
limit: remainingLimit,
offset: adjustedOffset,
@@ -241,13 +241,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
if (includeSecrets && permissiveEnvs.length) {
if (includeSecrets) {
// this is the unique count, ie duplicate secrets across envs only count as 1
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments: permissiveEnvs,
environments,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
@@ -260,7 +260,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environments: permissiveEnvs,
environments,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
@@ -272,7 +272,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
isInternal: true
});
for await (const environment of permissiveEnvs) {
for await (const environment of environments) {
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
if (secretCountFromEnv) {

View File

@@ -67,7 +67,8 @@ const getIntegrationSecretsV2 = async (
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
allowedImports: secretImports
secretImports,
hasSecretAccess: () => true
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {

View File

@@ -27,6 +27,7 @@ type TSecretImportSecretsV2 = {
slug: string;
name: string;
};
id: string;
folderId: string | undefined;
importFolderId: string;
secrets: (TSecretsV2 & {
@@ -139,24 +140,22 @@ export const fnSecretsFromImports = async ({
return secrets;
};
/* eslint-disable no-await-in-loop, no-continue */
export const fnSecretsV2FromImports = async ({
allowedImports: possibleCyclicImports,
secretImports: rootSecretImports,
folderDAL,
secretDAL,
secretImportDAL,
depth = 0,
cyclicDetector = new Set(),
decryptor,
expandSecretReferences
expandSecretReferences,
hasSecretAccess
}: {
allowedImports: (Omit<TSecretImports, "importEnv"> & {
secretImports: (Omit<TSecretImports, "importEnv"> & {
importEnv: { id: string; slug: string; name: string };
})[];
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
depth?: number;
cyclicDetector?: Set<string>;
decryptor: (value?: Buffer | null) => string;
expandSecretReferences?: (inputSecret: {
value?: string;
@@ -164,92 +163,108 @@ export const fnSecretsV2FromImports = async ({
secretPath: string;
environment: string;
}) => Promise<string | undefined>;
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}) => {
// avoid going more than a depth
if (depth >= LEVEL_BREAK) return [];
const cyclicDetector = new Set();
const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [
{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }
];
const allowedImports = possibleCyclicImports.filter(
({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath))
);
const processedImports: TSecretImportSecretsV2[] = [];
const importedFolders = (
await folderDAL.findByManySecretPath(
allowedImports.map(({ importEnv, importPath }) => ({
while (stack.length) {
const { secretImports, depth, parentImportedSecrets } = stack.pop()!;
if (depth > LEVEL_BREAK) continue;
const sanitizedImports = secretImports.filter(
({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath))
);
if (sanitizedImports.length) continue;
const importedFolders = await folderDAL.findByManySecretPath(
sanitizedImports.map(({ importEnv, importPath }) => ({
envId: importEnv.id,
secretPath: importPath
}))
)
).filter(Boolean); // remove undefined ones
if (!importedFolders.length) {
return [];
}
);
if (!importedFolders.length) continue;
const importedFolderIds = importedFolders.map((el) => el?.id) as string[];
const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`);
const importedSecrets = await secretDAL.find(
{
$in: { folderId: importedFolderIds },
type: SecretType.Shared
},
{
sort: [["id", "asc"]]
}
);
const importedFolderIds = importedFolders.map((el) => el?.id) as string[];
const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`);
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
const importedSecrets = await secretDAL.find(
{
$in: { folderId: importedFolderIds },
type: SecretType.Shared
},
{
sort: [["id", "asc"]]
}
);
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
allowedImports.forEach(({ importPath, importEnv }) => {
cyclicDetector.add(getImportUniqKey(importEnv.slug, importPath));
});
// now we need to check recursively deeper imports made inside other imports
// we go level wise meaning we take all imports of a tree level and then go deeper ones level by level
const deeperImports = await secretImportDAL.findByFolderIds(importedFolderIds);
let secretsFromDeeperImports: TSecretImportSecretsV2[] = [];
if (deeperImports.length) {
secretsFromDeeperImports = await fnSecretsV2FromImports({
allowedImports: deeperImports.filter(({ isReplication }) => !isReplication),
secretImportDAL,
folderDAL,
secretDAL,
depth: depth + 1,
cyclicDetector,
decryptor,
expandSecretReferences
sanitizedImports.forEach(({ importPath, importEnv }) => {
cyclicDetector.add(getImportUniqKey(importEnv.slug, importPath));
});
// now we need to check recursively deeper imports made inside other imports
// we go level wise meaning we take all imports of a tree level and then go deeper ones level by level
const deeperImports = await secretImportDAL.findByFolderIds(importedFolderIds);
const deeperImportsGroupByFolderId = groupBy(deeperImports, (i) => i.folderId);
const isFirstIteration = processedImports.length;
sanitizedImports.forEach(({ importPath, importEnv, id, folderId }, i) => {
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`]?.[0];
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
.filter((item) =>
hasSecretAccess(
importEnv.slug,
importPath,
item.key,
item.tags.map((el) => el.slug)
)
)
.map((item) => ({
...item,
secretKey: item.key,
secretValue: decryptor(item.encryptedValue),
secretComment: decryptor(item.encryptedComment),
environment: importEnv.slug,
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
}));
if (deeperImportsGroupByFolderId?.[sourceImportFolder?.id || ""]) {
stack.push({
secretImports: deeperImportsGroupByFolderId[sourceImportFolder?.id || ""],
depth: depth + 1,
parentImportedSecrets: secretsWithDuplicate
});
}
if (isFirstIteration) {
processedImports.push({
secretPath: importPath,
environment: importEnv.slug,
environmentInfo: importEnv,
folderId: importedFolders?.[i]?.id,
id,
importFolderId: folderId,
secrets: unique(secretsWithDuplicate, (el) => el.secretKey)
});
} else {
parentImportedSecrets.push(...secretsWithDuplicate);
}
});
}
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`]?.[0];
const folderDeeperImportSecrets =
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
.map((item) => ({
...item,
secretKey: item.key,
secretValue: decryptor(item.encryptedValue),
secretComment: decryptor(item.encryptedComment),
environment: importEnv.slug,
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
}))
.concat(folderDeeperImportSecrets);
return {
secretPath: importPath,
environment: importEnv.slug,
environmentInfo: importEnv,
folderId: importedFolders?.[i]?.id,
id,
importFolderId: folderId,
secrets: unique(secretsWithDuplicate, (el) => el.secretKey)
};
});
/* eslint-enable */
if (expandSecretReferences) {
await Promise.allSettled(
processedImports.map((processedImport) =>
Promise.allSettled(
processedImports.map((processedImport) => {
// eslint-disable-next-line
processedImport.secrets = unique(processedImport.secrets, (i) => i.key);
return Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => {
const expandedSecretValue = await expandSecretReferences({
value: decryptedSecret.secretValue,
@@ -260,8 +275,8 @@ export const fnSecretsV2FromImports = async ({
// eslint-disable-next-line no-param-reassign
processedImport.secrets[index].secretValue = expandedSecretValue || "";
})
)
)
);
})
);
}

View File

@@ -581,11 +581,21 @@ export const secretImportServiceFactory = ({
projectId
});
const importedSecrets = await fnSecretsV2FromImports({
allowedImports: secretImports,
secretImports,
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
)
});
return importedSecrets;
}

View File

@@ -381,12 +381,14 @@ export const secretV2BridgeServiceFactory = ({
projectId
});
const encryptedValue = secretValue
? secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob
: undefined;
const secretReferences = secretValue ? getAllSecretReferences(secretValue) : undefined;
? {
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(secretValue) }).cipherTextBlob,
references: getAllSecretReferences(secretValue).nestedReferences
}
: {};
if (secretReferences) {
const { nestedReferences, localReferences } = secretReferences;
if (secretValue) {
const { nestedReferences, localReferences } = getAllSecretReferences(secretValue);
const allSecretReferences = nestedReferences.concat(
localReferences.map((el) => ({ secretKey: el, secretPath, environment }))
);
@@ -409,7 +411,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
key: inputSecret.newSecretName || secretName,
tags: inputSecret.tagIds,
...(encryptedValue ? { encryptedValue, references: secretReferences?.nestedReferences || [] } : {})
...encryptedValue
}
}
],
@@ -671,22 +673,34 @@ export const secretV2BridgeServiceFactory = ({
projectId
});
const decryptedSecrets = secrets.map((secret) =>
reshapeBridgeSecret(
projectId,
groupedPaths[secret.folderId][0].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: groupedPaths[el.folderId][0].environment,
secretPath: groupedPaths[el.folderId][0].path,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
)
)
);
.map((secret) =>
reshapeBridgeSecret(
projectId,
groupedPaths[secret.folderId][0].environment,
groupedPaths[secret.folderId][0].path,
{
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
}
)
);
return decryptedSecrets;
};
@@ -819,12 +833,22 @@ export const secretV2BridgeServiceFactory = ({
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
const allowedImports = secretImports.filter(({ isReplication }) => !isReplication);
const importedSecrets = await fnSecretsV2FromImports({
allowedImports,
secretImports: allowedImports,
secretDAL,
folderDAL,
secretImportDAL,
expandSecretReferences,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
)
});
return {
@@ -936,12 +960,22 @@ export const secretV2BridgeServiceFactory = ({
if (!secret && includeImports) {
const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
const importedSecrets = await fnSecretsV2FromImports({
allowedImports: secretImports,
secretImports,
secretDAL,
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: expandEnvironment,
secretPath: expandSecretPath,
secretName: expandSecretKey,
secretTags: expandSecretTags
})
)
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {

View File

@@ -342,7 +342,8 @@ export const secretQueueFactory = ({
secretDAL: secretV2BridgeDAL,
expandSecretReferences,
secretImportDAL,
allowedImports: secretImports
secretImports,
hasSecretAccess: () => true
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {