@@ -20,6 +20,9 @@ import (
2020 "fmt"
2121
2222 pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
23+ "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext"
24+ "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
25+ "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
2326 "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
2427
2528 "github.com/go-kratos/kratos/v2/errors"
@@ -233,17 +236,33 @@ func (g *GroupService) ListMembers(ctx context.Context, req *pb.GroupServiceList
233236 return nil , err
234237 }
235238
239+ if err := g .userHasPermissionToListGroupMember (ctx , currentOrg .ID , req .GetGroupReference ()); err != nil {
240+ return nil , err
241+ }
242+
243+ currentUser , err := requireCurrentUser (ctx )
244+ if err != nil {
245+ return nil , err
246+ }
247+
236248 // Parse orgID
237249 orgUUID , err := uuid .Parse (currentOrg .ID )
238250 if err != nil {
239251 return nil , errors .BadRequest ("invalid" , "invalid organization ID" )
240252 }
241253
254+ // Parse requesterID (current user)
255+ requesterUUID , err := uuid .Parse (currentUser .ID )
256+ if err != nil {
257+ return nil , errors .BadRequest ("invalid" , "invalid user ID" )
258+ }
259+
242260 // Initialize the options for listing members
243261 opts := & biz.ListMembersOpts {
244262 IdentityReference : & biz.IdentityReference {},
245263 Maintainers : req .Maintainers ,
246264 MemberEmail : req .MemberEmail ,
265+ RequesterID : requesterUUID ,
247266 }
248267
249268 // Parse groupID and groupName from the request
@@ -531,6 +550,79 @@ func (g *GroupService) ListProjects(ctx context.Context, req *pb.GroupServiceLis
531550 }, nil
532551}
533552
553+ // userHasPermissionToAddGroupMember checks if the user has permission to add members to a group
554+ func (g * GroupService ) userHasPermissionToAddGroupMember (ctx context.Context , orgID string , groupIdentifier * pb.IdentityReference ) error {
555+ return g .userHasPermissionOnGroupMembershipsWithPolicy (ctx , orgID , groupIdentifier , authz .PolicyGroupAddMemberships )
556+ }
557+
558+ // userHasPermissionToRemoveGroupMember checks if the user has permission to remove members from a group
559+ func (g * GroupService ) userHasPermissionToRemoveGroupMember (ctx context.Context , orgID string , groupIdentifier * pb.IdentityReference ) error {
560+ return g .userHasPermissionOnGroupMembershipsWithPolicy (ctx , orgID , groupIdentifier , authz .PolicyGroupRemoveMemberships )
561+ }
562+
563+ // userHasPermissionToListPendingGroupInvitations checks if the user has permission to list pending group invitations
564+ func (g * GroupService ) userHasPermissionToListPendingGroupInvitations (ctx context.Context , orgID string , groupIdentifier * pb.IdentityReference ) error {
565+ return g .userHasPermissionOnGroupMembershipsWithPolicy (ctx , orgID , groupIdentifier , authz .PolicyGroupListPendingInvitations )
566+ }
567+
568+ // userHasPermissionToListGroupMember checks if the user has permission to list group members
569+ func (g * GroupService ) userHasPermissionToListGroupMember (ctx context.Context , orgID string , groupIdentifier * pb.IdentityReference ) error {
570+ return g .userHasPermissionOnGroupMembershipsWithPolicy (ctx , orgID , groupIdentifier , authz .PolicyGroupListMemberships )
571+ }
572+
573+ // userHasPermissionToUpdateMembership checks if the user has permission to remove members from a group
574+ func (g * GroupService ) userHasPermissionToUpdateMembership (ctx context.Context , orgID string , groupIdentifier * pb.IdentityReference ) error {
575+ return g .userHasPermissionOnGroupMembershipsWithPolicy (ctx , orgID , groupIdentifier , authz .PolicyGroupUpdateMemberships )
576+ }
577+
578+ // userHasPermissionOnGroupMembershipsWithPolicy is the core implementation that checks if a user has permission on a group
579+ // with an optional specific policy check. If the policy is nil, it falls back to the basic permission check.
580+ func (g * GroupService ) userHasPermissionOnGroupMembershipsWithPolicy (ctx context.Context , orgID string , groupIdentifier * pb.IdentityReference , policy * authz.Policy ) error {
581+ // Check if the user has admin or owner role in the organization
582+ userRole := usercontext .CurrentAuthzSubject (ctx )
583+ if userRole == "" {
584+ return errors .NotFound ("not found" , "current membership not found" )
585+ }
586+
587+ // Allow if user has admin or owner role
588+ if userRole == string (authz .RoleAdmin ) || userRole == string (authz .RoleOwner ) {
589+ return nil
590+ }
591+
592+ groupID , groupName , err := groupIdentifier .Parse ()
593+ if err != nil {
594+ return errors .BadRequest ("invalid" , fmt .Sprintf ("invalid project reference: %s" , err .Error ()))
595+ }
596+
597+ orgUUID , err := uuid .Parse (orgID )
598+ if err != nil {
599+ return errors .BadRequest ("invalid" , "invalid organization ID" )
600+ }
601+
602+ // Resolve the group identifier to a valid group ID
603+ resolvedGroupID , err := g .groupUseCase .ValidateGroupIdentifier (ctx , orgUUID , groupID , groupName )
604+ if err != nil {
605+ return handleUseCaseErr (err , g .log )
606+ }
607+
608+ // Check the user's membership in the organization
609+ m := entities .CurrentMembership (ctx )
610+ for _ , rm := range m .Resources {
611+ if rm .ResourceType == authz .ResourceTypeGroup && rm .ResourceID == resolvedGroupID {
612+ pass , err := g .enforcer .Enforce (string (rm .Role ), policy )
613+ if err != nil {
614+ return handleUseCaseErr (err , g .log )
615+ }
616+ if pass {
617+ return nil
618+ }
619+ }
620+ }
621+
622+ // If neither a maintainer nor admin/owner, nor has specific policy permission, forbid the operation
623+ return errors .Forbidden ("forbidden" , "operation not allowed" )
624+ }
625+
534626// bizGroupToPb converts a biz.Group to a pb.Group protobuf message.
535627func bizGroupToPb (gr * biz.Group ) * pb.Group {
536628 base := & pb.Group {
0 commit comments