Skip to content

Commit ecd792d

Browse files
committed
fix(membership): Removing a user from the org should cleanup resources
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
1 parent 176b703 commit ecd792d

6 files changed

Lines changed: 134 additions & 9 deletions

File tree

app/controlplane/cmd/wire_gen.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/pkg/biz/group.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ type GroupRepo interface {
4747
AddMemberToGroup(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID, maintainer bool) (*GroupMembership, error)
4848
// RemoveMemberFromGroup removes a user from a group.
4949
RemoveMemberFromGroup(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID) error
50+
// RemoveMemberFromAllGroups removes a user from all groups in the organization.
51+
RemoveMemberFromAllGroups(ctx context.Context, orgID uuid.UUID, userID uuid.UUID) error
5052
// UpdateMemberMaintainerStatus updates the maintainer status of a group member.
5153
UpdateMemberMaintainerStatus(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID, isMaintainer bool) error
5254
// ListPendingInvitationsByGroup retrieves a list of pending invitations for a group

app/controlplane/pkg/biz/membership.go

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type MembershipRepo interface {
5757
// RBAC methods
5858

5959
ListAllByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error)
60+
ListDirectMembershipsByUserInOrg(ctx context.Context, orgID uuid.UUID, userID uuid.UUID) ([]*Membership, error)
6061
// ListGroupMembershipsByUser returns all memberships of the users inherited from groups
6162
ListGroupMembershipsByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error)
6263
ListAllByResource(ctx context.Context, rt authz.ResourceType, id uuid.UUID) ([]*Membership, error)
@@ -69,13 +70,14 @@ type MembershipsRBAC interface {
6970

7071
type MembershipUseCase struct {
7172
repo MembershipRepo
73+
groupReo GroupRepo
7274
orgUseCase *OrganizationUseCase
7375
logger *log.Helper
7476
auditor *AuditorUseCase
7577
}
7678

77-
func NewMembershipUseCase(repo MembershipRepo, orgUC *OrganizationUseCase, auditor *AuditorUseCase, logger log.Logger) *MembershipUseCase {
78-
return &MembershipUseCase{repo: repo, orgUseCase: orgUC, logger: log.NewHelper(logger), auditor: auditor}
79+
func NewMembershipUseCase(repo MembershipRepo, orgUC *OrganizationUseCase, auditor *AuditorUseCase, groupRepo GroupRepo, logger log.Logger) *MembershipUseCase {
80+
return &MembershipUseCase{repo: repo, orgUseCase: orgUC, logger: log.NewHelper(logger), auditor: auditor, groupReo: groupRepo}
7981
}
8082

8183
// LeaveAndDeleteOrg deletes a membership (and the org i) from the database associated with the current user
@@ -99,9 +101,23 @@ func (uc *MembershipUseCase) LeaveAndDeleteOrg(ctx context.Context, userID, memb
99101
return NewErrNotFound("membership")
100102
}
101103

102-
uc.logger.Infow("msg", "Deleting membership", "user_id", userID, "membership_id", m.ID.String())
103-
if err := uc.repo.Delete(ctx, membershipUUID); err != nil {
104-
return fmt.Errorf("failed to delete membership: %w", err)
104+
// Get all direct memberships for the user in the org in regard to resources
105+
resourceMemberships, err := uc.repo.ListDirectMembershipsByUserInOrg(ctx, m.OrganizationID, userUUID)
106+
if err != nil {
107+
return fmt.Errorf("failed to find direct memberships in org: %w", err)
108+
}
109+
110+
for _, rm := range resourceMemberships {
111+
uc.logger.Infow("msg", "Deleting membership", "user_id", userID, "membership_id", rm.ID.String())
112+
113+
if err := uc.repo.Delete(ctx, rm.ID); err != nil {
114+
return fmt.Errorf("failed to delete membership: %w", err)
115+
}
116+
}
117+
118+
// Remove the user from all groups in the org
119+
if err := uc.groupReo.RemoveMemberFromAllGroups(ctx, m.OrganizationID, userUUID); err != nil {
120+
return fmt.Errorf("failed to remove user from all groups in org: %w", err)
105121
}
106122

107123
uc.auditor.Dispatch(ctx, &events.OrgUserLeft{
@@ -142,22 +158,59 @@ func (uc *MembershipUseCase) DeleteOther(ctx context.Context, orgID, userID, mem
142158
return NewErrInvalidUUID(err)
143159
}
144160

161+
// Find the membership to delete
145162
m, err := uc.repo.FindByIDInOrg(ctx, orgUUID, membershipUUID)
146163
if err != nil {
147164
return fmt.Errorf("failed to find membership: %w", err)
148165
} else if m == nil {
149166
return NewErrNotFound("membership")
150167
}
151168

169+
// Prevent users from deleting their own membership
152170
if m.User.ID == userID {
153171
return NewErrValidationStr("cannot delete yourself from the org")
154172
}
155173

174+
// Parse the user ID of the membership to be removed
175+
toRemoveUserUUID, err := uuid.Parse(m.User.ID)
176+
if err != nil {
177+
return NewErrInvalidUUID(err)
178+
}
179+
180+
// Log the deletion
156181
uc.logger.Infow("msg", "Deleting membership", "org_id", orgID, "membership_id", m.ID.String())
182+
183+
// Delete the main membership
157184
if err := uc.repo.Delete(ctx, membershipUUID); err != nil {
158185
return fmt.Errorf("failed to delete membership: %w", err)
159186
}
160187

188+
// Clean up all resource-related memberships
189+
if err := uc.deleteRelatedMemberships(ctx, m.OrganizationID, toRemoveUserUUID); err != nil {
190+
return err
191+
}
192+
193+
// Remove the user from all groups in the org
194+
if err := uc.groupReo.RemoveMemberFromAllGroups(ctx, m.OrganizationID, toRemoveUserUUID); err != nil {
195+
return fmt.Errorf("failed to remove user from all groups in org: %w", err)
196+
}
197+
198+
return nil
199+
}
200+
201+
// deleteRelatedMemberships removes all resource-related memberships for a user in an organization
202+
func (uc *MembershipUseCase) deleteRelatedMemberships(ctx context.Context, orgID, userID uuid.UUID) error {
203+
resourceMemberships, err := uc.repo.ListDirectMembershipsByUserInOrg(ctx, orgID, userID)
204+
if err != nil {
205+
return fmt.Errorf("failed to find direct memberships in org: %w", err)
206+
}
207+
208+
for _, rm := range resourceMemberships {
209+
if err := uc.repo.Delete(ctx, rm.ID); err != nil {
210+
return fmt.Errorf("failed to delete membership: %w", err)
211+
}
212+
}
213+
161214
return nil
162215
}
163216

app/controlplane/pkg/biz/testhelpers/wire_gen.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/pkg/data/group.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,59 @@ func (g GroupRepo) RemoveMemberFromGroup(ctx context.Context, orgID uuid.UUID, g
475475
return nil
476476
}
477477

478+
// RemoveMemberFromAllGroups removes a user from all groups in an organization.
479+
func (g GroupRepo) RemoveMemberFromAllGroups(ctx context.Context, orgID uuid.UUID, userID uuid.UUID) error {
480+
return WithTx(ctx, g.data.DB, func(tx *ent.Tx) error {
481+
// Find all group memberships for the user in the organization
482+
memberships, err := tx.GroupMembership.Query().
483+
Where(
484+
groupmembership.UserIDEQ(userID),
485+
groupmembership.DeletedAtIsNil(),
486+
groupmembership.HasGroupWith(group.OrganizationIDEQ(orgID)),
487+
).
488+
All(ctx)
489+
if err != nil {
490+
return fmt.Errorf("failed to query group memberships: %w", err)
491+
}
492+
493+
if len(memberships) == 0 {
494+
return nil // No memberships to remove
495+
}
496+
497+
now := time.Now()
498+
499+
for _, mem := range memberships {
500+
// Soft delete each membership
501+
if _, err := tx.GroupMembership.UpdateOne(mem).
502+
SetDeletedAt(now).
503+
SetUpdatedAt(now).
504+
Save(ctx); err != nil {
505+
return fmt.Errorf("failed to remove user from group: %w", err)
506+
}
507+
508+
if mem.Maintainer {
509+
// Also remove the user membership if it exists
510+
if _, err := tx.Membership.Delete().Where(
511+
membership.MemberID(userID),
512+
membership.ResourceTypeEQ(authz.ResourceTypeGroup),
513+
membership.HasOrganizationWith(organization.ID(orgID)),
514+
).Exec(ctx); err != nil {
515+
return fmt.Errorf("failed to remove user from group: %w", err)
516+
}
517+
}
518+
519+
// Decrement the member count of the group
520+
if err := tx.Group.UpdateOneID(mem.GroupID).
521+
AddMemberCount(-1).
522+
Exec(ctx); err != nil {
523+
return fmt.Errorf("failed to decrement group member count: %w", err)
524+
}
525+
}
526+
527+
return nil
528+
})
529+
}
530+
478531
// UpdateMemberMaintainerStatus updates the maintainer status of a group member.
479532
func (g GroupRepo) UpdateMemberMaintainerStatus(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, userID uuid.UUID, isMaintainer bool) error {
480533
return WithTx(ctx, g.data.DB, func(tx *ent.Tx) error {

app/controlplane/pkg/data/membership.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,23 @@ func (r *MembershipRepo) ListAllByUser(ctx context.Context, userID uuid.UUID) ([
259259
return entMembershipsToBiz(mm), nil
260260
}
261261

262+
// ListDirectMembershipsByUserInOrg returns all memberships of the user in a given organization
263+
func (r *MembershipRepo) ListDirectMembershipsByUserInOrg(ctx context.Context, orgID uuid.UUID, userID uuid.UUID) ([]*biz.Membership, error) {
264+
mm, err := r.data.DB.Membership.Query().Where(
265+
membership.MembershipTypeEQ(authz.MembershipTypeUser),
266+
membership.MemberID(userID),
267+
membership.HasOrganizationWith(
268+
organization.ID(orgID),
269+
),
270+
).WithOrganization().All(ctx)
271+
272+
if err != nil {
273+
return nil, fmt.Errorf("failed to query memberships: %w", err)
274+
}
275+
276+
return entMembershipsToBiz(mm), nil
277+
}
278+
262279
// ListGroupMembershipsByUser returns all memberships of the users inherited from groups
263280
func (r *MembershipRepo) ListGroupMembershipsByUser(ctx context.Context, userID uuid.UUID) ([]*biz.Membership, error) {
264281
// First query all group memberships for the user directly

0 commit comments

Comments
 (0)