Add/remove bulk users to projects upon add/remove users to/from groups

This commit is contained in:
Tuan Dang
2024-03-28 12:18:44 -07:00
parent 0da6262ead
commit 372f71f2b0
11 changed files with 299 additions and 67 deletions
@@ -155,6 +155,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod, req.permission.authMethod,
req.permission.orgId req.permission.orgId
); );
return { data: { permissions, membership } }; return { data: { permissions, membership } };
} }
}); });
+109 -7
View File
@@ -1,12 +1,17 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { OrgMembershipRole, SecretKeyEncoding, TOrgRoles } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGroupProjectDALFactory } from "../../../services/group-project/group-project-dal";
import { TOrgDALFactory } from "../../../services/org/org-dal"; import { TOrgDALFactory } from "../../../services/org/org-dal";
import { TProjectDALFactory } from "../../../services/project/project-dal";
import { TProjectBotDALFactory } from "../../../services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../../../services/project-key/project-key-dal";
import { TUserDALFactory } from "../../../services/user/user-dal"; import { TUserDALFactory } from "../../../services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -23,10 +28,17 @@ import {
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = { type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findOne">; userDAL: Pick<TUserDALFactory, "findOne" | "findUserEncKeyByUsername">;
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">; groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
groupProjectDAL: TGroupProjectDALFactory;
orgDAL: Pick<TOrgDALFactory, "findMembership">; orgDAL: Pick<TOrgDALFactory, "findMembership">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findOne" | "create" | "delete">; userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"findOne" | "create" | "delete" | "filterProjectsByUserMembership"
>;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "create" | "delete" | "findLatestProjectKey">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -36,8 +48,12 @@ export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
export const groupServiceFactory = ({ export const groupServiceFactory = ({
userDAL, userDAL,
groupDAL, groupDAL,
groupProjectDAL,
orgDAL, orgDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
projectDAL,
projectBotDAL,
projectKeyDAL,
permissionService, permissionService,
licenseService licenseService
}: TGroupServiceFactoryDep) => { }: TGroupServiceFactoryDep) => {
@@ -211,7 +227,7 @@ export const groupServiceFactory = ({
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" }); throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
// get user with username // get user with username
const user = await userDAL.findOne({ const user = await userDAL.findUserEncKeyByUsername({
username username
}); });
@@ -223,7 +239,7 @@ export const groupServiceFactory = ({
// check if user group membership already exists // check if user group membership already exists
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({ const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
groupId: group.id, groupId: group.id,
userId: user.id userId: user.userId
}); });
if (existingUserGroupMembership) if (existingUserGroupMembership)
@@ -233,7 +249,7 @@ export const groupServiceFactory = ({
// check if user is even part of the organization // check if user is even part of the organization
const existingUserOrgMembership = await orgDAL.findMembership({ const existingUserOrgMembership = await orgDAL.findMembership({
userId: user.id, userId: user.userId,
orgId orgId
}); });
@@ -243,10 +259,77 @@ export const groupServiceFactory = ({
}); });
await userGroupMembershipDAL.create({ await userGroupMembershipDAL.create({
userId: user.id, userId: user.userId,
groupId: group.id groupId: group.id
}); });
// check which projects the group is part of
const projectIds = (
await groupProjectDAL.find({
groupId: group.id
})
).map((gp) => gp.projectId);
const keys = await projectKeyDAL.find({
receiverId: user.userId,
$in: {
projectId: projectIds
}
});
const keysSet = new Set(keys.map((k) => k.projectId));
const projectsToAddKeyFor = projectIds.filter((p) => !keysSet.has(p));
for await (const projectId of projectsToAddKeyFor) {
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 { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, user.publicKey, botPrivateKey);
await projectKeyDAL.create({
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: user.userId,
projectId
});
}
return user; return user;
}; };
@@ -300,6 +383,25 @@ export const groupServiceFactory = ({
message: `User ${username} is not part of the group ${groupSlug}` message: `User ${username} is not part of the group ${groupSlug}`
}); });
const projectIds = (
await groupProjectDAL.find({
groupId: group.id
})
).map((gp) => gp.projectId);
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(user.id, group.id, projectIds);
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
if (projectsToDeleteKeyFor.length) {
await projectKeyDAL.delete({
receiverId: user.id,
$in: {
projectId: projectsToDeleteKeyFor
}
});
}
await userGroupMembershipDAL.delete({ await userGroupMembershipDAL.delete({
groupId: group.id, groupId: group.id,
userId: user.id userId: user.id
@@ -8,42 +8,49 @@ export type TUserGroupMembershipDALFactory = ReturnType<typeof userGroupMembersh
export const userGroupMembershipDALFactory = (db: TDbClient) => { export const userGroupMembershipDALFactory = (db: TDbClient) => {
const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership); const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership);
// special query /**
const findGroupMembersInProject = async (groupId: string, projectId: string) => { * Returns a sub-set of projectIds fed into this function corresponding to projects where either:
try { * - The user is a direct member of the project.
const members = await db(TableName.UserGroupMembership) * - The user is a member of a group that is a member of the project, excluding projects that they are part of
.where(`${TableName.UserGroupMembership}.groupId`, groupId) * through the group with id [groupId].
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) */
.join(TableName.ProjectMembership, function () { const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[]) => {
this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn( const userProjectMemberships: string[] = await db(TableName.ProjectMembership)
`${TableName.ProjectMembership}.projectId`, .where(`${TableName.ProjectMembership}.userId`, userId)
"=", .whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
db.raw("?", [projectId]) .pluck(`${TableName.ProjectMembership}.projectId`);
);
})
.leftJoin<TUserEncryptionKeys>(
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 }) => ({ const userGroupMemberships: string[] = await db(TableName.UserGroupMembership)
...data, .where(`${TableName.UserGroupMembership}.userId`, userId)
user: { email, username, firstName, lastName, id: userId, publicKey } .whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
})); .join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId`
)
.whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.GroupProjectMembership}.projectId`);
return new Set(userProjectMemberships.concat(userGroupMemberships));
};
// special query
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
try {
const usernameDocs: string[] = await db(TableName.UserGroupMembership)
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId`
)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.whereIn(`${TableName.Users}.username`, usernames) // TODO: pluck usernames
.pluck(`${TableName.Users}.id`);
return usernameDocs;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find group members in project" }); throw new DatabaseError({ error, name: "Find user group members in project" });
} }
}; };
@@ -51,7 +58,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
* Return list of users that are part of the group with id [groupId] * 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]. * that have not yet been added individually to project with id [projectId].
* *
* Filters out users that are part of other groups in the project. * Note: Filters out users that are part of other groups in the project.
* @param groupId * @param groupId
* @param projectId * @param projectId
* @returns * @returns
@@ -111,7 +118,8 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
return { return {
...userGroupMembershipOrm, ...userGroupMembershipOrm,
findGroupMembersInProject, filterProjectsByUserMembership,
findUserGroupMembershipsInProject,
findGroupMembersNotInProject findGroupMembersNotInProject
}; };
}; };
@@ -45,6 +45,44 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectPermission = async (userId: string, projectId: string) => { const getProjectPermission = async (userId: string, projectId: string) => {
try { try {
const groups: string[] = await db(TableName.GroupProjectMembership)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.pluck(`${TableName.GroupProjectMembership}.groupId`);
const groupDocs = await db(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.userId`, userId)
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
.join(
TableName.GroupProjectMembership,
`${TableName.GroupProjectMembership}.groupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
// TODO(roll-forward-migration): remove this field when we drop this in next migration after a week
db.ref("role").withSchema(TableName.GroupProjectMembership).as("oldRoleField"),
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"),
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
)
.select("permissions");
const docs = await db(TableName.ProjectMembership) const docs = await db(TableName.ProjectMembership)
.join( .join(
TableName.ProjectUserMembershipRole, TableName.ProjectUserMembershipRole,
@@ -67,17 +105,16 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"), db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"),
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
db.ref("projectId").withSchema(TableName.ProjectMembership),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project), db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug") db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
) )
.select("permissions"); .select("permissions");
console.log("getProjectPermission docs: ", docs);
const permission = sqlNestRelationships({ const permission = sqlNestRelationships({
data: docs, data: docs.concat(groupDocs),
key: "membershipId", key: "projectId",
parentMapper: ({ parentMapper: ({
orgId, orgId,
orgAuthEnforced, orgAuthEnforced,
@@ -107,11 +144,48 @@ export const permissionDALFactory = (db: TDbClient) => {
} }
] ]
}); });
// const permission = sqlNestRelationships({
// data: docs, // TODO: concat?
// key: "membershipId",
// parentMapper: ({
// orgId,
// orgAuthEnforced,
// membershipId,
// membershipCreatedAt,
// membershipUpdatedAt,
// oldRoleField,
// // role
// }) => ({
// orgId,
// orgAuthEnforced,
// userId,
// role: oldRoleField,
// // role,
// id: membershipId,
// projectId,
// createdAt: membershipCreatedAt,
// updatedAt: membershipUpdatedAt
// }),
// childrenMapper: [
// {
// key: "id",
// label: "roles" as const,
// mapper: (data) =>
// ProjectUserMembershipRolesSchema.extend({
// permissions: z.unknown(),
// customRoleSlug: z.string().optional().nullable()
// }).parse(data)
// }
// ]
// });
// when introducting cron mode change it here // when introducting cron mode change it here
const activeRoles = permission?.[0]?.roles.filter( const activeRoles = permission?.[0]?.roles.filter(
({ isTemporary, temporaryAccessEndTime }) => ({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
); );
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined; return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" }); throw new DatabaseError({ error, name: "GetProjectPermission" });
@@ -218,6 +218,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
+5
View File
@@ -247,8 +247,12 @@ export const registerRoutes = async (
const groupService = groupServiceFactory({ const groupService = groupServiceFactory({
userDAL, userDAL,
groupDAL, groupDAL,
groupProjectDAL,
orgDAL, orgDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
projectDAL,
projectBotDAL,
projectKeyDAL,
permissionService, permissionService,
licenseService licenseService
}); });
@@ -362,6 +366,7 @@ export const registerRoutes = async (
projectBotDAL, projectBotDAL,
orgDAL, orgDAL,
userDAL, userDAL,
userGroupMembershipDAL,
smtpService, smtpService,
projectKeyDAL, projectKeyDAL,
projectRoleDAL, projectRoleDAL,
@@ -17,6 +17,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
@@ -45,6 +46,7 @@ type TProjectMembershipServiceFactoryDep = {
projectMembershipDAL: TProjectMembershipDALFactory; projectMembershipDAL: TProjectMembershipDALFactory;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">; userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
@@ -63,6 +65,7 @@ export const projectMembershipServiceFactory = ({
projectBotDAL, projectBotDAL,
orgDAL, orgDAL,
userDAL, userDAL,
userGroupMembershipDAL,
projectDAL, projectDAL,
projectKeyDAL, projectKeyDAL,
licenseService licenseService
@@ -120,6 +123,13 @@ export const projectMembershipServiceFactory = ({
}); });
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" }); if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(
orgMembers.map(({ username }) => username),
projectId
)
);
await projectMembershipDAL.transaction(async (tx) => { await projectMembershipDAL.transaction(async (tx) => {
const projectMemberships = await projectMembershipDAL.insertMany( const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({ orgMembers.map(({ userId }) => ({
@@ -135,13 +145,15 @@ export const projectMembershipServiceFactory = ({
); );
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId); const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany( await projectKeyDAL.insertMany(
orgMembers.map(({ userId, id }) => ({ orgMembers
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, .filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId as string))
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, .map(({ userId, id }) => ({
senderId: actorId, encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
receiverId: userId as string, nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
projectId senderId: actorId,
})), receiverId: userId as string,
projectId
})),
tx tx
); );
}); });
@@ -247,6 +259,10 @@ export const projectMembershipServiceFactory = ({
const members: TProjectMemberships[] = []; const members: TProjectMemberships[] = [];
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
await projectMembershipDAL.transaction(async (tx) => { await projectMembershipDAL.transaction(async (tx) => {
const projectMemberships = await projectMembershipDAL.insertMany( const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({ orgMembers.map(({ user }) => ({
@@ -265,13 +281,15 @@ export const projectMembershipServiceFactory = ({
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId); const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany( await projectKeyDAL.insertMany(
orgMembers.map(({ user, id }) => ({ orgMembers
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, .filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, .map(({ user, id }) => ({
senderId: ghostUser.id, encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
receiverId: user.id, nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
projectId senderId: ghostUser.id,
})), receiverId: user.id,
projectId
})),
tx tx
); );
}); });
@@ -458,6 +476,10 @@ export const projectMembershipServiceFactory = ({
}); });
} }
const userIdsToExcludeFromProjectKeyRemoval = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
const memberships = await projectMembershipDAL.transaction(async (tx) => { const memberships = await projectMembershipDAL.transaction(async (tx) => {
const deletedMemberships = await projectMembershipDAL.delete( const deletedMemberships = await projectMembershipDAL.delete(
{ {
@@ -469,11 +491,15 @@ export const projectMembershipServiceFactory = ({
tx tx
); );
// delete project keys belonging to users that are not part of any other groups in the project
await projectKeyDAL.delete( await projectKeyDAL.delete(
{ {
projectId, projectId,
$in: { $in: {
receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean) receiverId: projectMembers
.filter(({ user }) => !userIdsToExcludeFromProjectKeyRemoval.has(user.id))
.map(({ user }) => user.id)
.filter(Boolean)
} }
}, },
tx tx
@@ -7,6 +7,7 @@ import {
faMoneyBill, faMoneyBill,
faServer, faServer,
faSignIn, faSignIn,
faUser,
faUserCog, faUserCog,
faUsers faUsers
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
@@ -35,11 +36,17 @@ type Props = {
const SIMPLE_PERMISSION_OPTIONS = [ const SIMPLE_PERMISSION_OPTIONS = [
{ {
title: "Members management", title: "User management",
subtitle: "Invite, view and remove members from the organization", subtitle: "Invite, view and remove users from the organization",
icon: faUsers, icon: faUser,
formName: "member" formName: "member"
}, },
{
title: "Group management",
subtitle: "Invite, view and remove user groups from the organization",
icon: faUsers,
formName: "groups"
},
{ {
title: "Machine identity management", title: "Machine identity management",
subtitle: "Create, view, update and remove (machine) identities from the organization", subtitle: "Create, view, update and remove (machine) identities from the organization",
@@ -28,6 +28,7 @@ export const formSchema = z.object({
}) })
.optional(), .optional(),
member: generalPermissionSchema, member: generalPermissionSchema,
groups: generalPermissionSchema,
role: generalPermissionSchema, role: generalPermissionSchema,
settings: generalPermissionSchema, settings: generalPermissionSchema,
"service-account": generalPermissionSchema, "service-account": generalPermissionSchema,
@@ -55,11 +55,17 @@ const SINGLE_PERMISSION_LIST = [
formName: "role" formName: "role"
}, },
{ {
title: "Project Members", title: "User management",
subtitle: "Project members management control", subtitle: "Add, view and remove users from the project",
icon: faUser, icon: faUser,
formName: "member" formName: "member"
}, },
{
title: "Group management",
subtitle: "Add, view and remove user groups from the project",
icon: faUsers,
formName: "groups"
},
{ {
title: "Machine identity management", title: "Machine identity management",
subtitle: "Add, view, update and remove (machine) identities from the project", subtitle: "Add, view, update and remove (machine) identities from the project",
@@ -36,6 +36,7 @@ export const formSchema = z.object({
.object({ .object({
secrets: z.record(multiEnvPermissionSchema).optional(), secrets: z.record(multiEnvPermissionSchema).optional(),
member: generalPermissionSchema, member: generalPermissionSchema,
groups: generalPermissionSchema,
identity: generalPermissionSchema, identity: generalPermissionSchema,
role: generalPermissionSchema, role: generalPermissionSchema,
integrations: generalPermissionSchema, integrations: generalPermissionSchema,