Merge remote-tracking branch 'origin' into feat/import-sync-secret

This commit is contained in:
Tuan Dang
2024-04-09 08:40:10 -07:00
149 changed files with 2305 additions and 1385 deletions
@@ -153,7 +153,7 @@ export const authLoginServiceFactory = ({
username: email
});
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
throw new Error("Failed to find user");
}
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
validateProviderAuthToken(providerAuthToken as string, email);
@@ -192,7 +192,7 @@ export const authPaswordServiceFactory = ({
}: TCreateBackupPrivateKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
throw new Error("Failed to find user");
}
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key");
@@ -239,7 +239,7 @@ export const authPaswordServiceFactory = ({
const getBackupPrivateKeyOfUser = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) {
throw new Error("Failed to find user");
throw new Error("Failed to find user");
}
const backupKey = await authDAL.getBackupPrivateKeyByUserId(userId);
if (!backupKey) throw new Error("Failed to find user backup key");
+1
View File
@@ -7,6 +7,7 @@ export enum AuthMethod {
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml",
KEYCLOAK_SAML = "keycloak-saml",
LDAP = "ldap"
}
@@ -14,16 +14,25 @@ import {
import { BadRequestError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "./project-role-dal";
type TProjectRoleServiceFactoryDep = {
projectRoleDAL: TProjectRoleDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
};
export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>;
export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: TProjectRoleServiceFactoryDep) => {
export const projectRoleServiceFactory = ({
projectRoleDAL,
permissionService,
identityProjectMembershipRoleDAL,
projectUserMembershipRoleDAL
}: TProjectRoleServiceFactoryDep) => {
const createRole = async (
actor: ActorType,
actorId: string,
@@ -96,8 +105,25 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId });
const projectUserRole = await projectUserMembershipRoleDAL.findOne({ customRoleId: roleId });
if (identityRole) {
throw new BadRequestError({
message: "The role is assigned to one or more identities. Make sure to unassign them before deleting the role.",
name: "Delete role"
});
}
if (projectUserRole) {
throw new BadRequestError({
message: "The role is assigned to one or more users. Make sure to unassign them before deleting the role.",
name: "Delete role"
});
}
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" });
return deletedRole;
};
@@ -6,6 +6,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
@@ -65,6 +66,7 @@ type TProjectServiceFactoryDep = {
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -86,7 +88,8 @@ export const projectServiceFactory = ({
projectEnvDAL,
licenseService,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL
identityProjectMembershipRoleDAL,
keyStore
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@@ -323,6 +326,7 @@ export const projectServiceFactory = ({
};
});
await keyStore.deleteItem(`infisical-cloud-plan-${actorOrgId}`);
return results;
};
@@ -350,6 +354,7 @@ export const projectServiceFactory = ({
return delProject;
});
await keyStore.deleteItem(`infisical-cloud-plan-${actorOrgId}`);
return deletedProject;
};
@@ -70,9 +70,31 @@ export const secretImportDALFactory = (db: TDbClient) => {
}
};
const findByFolderIds = async (folderIds: string[], tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretImport)
.whereIn("folderId", folderIds)
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
.select(
db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
db.ref("slug").withSchema(TableName.Environment),
db.ref("name").withSchema(TableName.Environment),
db.ref("id").withSchema(TableName.Environment).as("envId")
)
.orderBy("position", "asc");
return docs.map(({ envId, slug, name, ...el }) => ({
...el,
importEnv: { id: envId, slug, name }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find secret imports" });
}
};
return {
...secretImportOrm,
find,
findByFolderIds,
findLastImportPosition,
updateAllPosition
};
+45
View File
@@ -171,6 +171,50 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
if (userId && !uuidValidate(userId)) {
// eslint-disable-next-line no-param-reassign
userId = undefined;
}
const secs = await (tx || db)(TableName.Secret)
.whereIn("folderId", folderIds)
.where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null });
})
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.select(selectAllTableCols(TableName.Secret))
.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"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
data: secs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
id,
color,
slug,
name
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get all secret" });
}
};
const findByBlindIndexes = async (
folderId: string,
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
@@ -207,6 +251,7 @@ export const secretDALFactory = (db: TDbClient) => {
bulkUpdateNoVersionIncrement,
getSecretTags,
findByFolderId,
findByFolderIds,
findByBlindIndexes
};
};
+139 -4
View File
@@ -1,4 +1,5 @@
/* eslint-disable no-await-in-loop */
import { subject } from "@casl/ability";
import path from "path";
import {
@@ -7,8 +8,11 @@ import {
SecretType,
TableName,
TSecretBlindIndexes,
TSecretFolders,
TSecrets
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import {
buildSecretBlindIndexFromName,
@@ -18,7 +22,9 @@ import {
import { BadRequestError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretDALFactory } from "./secret-dal";
import {
@@ -45,6 +51,133 @@ export const generateSecretBlindIndexBySalt = async (secretName: string, secretB
return secretBlindIndex;
};
type TRecursivelyFetchSecretsFromFoldersArg = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
};
type TGetPathsDTO = {
projectId: string;
environment: string;
currentPath: string;
auth: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
};
};
// Introduce a new interface for mapping parent IDs to their children
interface FolderMap {
[parentId: string]: TSecretFolders[];
}
const buildHierarchy = (folders: TSecretFolders[]): FolderMap => {
const map: FolderMap = {};
map.null = []; // Initialize mapping for root directory
folders.forEach((folder) => {
const parentId = folder.parentId || "null";
if (!map[parentId]) {
map[parentId] = [];
}
map[parentId].push(folder);
});
return map;
};
const generatePaths = (
map: FolderMap,
parentId: string = "null",
basePath: string = ""
): { path: string; folderId: string }[] => {
const children = map[parentId || "null"] || [];
let paths: { path: string; folderId: string }[] = [];
children.forEach((child) => {
// Determine if this is the root folder of the environment. If no parentId is present and the name is root, it's the root folder
const isRootFolder = child.name === "root" && !child.parentId;
// Form the current path based on the base path and the current child
// eslint-disable-next-line no-nested-ternary
const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`;
paths.push({
path: currPath,
folderId: child.id
}); // Add the current path
// Recursively generate paths for children, passing down the formatted pathh
const childPaths = generatePaths(map, child.id, currPath);
paths = paths.concat(
childPaths.map((p) => ({
path: p.path,
folderId: p.folderId
}))
);
});
return paths;
};
export const recursivelyGetSecretPaths = ({
folderDAL,
projectEnvDAL,
permissionService
}: TRecursivelyFetchSecretsFromFoldersArg) => {
const getPaths = async ({ projectId, environment, currentPath, auth }: TGetPathsDTO) => {
const env = await projectEnvDAL.findOne({
projectId,
slug: environment
});
if (!env) {
throw new Error(`'${environment}' environment not found in project with ID ${projectId}`);
}
// Fetch all folders in env once with a single query
const folders = await folderDAL.find({
envId: env.id
});
// Build the folder hierarchy map
const folderMap = buildHierarchy(folders);
// Generate the paths paths and normalize the root path to /
const paths = generatePaths(folderMap).map((p) => ({
path: p.path === "/" ? p.path : p.path.substring(1),
folderId: p.folderId
}));
const { permission } = await permissionService.getProjectPermission(
auth.actor,
auth.actorId,
projectId,
auth.actorAuthMethod,
auth.actorOrgId
);
// 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) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: folder.path
})
) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
);
return allowedPaths;
};
return getPaths;
};
type TInterpolateSecretArg = {
projectId: string;
secretEncKey: string;
@@ -202,9 +335,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
);
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedVal
: formatMultiValueEnv(expandedVal);
secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal);
}
return secrets;
@@ -212,7 +343,10 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
return expandSecrets;
};
export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environment: string }, key: string) => {
export const decryptSecretRaw = (
secret: TSecrets & { workspace: string; environment: string; secretPath?: string },
key: string
) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
@@ -240,6 +374,7 @@ export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environ
return {
secretKey,
secretPath: secret.secretPath,
workspace: secret.workspace,
environment: secret.environment,
secretValue,
+86 -19
View File
@@ -1,3 +1,5 @@
/* eslint-disable no-unreachable-loop */
/* eslint-disable no-await-in-loop */
import { ForbiddenError, subject } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas";
@@ -13,13 +15,20 @@ import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsFromImports } from "../secret-import/secret-import-fns";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretDALFactory } from "./secret-dal";
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
import {
decryptSecretRaw,
fnSecretBlindIndexCheck,
fnSecretBulkInsert,
fnSecretBulkUpdate,
recursivelyGetSecretPaths
} from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue";
import {
TAttachSecretTagsDTO,
@@ -47,20 +56,25 @@ type TSecretServiceFactoryDep = {
secretDAL: TSecretDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick<
TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
>;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretImportDAL: Pick<TSecretImportDALFactory, "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
};
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
export const secretServiceFactory = ({
secretDAL,
projectEnvDAL,
secretTagDAL,
secretVersionDAL,
folderDAL,
@@ -425,7 +439,8 @@ export const secretServiceFactory = ({
actor,
actorOrgId,
actorAuthMethod,
includeImports
includeImports,
recursive
}: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
@@ -434,19 +449,52 @@ export const secretServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
let paths: { folderId: string; path: string }[] = [];
if (recursive) {
const getPaths = recursivelyGetSecretPaths({
permissionService,
folderDAL,
projectEnvDAL
});
const deepPaths = await getPaths({
projectId,
environment,
currentPath: path,
auth: {
actor,
actorId,
actorAuthMethod,
actorOrgId
}
});
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: [] };
paths = [{ folderId: folder.id, path }];
}
const groupedPaths = groupBy(paths, (p) => p.folderId);
const secrets = await secretDAL.findByFolderIds(
paths.map((p) => p.folderId),
actorId
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return { secrets: [], imports: [] };
const folderId = folder.id;
const secrets = await secretDAL.findByFolderId(folderId, actorId);
if (includeImports) {
const secretImports = await secretImportDAL.find({ folderId });
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
// if its service token allow full access over imported one
actor === ActorType.SERVICE
@@ -464,12 +512,26 @@ export const secretServiceFactory = ({
secretDAL,
folderDAL
});
return {
secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })),
secrets: secrets.map((secret) => ({
...secret,
workspace: projectId,
environment,
secretPath: groupedPaths[secret.folderId][0].path
})),
imports: importedSecrets
};
}
return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) };
return {
secrets: secrets.map((secret) => ({
...secret,
workspace: projectId,
environment,
secretPath: groupedPaths[secret.folderId][0].path
}))
};
};
const getSecretByName = async ({
@@ -789,7 +851,8 @@ export const secretServiceFactory = ({
actorOrgId,
actorAuthMethod,
environment,
includeImports
includeImports,
recursive
}: TGetSecretsRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
@@ -802,7 +865,8 @@ export const secretServiceFactory = ({
actorOrgId,
actorAuthMethod,
path,
includeImports
includeImports,
recursive
});
return {
@@ -810,7 +874,10 @@ export const secretServiceFactory = ({
imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
...el,
secrets: importedSecrets.map((sec) =>
decryptSecretRaw({ ...sec, environment: el.environment, workspace: projectId }, botKey)
decryptSecretRaw(
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
botKey
)
)
}))
};
@@ -74,6 +74,7 @@ export type TGetSecretsDTO = {
path: string;
environment: string;
includeImports?: boolean;
recursive?: boolean;
} & TProjectPermission;
export type TGetASecretDTO = {
@@ -140,6 +141,7 @@ export type TGetSecretsRawDTO = {
path: string;
environment: string;
includeImports?: boolean;
recursive?: boolean;
} & TProjectPermission;
export type TGetASecretRawDTO = {