-
Notifications
You must be signed in to change notification settings - Fork 38
fix(group): Only org admin/owners and group maintainers can list group members #2269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,9 @@ import ( | |
"fmt" | ||
|
||
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" | ||
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext" | ||
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" | ||
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" | ||
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" | ||
|
||
"github.com/go-kratos/kratos/v2/errors" | ||
|
@@ -233,17 +236,33 @@ func (g *GroupService) ListMembers(ctx context.Context, req *pb.GroupServiceList | |
return nil, err | ||
} | ||
|
||
if err := g.userHasPermissionToListGroupMember(ctx, currentOrg.ID, req.GetGroupReference()); err != nil { | ||
return nil, err | ||
} | ||
|
||
currentUser, err := requireCurrentUser(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Parse orgID | ||
orgUUID, err := uuid.Parse(currentOrg.ID) | ||
if err != nil { | ||
return nil, errors.BadRequest("invalid", "invalid organization ID") | ||
} | ||
|
||
// Parse requesterID (current user) | ||
requesterUUID, err := uuid.Parse(currentUser.ID) | ||
if err != nil { | ||
return nil, errors.BadRequest("invalid", "invalid user ID") | ||
} | ||
|
||
// Initialize the options for listing members | ||
opts := &biz.ListMembersOpts{ | ||
IdentityReference: &biz.IdentityReference{}, | ||
Maintainers: req.Maintainers, | ||
MemberEmail: req.MemberEmail, | ||
RequesterID: requesterUUID, | ||
} | ||
|
||
// Parse groupID and groupName from the request | ||
|
@@ -531,6 +550,79 @@ func (g *GroupService) ListProjects(ctx context.Context, req *pb.GroupServiceLis | |
}, nil | ||
} | ||
|
||
// userHasPermissionToAddGroupMember checks if the user has permission to add members to a group | ||
func (g *GroupService) userHasPermissionToAddGroupMember(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference) error { | ||
return g.userHasPermissionOnGroupMembershipsWithPolicy(ctx, orgID, groupIdentifier, authz.PolicyGroupAddMemberships) | ||
} | ||
|
||
// userHasPermissionToRemoveGroupMember checks if the user has permission to remove members from a group | ||
func (g *GroupService) userHasPermissionToRemoveGroupMember(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference) error { | ||
return g.userHasPermissionOnGroupMembershipsWithPolicy(ctx, orgID, groupIdentifier, authz.PolicyGroupRemoveMemberships) | ||
} | ||
|
||
// userHasPermissionToListPendingGroupInvitations checks if the user has permission to list pending group invitations | ||
func (g *GroupService) userHasPermissionToListPendingGroupInvitations(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference) error { | ||
return g.userHasPermissionOnGroupMembershipsWithPolicy(ctx, orgID, groupIdentifier, authz.PolicyGroupListPendingInvitations) | ||
} | ||
|
||
// userHasPermissionToListGroupMember checks if the user has permission to list group members | ||
func (g *GroupService) userHasPermissionToListGroupMember(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved all these methods from servier to |
||
return g.userHasPermissionOnGroupMembershipsWithPolicy(ctx, orgID, groupIdentifier, authz.PolicyGroupListMemberships) | ||
} | ||
|
||
// userHasPermissionToUpdateMembership checks if the user has permission to remove members from a group | ||
func (g *GroupService) userHasPermissionToUpdateMembership(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference) error { | ||
return g.userHasPermissionOnGroupMembershipsWithPolicy(ctx, orgID, groupIdentifier, authz.PolicyGroupUpdateMemberships) | ||
} | ||
|
||
// userHasPermissionOnGroupMembershipsWithPolicy is the core implementation that checks if a user has permission on a group | ||
// with an optional specific policy check. If the policy is nil, it falls back to the basic permission check. | ||
func (g *GroupService) userHasPermissionOnGroupMembershipsWithPolicy(ctx context.Context, orgID string, groupIdentifier *pb.IdentityReference, policy *authz.Policy) error { | ||
// Check if the user has admin or owner role in the organization | ||
userRole := usercontext.CurrentAuthzSubject(ctx) | ||
if userRole == "" { | ||
return errors.NotFound("not found", "current membership not found") | ||
} | ||
|
||
// Allow if user has admin or owner role | ||
if userRole == string(authz.RoleAdmin) || userRole == string(authz.RoleOwner) { | ||
return nil | ||
} | ||
|
||
groupID, groupName, err := groupIdentifier.Parse() | ||
if err != nil { | ||
return errors.BadRequest("invalid", fmt.Sprintf("invalid project reference: %s", err.Error())) | ||
} | ||
|
||
orgUUID, err := uuid.Parse(orgID) | ||
if err != nil { | ||
return errors.BadRequest("invalid", "invalid organization ID") | ||
} | ||
|
||
// Resolve the group identifier to a valid group ID | ||
resolvedGroupID, err := g.groupUseCase.ValidateGroupIdentifier(ctx, orgUUID, groupID, groupName) | ||
if err != nil { | ||
return handleUseCaseErr(err, g.log) | ||
} | ||
|
||
// Check the user's membership in the organization | ||
m := entities.CurrentMembership(ctx) | ||
for _, rm := range m.Resources { | ||
if rm.ResourceType == authz.ResourceTypeGroup && rm.ResourceID == resolvedGroupID { | ||
pass, err := g.enforcer.Enforce(string(rm.Role), policy) | ||
if err != nil { | ||
return handleUseCaseErr(err, g.log) | ||
} | ||
if pass { | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
// If neither a maintainer nor admin/owner, nor has specific policy permission, forbid the operation | ||
return errors.Forbidden("forbidden", "operation not allowed") | ||
} | ||
|
||
// bizGroupToPb converts a biz.Group to a pb.Group protobuf message. | ||
func bizGroupToPb(gr *biz.Group) *pb.Group { | ||
base := &pb.Group{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -128,6 +128,9 @@ type ListMembersOpts struct { | |
Maintainers *bool | ||
// MemberEmail is the email of the member to filter by. | ||
MemberEmail *string | ||
// RequesterID is the ID of the user who is requesting to list mmebers. Optional. | ||
// If provided, the requester must be a maintainer or admin. | ||
RequesterID uuid.UUID | ||
} | ||
|
||
// AddMemberToGroupOpts defines options for adding a member to a group. | ||
|
@@ -230,6 +233,13 @@ func (uc *GroupUseCase) ListMembers(ctx context.Context, orgID uuid.UUID, opts * | |
return nil, 0, err | ||
} | ||
|
||
// Validate requester permissions only if RequesterID is provided | ||
if opts.RequesterID != uuid.Nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's this requester part? When is it meant to not to exist? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the calling this method from a third party code. For example, we have another service calling It's basically here to check permissions a part from the service |
||
if err := uc.validateRequesterPermissions(ctx, orgID, opts.RequesterID, resolvedGroupID); err != nil { | ||
return nil, 0, fmt.Errorf("failed to validate requester permissions: %w", err) | ||
} | ||
} | ||
|
||
pgOpts := pagination.NewDefaultOffsetPaginationOpts() | ||
if paginationOpts != nil { | ||
pgOpts = paginationOpts | ||
|
@@ -512,7 +522,7 @@ func (uc *GroupUseCase) validateRequesterPermissions(ctx context.Context, orgID, | |
|
||
// If not a maintainer of this group, deny access | ||
if requesterGroupMembership == nil || requesterGroupMembership.Role != authz.RoleGroupMaintainer { | ||
return NewErrValidationStr("requester does not have permission to add members to this group") | ||
return NewErrValidationStr("requester does not have permission to manage members on this group") | ||
} | ||
|
||
return nil | ||
|
@@ -633,33 +643,10 @@ func (uc *GroupUseCase) RemoveMemberFromGroup(ctx context.Context, orgID uuid.UU | |
return NewErrNotFound("group") | ||
} | ||
|
||
// Validate requester permissions only if RequesterID is provided | ||
if opts.RequesterID != uuid.Nil { | ||
// Check if the requester is part of the organization | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was this code duplicated? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this code can be found in |
||
requesterMembership, err := uc.membershipRepo.FindByOrgAndUser(ctx, orgID, opts.RequesterID) | ||
if err != nil && !IsNotFound(err) { | ||
return NewErrValidationStr("failed to check existing membership") | ||
} | ||
|
||
if requesterMembership == nil { | ||
return NewErrValidationStr("requester is not a member of the organization") | ||
} | ||
|
||
// Check if the requester has sufficient permissions | ||
// Allow if the requester is an org owner or admin | ||
isAdminOrOwner := requesterMembership.Role == authz.RoleOwner || requesterMembership.Role == authz.RoleAdmin | ||
|
||
// If not an admin/owner, check if the requester is a maintainer of this group | ||
if !isAdminOrOwner { | ||
// Check if the requester is a maintainer of this group | ||
requesterGroupMembership, err := uc.membershipRepo.FindByUserAndResourceID(ctx, opts.RequesterID, resolvedGroupID) | ||
if err != nil && !IsNotFound(err) { | ||
return fmt.Errorf("failed to check requester's group membership: %w", err) | ||
} | ||
|
||
// If not a maintainer of this group, deny access | ||
if requesterGroupMembership == nil || requesterGroupMembership.Role != authz.RoleGroupMaintainer { | ||
return NewErrValidationStr("requester does not have permission to add members to this group") | ||
} | ||
if err := uc.validateRequesterPermissions(ctx, orgID, opts.RequesterID, resolvedGroupID); err != nil { | ||
return fmt.Errorf("failed to validate requester permissions: %w", err) | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this just for testing? or is it needed for any use case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the same way we have
AddMembers
and `RemoveMembers, I pass the requester to check the permissions on biz, only if present and to test it, yes. In this way we allow third party code to call this code without the problem of forcing a requester.