Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-- Fix memberships for deleted groups
-- This migration does the following:
-- 1. Removes entries from the membership table where the member_id is a deleted group or resource_id is a deleted group
-- 2. Marks group memberships with deleted group IDs as deleted (sets deleted_at)
-- 3. Marks as deleted any pending invitations for deleted groups

-- First, delete all memberships where the member is a deleted group or the resource is a deleted group
DELETE FROM "memberships"
WHERE "member_id" IN (
SELECT "id" FROM "groups"
WHERE "deleted_at" IS NOT NULL
) OR (
"resource_id" IN (
SELECT "id" FROM "groups"
WHERE "deleted_at" IS NOT NULL
)
AND "resource_type" = 'group'
);

-- Next, mark all group_memberships as deleted for deleted groups
-- Only update records that don't already have a deleted_at value
UPDATE "group_memberships"
SET
"deleted_at" = NOW(),
"updated_at" = NOW()
WHERE
"group_id" IN (
SELECT "id" FROM "groups"
WHERE "deleted_at" IS NOT NULL
)
AND "deleted_at" IS NULL;

-- Finally, ark as deleted any pending invitations for deleted groups
UPDATE "org_invitations"
SET
"deleted_at" = NOW()
WHERE
"status" = 'pending'
AND "deleted_at" IS NULL
AND "context"::jsonb ? 'group_id_to_join'
AND "context"::jsonb->>'group_id_to_join' IN (
SELECT "id"::text FROM "groups"
WHERE "deleted_at" IS NOT NULL
);
3 changes: 2 additions & 1 deletion app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
h1:i8q/MAG0rdlc1GCi0fAagiO70ZEhj6wvvFp8kgM+l50=
h1:WasPyGuTE05lx75h+qqmoAknSDJPoN50E8hqbhNFVQs=
20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M=
20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g=
20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI=
Expand Down Expand Up @@ -96,3 +96,4 @@ h1:i8q/MAG0rdlc1GCi0fAagiO70ZEhj6wvvFp8kgM+l50=
20250702112642.sql h1:wrjVS+5h2hs7KNwPRBece5LgAsoEzxN/zNfvCnjoIUw=
20250704090359.sql h1:a0ksfjy2dtzviJL16HbC4eT1xBxy2qFH5mNFOpYlUrA=
20250710105502.sql h1:EA6Ta1qsZcrNoOrO5zUNgiweHDtjl0HUlobukRuruko=
20250714172256.sql h1:S0ImNk0sMjWVVZvS6VVHn2h96/nx8GOf4aVxELbJAcg=
70 changes: 59 additions & 11 deletions app/controlplane/pkg/data/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,20 +369,68 @@ func (g GroupRepo) Update(ctx context.Context, orgID uuid.UUID, groupID uuid.UUI
}

// SoftDelete soft-deletes a group by setting the DeletedAt field to the current time.
// It also marks all group memberships as deleted and removes any pending invitations related to the group.
func (g GroupRepo) SoftDelete(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID) error {
// Softly delete the group by setting the DeletedAt field
_, err := g.data.DB.Group.UpdateOneID(groupID).
SetDeletedAt(time.Now()).
Where(group.OrganizationIDEQ(orgID), group.DeletedAtIsNil()).
Save(ctx)
if err != nil {
if ent.IsNotFound(err) {
return biz.NewErrNotFound("group")
return WithTx(ctx, g.data.DB, func(tx *ent.Tx) error {
now := time.Now()

// Softly delete the group by setting the DeletedAt field
_, err := tx.Group.UpdateOneID(groupID).
SetDeletedAt(now).
Where(group.OrganizationIDEQ(orgID), group.DeletedAtIsNil()).
Save(ctx)
if err != nil {
if ent.IsNotFound(err) {
return biz.NewErrNotFound("group")
}
return fmt.Errorf("failed to mark group as deleted: %w", err)
}
return err
}

return nil
// Mark as deleted all group memberships for this group
_, err = tx.GroupMembership.Update().
Where(
groupmembership.GroupID(groupID),
groupmembership.DeletedAtIsNil(),
).
SetDeletedAt(now).
Save(ctx)
if err != nil && !ent.IsNotFound(err) {
return fmt.Errorf("failed to mark group memberships as deleted: %w", err)
}

// Delete all memberships where this group is either the member or the resource
_, err = tx.Membership.Delete().Where(
membership.HasOrganizationWith(organization.ID(orgID)),
membership.Or(
membership.MemberID(groupID),
membership.And(
membership.ResourceID(groupID),
membership.ResourceTypeEQ(authz.ResourceTypeGroup),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this possible? a membership with type = group? Group memberships have their own table, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replying myself, the maintainer role is stored here, thanks!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the maintainer one

),
),
).Exec(ctx)
if err != nil && !ent.IsNotFound(err) {
return fmt.Errorf("failed to delete group memberships: %w", err)
}

// Mark as deleted any pending invitations for this group
_, err = tx.OrgInvitation.Update().
Where(
orginvitation.OrganizationIDEQ(orgID),
orginvitation.DeletedAtIsNil(),
orginvitation.StatusEQ(biz.OrgInvitationStatusPending),
func(s *sql.Selector) {
s.Where(sqljson.ValueEQ(orginvitation.FieldContext, groupID.String(), sqljson.DotPath("group_id_to_join")))
},
).
SetDeletedAt(now).
Save(ctx)
if err != nil {
return fmt.Errorf("failed to cancel pending invitations for deleted group: %w", err)
}

return nil
})
}

// AddMemberToGroup adds a user to a group, creating a new membership if they are not already a member.
Expand Down
4 changes: 3 additions & 1 deletion app/controlplane/pkg/data/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"fmt"
"time"

"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/group"

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