diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index a08a9851b..3be670027 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -155,6 +155,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { req.permission.authMethod, req.permission.orgId ); + return { data: { permissions, membership } }; } }); diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 919e0404e..fa2ba75c5 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -1,12 +1,17 @@ import { ForbiddenError } from "@casl/ability"; 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 { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { TGroupProjectDALFactory } from "../../../services/group-project/group-project-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 { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; @@ -23,10 +28,17 @@ import { import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal"; type TGroupServiceFactoryDep = { - userDAL: Pick; + userDAL: Pick; groupDAL: Pick; + groupProjectDAL: TGroupProjectDALFactory; orgDAL: Pick; - userGroupMembershipDAL: Pick; + userGroupMembershipDAL: Pick< + TUserGroupMembershipDALFactory, + "findOne" | "create" | "delete" | "filterProjectsByUserMembership" + >; + projectDAL: Pick; + projectBotDAL: Pick; + projectKeyDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -36,8 +48,12 @@ export type TGroupServiceFactory = ReturnType; export const groupServiceFactory = ({ userDAL, groupDAL, + groupProjectDAL, orgDAL, userGroupMembershipDAL, + projectDAL, + projectBotDAL, + projectKeyDAL, permissionService, licenseService }: TGroupServiceFactoryDep) => { @@ -211,7 +227,7 @@ export const groupServiceFactory = ({ throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" }); // get user with username - const user = await userDAL.findOne({ + const user = await userDAL.findUserEncKeyByUsername({ username }); @@ -223,7 +239,7 @@ export const groupServiceFactory = ({ // check if user group membership already exists const existingUserGroupMembership = await userGroupMembershipDAL.findOne({ groupId: group.id, - userId: user.id + userId: user.userId }); if (existingUserGroupMembership) @@ -233,7 +249,7 @@ export const groupServiceFactory = ({ // check if user is even part of the organization const existingUserOrgMembership = await orgDAL.findMembership({ - userId: user.id, + userId: user.userId, orgId }); @@ -243,10 +259,77 @@ export const groupServiceFactory = ({ }); await userGroupMembershipDAL.create({ - userId: user.id, + userId: user.userId, 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; }; @@ -300,6 +383,25 @@ export const groupServiceFactory = ({ 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({ groupId: group.id, userId: user.id 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 e6b9db730..e8a262c3e 100644 --- a/backend/src/ee/services/group/user-group-membership-dal.ts +++ b/backend/src/ee/services/group/user-group-membership-dal.ts @@ -8,42 +8,49 @@ 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 + /** + * Returns a sub-set of projectIds fed into this function corresponding to projects where either: + * - The user is a direct member of the project. + * - The user is a member of a group that is a member of the project, excluding projects that they are part of + * through the group with id [groupId]. + */ + const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[]) => { + const userProjectMemberships: string[] = await db(TableName.ProjectMembership) + .where(`${TableName.ProjectMembership}.userId`, userId) + .whereIn(`${TableName.ProjectMembership}.projectId`, projectIds) + .pluck(`${TableName.ProjectMembership}.projectId`); - return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({ - ...data, - user: { email, username, firstName, lastName, id: userId, publicKey } - })); + const userGroupMemberships: string[] = await db(TableName.UserGroupMembership) + .where(`${TableName.UserGroupMembership}.userId`, userId) + .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) { - 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] * 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 projectId * @returns @@ -111,7 +118,8 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { return { ...userGroupMembershipOrm, - findGroupMembersInProject, + filterProjectsByUserMembership, + findUserGroupMembershipsInProject, findGroupMembersNotInProject }; }; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index a7c5ef0d4..cd223399f 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -45,6 +45,44 @@ export const permissionDALFactory = (db: TDbClient) => { const getProjectPermission = async (userId: string, projectId: string) => { 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) .join( TableName.ProjectUserMembershipRole, @@ -67,17 +105,16 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"), db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), + db.ref("projectId").withSchema(TableName.ProjectMembership), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("orgId").withSchema(TableName.Project), db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug") ) .select("permissions"); - console.log("getProjectPermission docs: ", docs); - const permission = sqlNestRelationships({ - data: docs, - key: "membershipId", + data: docs.concat(groupDocs), + key: "projectId", parentMapper: ({ orgId, 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 const activeRoles = permission?.[0]?.roles.filter( ({ isTemporary, temporaryAccessEndTime }) => !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ); + return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined; } catch (error) { throw new DatabaseError({ error, name: "GetProjectPermission" }); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 87ba77e01..b24024bd4 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -218,6 +218,7 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index dfb0a6f28..89f448d98 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -247,8 +247,12 @@ export const registerRoutes = async ( const groupService = groupServiceFactory({ userDAL, groupDAL, + groupProjectDAL, orgDAL, userGroupMembershipDAL, + projectDAL, + projectBotDAL, + projectKeyDAL, permissionService, licenseService }); @@ -362,6 +366,7 @@ export const registerRoutes = async ( projectBotDAL, orgDAL, userDAL, + userGroupMembershipDAL, smtpService, projectKeyDAL, projectRoleDAL, diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 059c93210..163a3e23e 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -17,6 +17,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; +import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; import { ActorType } from "../auth/auth-type"; import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; @@ -45,6 +46,7 @@ type TProjectMembershipServiceFactoryDep = { projectMembershipDAL: TProjectMembershipDALFactory; projectUserMembershipRoleDAL: Pick; userDAL: Pick; + userGroupMembershipDAL: TUserGroupMembershipDALFactory; projectRoleDAL: Pick; orgDAL: Pick; projectDAL: Pick; @@ -63,6 +65,7 @@ export const projectMembershipServiceFactory = ({ projectBotDAL, orgDAL, userDAL, + userGroupMembershipDAL, projectDAL, projectKeyDAL, licenseService @@ -120,6 +123,13 @@ export const projectMembershipServiceFactory = ({ }); 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) => { const projectMemberships = await projectMembershipDAL.insertMany( orgMembers.map(({ userId }) => ({ @@ -135,13 +145,15 @@ export const projectMembershipServiceFactory = ({ ); const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId); await projectKeyDAL.insertMany( - orgMembers.map(({ userId, id }) => ({ - encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, - nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, - senderId: actorId, - receiverId: userId as string, - projectId - })), + orgMembers + .filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId as string)) + .map(({ userId, id }) => ({ + encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, + nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, + senderId: actorId, + receiverId: userId as string, + projectId + })), tx ); }); @@ -247,6 +259,10 @@ export const projectMembershipServiceFactory = ({ const members: TProjectMemberships[] = []; + const userIdsToExcludeForProjectKeyAddition = new Set( + await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId) + ); + await projectMembershipDAL.transaction(async (tx) => { const projectMemberships = await projectMembershipDAL.insertMany( orgMembers.map(({ user }) => ({ @@ -265,13 +281,15 @@ export const projectMembershipServiceFactory = ({ const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId); await projectKeyDAL.insertMany( - orgMembers.map(({ user, id }) => ({ - encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, - nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, - senderId: ghostUser.id, - receiverId: user.id, - projectId - })), + orgMembers + .filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id)) + .map(({ user, id }) => ({ + encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, + nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, + senderId: ghostUser.id, + receiverId: user.id, + projectId + })), 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 deletedMemberships = await projectMembershipDAL.delete( { @@ -469,11 +491,15 @@ export const projectMembershipServiceFactory = ({ tx ); + // delete project keys belonging to users that are not part of any other groups in the project await projectKeyDAL.delete( { projectId, $in: { - receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean) + receiverId: projectMembers + .filter(({ user }) => !userIdsToExcludeFromProjectKeyRemoval.has(user.id)) + .map(({ user }) => user.id) + .filter(Boolean) } }, tx diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx index 106da7047..684c79adb 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx @@ -7,6 +7,7 @@ import { faMoneyBill, faServer, faSignIn, + faUser, faUserCog, faUsers } from "@fortawesome/free-solid-svg-icons"; @@ -35,11 +36,17 @@ type Props = { const SIMPLE_PERMISSION_OPTIONS = [ { - title: "Members management", - subtitle: "Invite, view and remove members from the organization", - icon: faUsers, + title: "User management", + subtitle: "Invite, view and remove users from the organization", + icon: faUser, formName: "member" }, + { + title: "Group management", + subtitle: "Invite, view and remove user groups from the organization", + icon: faUsers, + formName: "groups" + }, { title: "Machine identity management", subtitle: "Create, view, update and remove (machine) identities from the organization", diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts index f5e535a18..e85e62d0c 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts @@ -28,6 +28,7 @@ export const formSchema = z.object({ }) .optional(), member: generalPermissionSchema, + groups: generalPermissionSchema, role: generalPermissionSchema, settings: generalPermissionSchema, "service-account": generalPermissionSchema, diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx index 8f09527d8..6fbef0cc8 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx @@ -55,11 +55,17 @@ const SINGLE_PERMISSION_LIST = [ formName: "role" }, { - title: "Project Members", - subtitle: "Project members management control", + title: "User management", + subtitle: "Add, view and remove users from the project", icon: faUser, formName: "member" }, + { + title: "Group management", + subtitle: "Add, view and remove user groups from the project", + icon: faUsers, + formName: "groups" + }, { title: "Machine identity management", subtitle: "Add, view, update and remove (machine) identities from the project", diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts index ac0350d76..08ef43d19 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts @@ -36,6 +36,7 @@ export const formSchema = z.object({ .object({ secrets: z.record(multiEnvPermissionSchema).optional(), member: generalPermissionSchema, + groups: generalPermissionSchema, identity: generalPermissionSchema, role: generalPermissionSchema, integrations: generalPermissionSchema,