diff --git a/backend/src/ee/services/group/user-group-membership-dal.ts b/backend/src/ee/services/group/user-group-membership-dal.ts index 1fad97441..e6b9db730 100644 --- a/backend/src/ee/services/group/user-group-membership-dal.ts +++ b/backend/src/ee/services/group/user-group-membership-dal.ts @@ -1,5 +1,6 @@ import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TUserGroupMembershipDALFactory = ReturnType; @@ -7,7 +8,110 @@ export type TUserGroupMembershipDALFactory = ReturnType { const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership); + // special query + const findGroupMembersInProject = async (groupId: string, projectId: string) => { + try { + const members = await db(TableName.UserGroupMembership) + .where(`${TableName.UserGroupMembership}.groupId`, groupId) + .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) + .join(TableName.ProjectMembership, function () { + this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn( + `${TableName.ProjectMembership}.projectId`, + "=", + db.raw("?", [projectId]) + ); + }) + .leftJoin( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .select( + db.ref("id").withSchema(TableName.UserGroupMembership), + db.ref("groupId").withSchema(TableName.UserGroupMembership), + db.ref("email").withSchema(TableName.Users), + db.ref("username").withSchema(TableName.Users), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users), + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("publicKey").withSchema(TableName.UserEncryptionKey) + ) + .where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER + + return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({ + ...data, + user: { email, username, firstName, lastName, id: userId, publicKey } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find group members in project" }); + } + }; + + /** + * Return list of users that are part of the group with id [groupId] + * that have not yet been added individually to project with id [projectId]. + * + * Filters out users that are part of other groups in the project. + * @param groupId + * @param projectId + * @returns + */ + const findGroupMembersNotInProject = async (groupId: string, projectId: string) => { + try { + // get list of groups in the project with id [projectId] + // that that are not the group with id [groupId] + const groups: string[] = await db(TableName.GroupProjectMembership) + .where(`${TableName.GroupProjectMembership}.projectId`, projectId) + .whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId) + .pluck(`${TableName.GroupProjectMembership}.groupId`); + + // main query + const members = await db(TableName.UserGroupMembership) + .where(`${TableName.UserGroupMembership}.groupId`, groupId) + .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) + .leftJoin(TableName.ProjectMembership, function () { + this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn( + `${TableName.ProjectMembership}.projectId`, + "=", + db.raw("?", [projectId]) + ); + }) + .whereNull(`${TableName.ProjectMembership}.userId`) + .leftJoin( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .select( + db.ref("id").withSchema(TableName.UserGroupMembership), + db.ref("groupId").withSchema(TableName.UserGroupMembership), + db.ref("email").withSchema(TableName.Users), + db.ref("username").withSchema(TableName.Users), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users), + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("publicKey").withSchema(TableName.UserEncryptionKey) + ) + .where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER + .whereNotIn(`${TableName.UserGroupMembership}.userId`, function () { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.select(`${TableName.UserGroupMembership}.userId`) + .from(TableName.UserGroupMembership) + .whereIn(`${TableName.UserGroupMembership}.groupId`, groups); + }); + + return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({ + ...data, + user: { email, username, firstName, lastName, id: userId, publicKey } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find group members not in project" }); + } + }; + return { - ...userGroupMembershipOrm + ...userGroupMembershipOrm, + findGroupMembersInProject, + findGroupMembersNotInProject }; }; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index fc0a85ac6..a7c5ef0d4 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -73,6 +73,8 @@ export const permissionDALFactory = (db: TDbClient) => { ) .select("permissions"); + console.log("getProjectPermission docs: ", docs); + const permission = sqlNestRelationships({ data: docs, key: "membershipId", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b091cd27a..dfb0a6f28 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -256,7 +256,10 @@ export const registerRoutes = async ( groupDAL, groupProjectDAL, groupProjectMembershipRoleDAL, + userGroupMembershipDAL, projectDAL, + projectKeyDAL, + projectBotDAL, projectRoleDAL, permissionService }); diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 0bb9c1c4d..b09f6594f 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,15 +1,20 @@ import { ForbiddenError } from "@casl/ability"; import ms from "ms"; -import { ProjectMembershipRole } from "@app/db/schemas"; +import { ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { TGroupDALFactory } from "../../ee/services/group/group-dal"; +import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; import { TProjectDALFactory } from "../project/project-dal"; +import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; +import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TGroupProjectDALFactory } from "./group-project-dal"; @@ -27,8 +32,11 @@ type TGroupProjectServiceFactoryDep = { TGroupProjectMembershipRoleDALFactory, "create" | "transaction" | "insertMany" | "delete" >; - projectDAL: Pick; + userGroupMembershipDAL: TUserGroupMembershipDALFactory; + projectDAL: Pick; + projectKeyDAL: Pick; projectRoleDAL: Pick; + projectBotDAL: TProjectBotDALFactory; groupDAL: Pick; permissionService: Pick; }; @@ -39,7 +47,10 @@ export const groupProjectServiceFactory = ({ groupDAL, groupProjectDAL, groupProjectMembershipRoleDAL, + userGroupMembershipDAL, projectDAL, + projectKeyDAL, + projectBotDAL, projectRoleDAL, permissionService }: TGroupProjectServiceFactoryDep) => { @@ -104,6 +115,66 @@ export const groupProjectServiceFactory = ({ ); return groupProjectMembership; }); + + // share project key with users in group that have not + // individually been added to the project and that are not part of + // other groups that are in the project + const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, projectId); + + if (groupMembers.length) { + const ghostUser = await projectDAL.findProjectGhostUser(projectId); + + if (!ghostUser) { + throw new BadRequestError({ + message: "Failed to find sudo user" + }); + } + + const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId); + + if (!ghostUserLatestKey) { + throw new BadRequestError({ + message: "Failed to find sudo user latest key" + }); + } + + const bot = await projectBotDAL.findOne({ projectId }); + + if (!bot) { + throw new BadRequestError({ + message: "Failed to find bot" + }); + } + + const botPrivateKey = infisicalSymmetricDecrypt({ + keyEncoding: bot.keyEncoding as SecretKeyEncoding, + iv: bot.iv, + tag: bot.tag, + ciphertext: bot.encryptedPrivateKey + }); + + const plaintextProjectKey = decryptAsymmetric({ + ciphertext: ghostUserLatestKey.encryptedKey, + nonce: ghostUserLatestKey.nonce, + publicKey: ghostUserLatestKey.sender.publicKey, + privateKey: botPrivateKey + }); + + const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => { + const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, publicKey, botPrivateKey); + + return { + encryptedKey, + nonce, + senderId: ghostUser.id, + receiverId: id, + projectId + }; + }); + + await projectKeyDAL.insertMany(projectKeyData); + } + return projectGroup; }; @@ -215,7 +286,19 @@ export const groupProjectServiceFactory = ({ const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to delete more privileged group" }); + const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, projectId); + + if (groupMembers.length) { + await projectKeyDAL.delete({ + projectId, + $in: { + receiverId: groupMembers.map(({ user: { id } }) => id) + } + }); + } + const [deletedGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId }); + return deletedGroup; }; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 369e005ac..09e99eca6 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -30,8 +30,33 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Environment}.position`, order: "asc" } ]); + const groups: string[] = await db(TableName.UserGroupMembership) + .where({ userId }) + .select(selectAllTableCols(TableName.UserGroupMembership)) + .pluck("groupId"); + + const groupWorkspaces = await db(TableName.GroupProjectMembership) + .whereIn("groupId", groups) + .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) + .whereNotIn( + `${TableName.Project}.id`, + workspaces.map(({ id }) => id) + ) + .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) + .select( + selectAllTableCols(TableName.Project), + db.ref("id").withSchema(TableName.Project).as("_id"), + db.ref("id").withSchema(TableName.Environment).as("envId"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName") + ) + .orderBy([ + { column: `${TableName.Project}.name`, order: "asc" }, + { column: `${TableName.Environment}.position`, order: "asc" } + ]); + const nestedWorkspaces = sqlNestRelationships({ - data: workspaces, + data: workspaces.concat(groupWorkspaces), key: "id", parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), childrenMapper: [ diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx index 883a59eb4..c4dd2ad4e 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx @@ -105,6 +105,7 @@ export const OrgGroupModal = ({ }); } handlePopUpToggle("group", false); + reset(); createNotification({ text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`,