feat: addressed backend review changes needed by scott

This commit is contained in:
=
2024-10-11 15:31:58 +05:30
parent 43cfd63660
commit 936a48f458
16 changed files with 104 additions and 93 deletions

View File

@@ -39,6 +39,8 @@ export async function up(knex: Knex): Promise<void> {
}
}
// secret permission is split into multiple ones like secrets, folders, imports and dynamic-secrets
// so we just find all the privileges with respective mapping and map it as needed
const identityPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select("*");
const updatedIdentityPrivilegesDocs = identityPrivileges
.filter((i) => {
@@ -63,7 +65,7 @@ export async function up(knex: Knex): Promise<void> {
}
const userPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select("*");
const updatedUserPriviegeDocs = userPrivileges
const updatedUserPrivilegeDocs = userPrivileges
.filter((i) => {
const permissionString = JSON.stringify(i.permissions || []);
return (
@@ -79,8 +81,8 @@ export async function up(knex: Knex): Promise<void> {
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions))))
}));
if (docs.length) {
for (let i = 0; i < updatedUserPriviegeDocs.length; i += CHUNK_SIZE) {
const chunk = updatedUserPriviegeDocs.slice(i, i + CHUNK_SIZE);
for (let i = 0; i < updatedUserPrivilegeDocs.length; i += CHUNK_SIZE) {
const chunk = updatedUserPrivilegeDocs.slice(i, i + CHUNK_SIZE);
await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge();
}
}

View File

@@ -203,14 +203,14 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:projectSlug/roles/slug/:slug",
url: "/:projectSlug/roles/slug/:roleSlug",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
}),
response: {
200: z.object({
@@ -226,7 +226,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
actor: req.permission.type,
projectSlug: req.params.projectSlug,
roleSlug: req.params.slug
roleSlug: req.params.roleSlug
});
return { role };
}

View File

@@ -1,36 +0,0 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ActorType } from "@app/services/auth/auth-type";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TIsApproversValid } from "./access-approval-policy-types";
export const isApproversValid = async ({
userIds,
projectId,
orgId,
envSlug,
actorAuthMethod,
secretPath,
permissionService
}: TIsApproversValid) => {
try {
for await (const userId of userIds) {
const { permission: approverPermission } = await permissionService.getProjectPermission(
ActorType.USER,
userId,
projectId,
actorAuthMethod,
orgId
);
ForbiddenError.from(approverPermission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath, secretName: "", secretTags: [] })
);
}
} catch {
return false;
}
return true;
};

View File

@@ -41,7 +41,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
// TODO(casl): change the lease to different permission
export const dynamicSecretLeaseServiceFactory = ({
dynamicSecretLeaseDAL,
dynamicSecretProviders,

View File

@@ -32,8 +32,6 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeServiceFactory
>;
// TODO(akhilmhdh): move this to more centralized
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])

View File

@@ -142,7 +142,7 @@ const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTI
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
// akhilmhdh: don't mondify this for v2
// akhilmhdh: don't modify this for v2
// if you want to update create a new schema
const SecretConditionV1Schema = z
.object({
@@ -339,6 +339,13 @@ const GeneralPermissionSchema = [
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Edit]).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
"Describe what action an entity can take."
)
})
];
@@ -360,13 +367,6 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
"Describe what action an entity can take."
)
}),
...GeneralPermissionSchema
]);

View File

@@ -74,7 +74,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
slug,
permissions: customPermission
});
return additionalPrivilege;
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);

View File

@@ -43,7 +43,7 @@ import {
fnSecretBulkDelete as fnSecretV2BridgeBulkDelete,
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllSecretReferences as getAllNestedSecretReferencesV2Bridge
getAllSecretReferences as getAllSecretReferencesV2Bridge
} 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";
@@ -523,7 +523,7 @@ export const secretApprovalRequestServiceFactory = ({
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
references: el.encryptedValue
? getAllNestedSecretReferencesV2Bridge(
? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({
cipherTextBlob: el.encryptedValue
}).toString()
@@ -547,7 +547,7 @@ export const secretApprovalRequestServiceFactory = ({
? {
encryptedValue: el.encryptedValue as Buffer,
references: el.encryptedValue
? getAllNestedSecretReferencesV2Bridge(
? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({
cipherTextBlob: el.encryptedValue
}).toString()

View File

@@ -28,7 +28,6 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
import {
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
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";
@@ -258,7 +257,6 @@ export const secretReplicationServiceFactory = ({
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
// TODO(casl): check with team
hasSecretAccess: () => true
});
// secrets that gets replicated across imports
@@ -418,9 +416,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: doc.secretValue
? getAllNestedSecretReferencesV2Bridge(doc.secretValue).nestedReferences
: []
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
};
})
});
@@ -446,9 +442,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue as Buffer,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: doc.secretValue
? getAllNestedSecretReferencesV2Bridge(doc.secretValue).nestedReferences
: []
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
}
};
})

View File

@@ -81,3 +81,25 @@ export const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
}
return chunks;
};
/*
* Returns all items from the first list that
* do not exist in the second list.
*/
export const diff = <T>(
root: readonly T[],
other: readonly T[],
identity: (item: T) => string | number | symbol = (t: T) => t as unknown as string | number | symbol
): T[] => {
if (!root?.length && !other?.length) return [];
if (root?.length === undefined) return [...other];
if (!other?.length) return [...root];
const bKeys = other.reduce(
(acc, item) => {
acc[identity(item)] = true;
return acc;
},
{} as Record<string | number | symbol, boolean>
);
return root.filter((a) => !bKeys[identity(a)]);
};

View File

@@ -21,10 +21,6 @@ type TKnexGroupOperator<T extends object> = {
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<T extends object> = TKnexGroupOperator<T> | TKnexNonGroupOperator<T>;
export const buildDynamicKnexQuery = <T extends object>(

View File

@@ -195,7 +195,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
req.permission.orgId
);
const allowedDynamicSecretEnviroments = // filter envs user has access to
const allowedDynamicSecretEnvironments = // filter envs user has access to
environments.filter((environment) =>
permission.can(
ProjectPermissionDynamicSecretActions.Lease,
@@ -203,7 +203,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
)
);
if (includeDynamicSecrets && allowedDynamicSecretEnviroments.length) {
if (includeDynamicSecrets && allowedDynamicSecretEnvironments.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,
@@ -212,7 +212,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
projectId,
search,
environmentSlugs: allowedDynamicSecretEnviroments,
environmentSlugs: allowedDynamicSecretEnvironments,
path: secretPath,
isInternal: true
});
@@ -227,7 +227,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search,
orderBy,
orderDirection,
environmentSlugs: allowedDynamicSecretEnviroments,
environmentSlugs: allowedDynamicSecretEnvironments,
path: secretPath,
limit: remainingLimit,
offset: adjustedOffset,

View File

@@ -543,9 +543,18 @@ export const secretImportServiceFactory = ({
// this will already order by position
// so anything based on this order will also be in right position
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
// TODO(casl): update here
return fnSecretsFromImports({ allowedImports: secretImports, folderDAL, secretDAL, secretImportDAL });
const allowedImports = secretImports.filter((el) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug,
secretPath: el.importPath,
secretName: "",
secretTags: []
})
)
);
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
};
const getRawSecretsFromImports = async ({
@@ -606,8 +615,19 @@ export const secretImportServiceFactory = ({
name: "bot_not_found_error"
});
const allowedImports = secretImports.filter((el) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: el.importEnv.slug,
secretPath: el.importPath,
secretName: "",
secretTags: []
})
)
);
const importedSecrets = await fnSecretsFromImports({
allowedImports: secretImports,
allowedImports,
folderDAL,
secretDAL,
secretImportDAL

View File

@@ -107,7 +107,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretV2}: FindOne` });
throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` });
}
};

View File

@@ -434,24 +434,24 @@ export const expandSecretReferencesFactory = ({
const [secretKey] = entities;
// eslint-disable-next-line no-continue,no-await-in-loop
const referedValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referedValue.tags))
const referredValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referredValue.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.`
});
const cacheKey = getCacheUniqueKey(environment, secretPath);
secretCache[cacheKey][secretKey] = referedValue;
if (INTERPOLATION_SYNTAX_REG.test(referedValue.value)) {
secretCache[cacheKey][secretKey] = referredValue;
if (INTERPOLATION_SYNTAX_REG.test(referredValue.value)) {
stack.push({
value: referedValue.value,
value: referredValue.value,
secretPath,
environment,
depth: depth + 1
});
}
if (referedValue) {
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value);
if (referredValue) {
expandedValue = expandedValue.replaceAll(interpolationSyntax, referredValue.value);
}
} else {
const secretReferenceEnvironment = entities[0];

View File

@@ -9,7 +9,7 @@ import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approv
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { diff, groupBy } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -105,7 +105,12 @@ export const secretV2BridgeServiceFactory = ({
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" });
throw new BadRequestError({
message: `Referenced environment not found. Missing ${diff(
uniqueReferenceEnvironmentSlugs,
referencesEnvironments.map((el) => el.slug)
).join(",")}`
});
const referencesEnvironmentGroupBySlug = groupBy(referencesEnvironments, (i) => i.slug);
const referredFolders = await folderDAL.findByManySecretPath(
@@ -122,7 +127,7 @@ export const secretV2BridgeServiceFactory = ({
const folderId =
referencesFolderGroupByPath[`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`][0]
?.id;
if (!folderId) throw new BadRequestError({ message: `Reference path ${el.secretPath} doesn't exist` });
if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` });
return {
operator: "and",
@@ -144,7 +149,12 @@ export const secretV2BridgeServiceFactory = ({
});
if (referredSecrets.length !== references.length)
throw new BadRequestError({ message: "Reference secret not found" });
throw new BadRequestError({
message: `Referenced secret not found. Found only ${diff(
references.map((el) => el.secretKey),
referredSecrets.map((el) => el.key)
).join(",")}`
});
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
references.forEach((el) => {
@@ -210,7 +220,8 @@ export const secretV2BridgeServiceFactory = ({
// 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.tagIds || []).length !== tags.length)
throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` });
const { secretName, type, ...inputSecretData } = inputSecret;
@@ -357,7 +368,8 @@ export const secretV2BridgeServiceFactory = ({
// 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.tagIds || []).length !== tags.length)
throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` });
// now check with new ids
ForbiddenError.from(permission).throwUnlessCan(
@@ -505,7 +517,7 @@ export const secretV2BridgeServiceFactory = ({
});
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
@@ -1088,7 +1100,8 @@ export const secretV2BridgeServiceFactory = ({
// 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" });
if (tags.length !== sanitizedTagIds.length)
throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` });
const tagsGroupByID = groupBy(tags, (i) => i.id);
inputSecrets.forEach((el) => {