Skip to content

Commit 174a8c5

Browse files
authored
fix(group): Deletion of groups handle correctly (#2256)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent 55828b6 commit 174a8c5

File tree

4 files changed

+108
-13
lines changed

4 files changed

+108
-13
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
-- Fix memberships for deleted groups
2+
-- This migration does the following:
3+
-- 1. Removes entries from the membership table where the member_id is a deleted group or resource_id is a deleted group
4+
-- 2. Marks group memberships with deleted group IDs as deleted (sets deleted_at)
5+
-- 3. Marks as deleted any pending invitations for deleted groups
6+
7+
-- First, delete all memberships where the member is a deleted group or the resource is a deleted group
8+
DELETE FROM "memberships"
9+
WHERE "member_id" IN (
10+
SELECT "id" FROM "groups"
11+
WHERE "deleted_at" IS NOT NULL
12+
) OR (
13+
"resource_id" IN (
14+
SELECT "id" FROM "groups"
15+
WHERE "deleted_at" IS NOT NULL
16+
)
17+
AND "resource_type" = 'group'
18+
);
19+
20+
-- Next, mark all group_memberships as deleted for deleted groups
21+
-- Only update records that don't already have a deleted_at value
22+
UPDATE "group_memberships"
23+
SET
24+
"deleted_at" = NOW(),
25+
"updated_at" = NOW()
26+
WHERE
27+
"group_id" IN (
28+
SELECT "id" FROM "groups"
29+
WHERE "deleted_at" IS NOT NULL
30+
)
31+
AND "deleted_at" IS NULL;
32+
33+
-- Finally, ark as deleted any pending invitations for deleted groups
34+
UPDATE "org_invitations"
35+
SET
36+
"deleted_at" = NOW()
37+
WHERE
38+
"status" = 'pending'
39+
AND "deleted_at" IS NULL
40+
AND "context"::jsonb ? 'group_id_to_join'
41+
AND "context"::jsonb->>'group_id_to_join' IN (
42+
SELECT "id"::text FROM "groups"
43+
WHERE "deleted_at" IS NOT NULL
44+
);

app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
h1:i8q/MAG0rdlc1GCi0fAagiO70ZEhj6wvvFp8kgM+l50=
1+
h1:WasPyGuTE05lx75h+qqmoAknSDJPoN50E8hqbhNFVQs=
22
20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M=
33
20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g=
44
20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI=
@@ -96,3 +96,4 @@ h1:i8q/MAG0rdlc1GCi0fAagiO70ZEhj6wvvFp8kgM+l50=
9696
20250702112642.sql h1:wrjVS+5h2hs7KNwPRBece5LgAsoEzxN/zNfvCnjoIUw=
9797
20250704090359.sql h1:a0ksfjy2dtzviJL16HbC4eT1xBxy2qFH5mNFOpYlUrA=
9898
20250710105502.sql h1:EA6Ta1qsZcrNoOrO5zUNgiweHDtjl0HUlobukRuruko=
99+
20250714172256.sql h1:S0ImNk0sMjWVVZvS6VVHn2h96/nx8GOf4aVxELbJAcg=

app/controlplane/pkg/data/group.go

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -369,20 +369,68 @@ func (g GroupRepo) Update(ctx context.Context, orgID uuid.UUID, groupID uuid.UUI
369369
}
370370

371371
// SoftDelete soft-deletes a group by setting the DeletedAt field to the current time.
372+
// It also marks all group memberships as deleted and removes any pending invitations related to the group.
372373
func (g GroupRepo) SoftDelete(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID) error {
373-
// Softly delete the group by setting the DeletedAt field
374-
_, err := g.data.DB.Group.UpdateOneID(groupID).
375-
SetDeletedAt(time.Now()).
376-
Where(group.OrganizationIDEQ(orgID), group.DeletedAtIsNil()).
377-
Save(ctx)
378-
if err != nil {
379-
if ent.IsNotFound(err) {
380-
return biz.NewErrNotFound("group")
374+
return WithTx(ctx, g.data.DB, func(tx *ent.Tx) error {
375+
now := time.Now()
376+
377+
// Softly delete the group by setting the DeletedAt field
378+
_, err := tx.Group.UpdateOneID(groupID).
379+
SetDeletedAt(now).
380+
Where(group.OrganizationIDEQ(orgID), group.DeletedAtIsNil()).
381+
Save(ctx)
382+
if err != nil {
383+
if ent.IsNotFound(err) {
384+
return biz.NewErrNotFound("group")
385+
}
386+
return fmt.Errorf("failed to mark group as deleted: %w", err)
381387
}
382-
return err
383-
}
384388

385-
return nil
389+
// Mark as deleted all group memberships for this group
390+
_, err = tx.GroupMembership.Update().
391+
Where(
392+
groupmembership.GroupID(groupID),
393+
groupmembership.DeletedAtIsNil(),
394+
).
395+
SetDeletedAt(now).
396+
Save(ctx)
397+
if err != nil && !ent.IsNotFound(err) {
398+
return fmt.Errorf("failed to mark group memberships as deleted: %w", err)
399+
}
400+
401+
// Delete all memberships where this group is either the member or the resource
402+
_, err = tx.Membership.Delete().Where(
403+
membership.HasOrganizationWith(organization.ID(orgID)),
404+
membership.Or(
405+
membership.MemberID(groupID),
406+
membership.And(
407+
membership.ResourceID(groupID),
408+
membership.ResourceTypeEQ(authz.ResourceTypeGroup),
409+
),
410+
),
411+
).Exec(ctx)
412+
if err != nil && !ent.IsNotFound(err) {
413+
return fmt.Errorf("failed to delete group memberships: %w", err)
414+
}
415+
416+
// Mark as deleted any pending invitations for this group
417+
_, err = tx.OrgInvitation.Update().
418+
Where(
419+
orginvitation.OrganizationIDEQ(orgID),
420+
orginvitation.DeletedAtIsNil(),
421+
orginvitation.StatusEQ(biz.OrgInvitationStatusPending),
422+
func(s *sql.Selector) {
423+
s.Where(sqljson.ValueEQ(orginvitation.FieldContext, groupID.String(), sqljson.DotPath("group_id_to_join")))
424+
},
425+
).
426+
SetDeletedAt(now).
427+
Save(ctx)
428+
if err != nil {
429+
return fmt.Errorf("failed to cancel pending invitations for deleted group: %w", err)
430+
}
431+
432+
return nil
433+
})
386434
}
387435

388436
// AddMemberToGroup adds a user to a group, creating a new membership if they are not already a member.

app/controlplane/pkg/data/project.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"fmt"
2121
"time"
2222

23+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/group"
24+
2325
"entgo.io/ent/dialect/sql"
2426
"entgo.io/ent/dialect/sql/sqljson"
2527
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
@@ -277,7 +279,7 @@ func (r *ProjectRepo) FindProjectMembershipByProjectAndID(ctx context.Context, o
277279
projectMembership.User = entUserToBizUser(u)
278280
case authz.MembershipTypeGroup:
279281
// Fetch the group details for group memberships
280-
g, err := r.data.DB.Group.Get(ctx, memberID)
282+
g, err := r.data.DB.Group.Query().Where(group.ID(memberID), group.DeletedAtIsNil()).Only(ctx)
281283
if err != nil {
282284
if ent.IsNotFound(err) {
283285
return nil, biz.NewErrNotFound("group")

0 commit comments

Comments
 (0)