Skip to content

feat: org soft-deletion #2362

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
36 changes: 10 additions & 26 deletions app/controlplane/pkg/biz/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,7 @@ func (uc *OrganizationUseCase) FindByName(ctx context.Context, name string) (*Or
return org, nil
}

// Delete deletes an organization and all relevant data
// This includes:
// - The organization
// - The associated repositories
// - The associated integrations
// The reason for just deleting these two associated components only is because
// they have external secrets that need to be deleted as well, and for that we leverage their own delete methods
// The rest of the data gets removed by the database cascade delete
// Delete soft-deletes an organization and all relevant data
func (uc *OrganizationUseCase) Delete(ctx context.Context, id string) error {
orgUUID, err := uuid.Parse(id)
if err != nil {
Expand All @@ -263,30 +256,21 @@ func (uc *OrganizationUseCase) Delete(ctx context.Context, id string) error {
return NewErrNotFound("organization")
}

// Delete all the integrations
integrations, err := uc.integrationUC.List(ctx, id)
// Delete all memberships for this organization
// Memberships should be removed when an organization is soft-deleted
// since they represent access rights that should be revoked
memberships, _, err := uc.membershipRepo.FindByOrg(ctx, orgUUID, nil, nil)
if err != nil {
return err
}

for _, i := range integrations {
if err := uc.integrationUC.Delete(ctx, id, i.ID.String()); err != nil {
return err
}
}

backends, err := uc.casBackendUseCase.List(ctx, org.ID)
if err != nil {
return fmt.Errorf("failed to list backends: %w", err)
return fmt.Errorf("failed to find memberships: %w", err)
}

for _, b := range backends {
if err := uc.casBackendUseCase.Delete(ctx, b.ID.String()); err != nil {
return fmt.Errorf("failed to delete backend: %w", err)
for _, m := range memberships {
if err := uc.membershipRepo.Delete(ctx, m.ID); err != nil {
return fmt.Errorf("failed to delete membership: %w", err)
}
}

// Delete the organization
// Soft-delete the organization
return uc.orgRepo.Delete(ctx, orgUUID)
}

Expand Down
23 changes: 16 additions & 7 deletions app/controlplane/pkg/biz/organization_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (s *OrgIntegrationTestSuite) TestCreate() {
}

for _, tc := range testCases {
s.T().Run(tc.name, func(_ *testing.T) {
s.Run(tc.name, func() {
org, err := s.Organization.Create(ctx, tc.name)
if tc.expectedError {
s.Error(err)
Expand Down Expand Up @@ -166,28 +166,37 @@ func (s *OrgIntegrationTestSuite) TestDeleteOrg() {
assert := assert.New(s.T())
ctx := context.Background()

s.T().Run("invalid org ID", func(t *testing.T) {
s.Run("invalid org ID", func() {
// Invalid org ID
err := s.Organization.Delete(ctx, "invalid")
assert.Error(err)
assert.True(biz.IsErrInvalidUUID(err))
})

s.T().Run("org non existent", func(t *testing.T) {
s.Run("org non existent", func() {
// org not found
err := s.Organization.Delete(ctx, uuid.NewString())
assert.Error(err)
assert.True(biz.IsNotFound(err))
})

s.T().Run("org, integrations and repositories deletion", func(t *testing.T) {
// Mock calls to credentials deletion for both the integration and the OCI repository
s.mockedCredsReaderWriter.On("DeleteCredentials", ctx, "stored-OCI-secret").Return(nil)
s.Run("org soft deletion and membership cleanup", func() {
// With soft-deletion, external credentials are NOT deleted, so no mock expectations

err := s.Organization.Delete(ctx, s.org.ID)
assert.NoError(err)

// Integrations and repo deleted as well
// Org is soft-deleted, so it can't be found
org, err := s.Organization.FindByID(ctx, s.org.ID)
assert.Nil(org)
assert.ErrorAs(err, &biz.ErrNotFound{})

// Memberships are deleted (hard-deleted)
memberships, _, err := s.Membership.ByOrg(ctx, s.org.ID, nil, nil)
assert.NoError(err)
assert.Empty(memberships)

// Related resources become inaccessible through org-scoped queries but remain in DB
integrations, err := s.Integration.List(ctx, s.org.ID)
assert.NoError(err)
assert.Empty(integrations)
Expand Down
2 changes: 1 addition & 1 deletion app/controlplane/pkg/data/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func toTimePtr(t time.Time) *time.Time {
}

func orgScopedQuery(client *ent.Client, orgID uuid.UUID) *ent.OrganizationQuery {
return client.Organization.Query().Where(organization.ID(orgID))
return client.Organization.Query().Where(organization.ID(orgID), organization.DeletedAtIsNil())
}

// WithTx initiates a transaction and wraps the DB function
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Modify "organizations" table
ALTER TABLE "organizations" ADD COLUMN "deleted_at" timestamptz NULL;
-- Create index "organization_name" to table: "organizations"
CREATE UNIQUE INDEX "organization_name" ON "organizations" ("name") WHERE (deleted_at IS NULL);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Drop index "organizations_name_key" from table: "organizations"
DROP INDEX "organizations_name_key";
4 changes: 3 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:QaClWT8b1Nr8BRNI+5O2WAYbWl/5tTlopLggA6RQwzw=
h1:RHLiR+aMRnWXumoeoZATujJDBqzz0vAwi1EJ/I7WPt4=
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 @@ -108,3 +108,5 @@ h1:QaClWT8b1Nr8BRNI+5O2WAYbWl/5tTlopLggA6RQwzw=
20250808164400.sql h1:r7S2LM8d3kbKQ7WNuggjvmNw3kcccx0rYzzklw8Q2I8=
20250808165202.sql h1:Oreh9FpYwo/cdcs3Oza/+ACzScXeTRBGIEvua8RqoLo=
20250812111458.sql h1:15yQlZoBymYR5GEjGLtV/j4ZZjg06u6eEzcRRl7vax4=
20250817154739.sql h1:myLtenqAYBq+qUmnB2loYATdXFRHMqTbPY3ZLhON4zE=
20250817161454.sql h1:Fw6tM1GqCevnUUl7Za/hbQZr6wISKmsJy7OfX11ZMQI=
13 changes: 12 additions & 1 deletion app/controlplane/pkg/data/ent/migrate/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,9 +413,10 @@ var (
// OrganizationsColumns holds the columns for the "organizations" table.
OrganizationsColumns = []*schema.Column{
{Name: "id", Type: field.TypeUUID, Unique: true},
{Name: "name", Type: field.TypeString, Unique: true},
{Name: "name", Type: field.TypeString},
{Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"},
{Name: "updated_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"},
{Name: "deleted_at", Type: field.TypeTime, Nullable: true},
{Name: "block_on_policy_violation", Type: field.TypeBool, Default: false},
{Name: "policies_allowed_hostnames", Type: field.TypeJSON, Nullable: true},
}
Expand All @@ -424,6 +425,16 @@ var (
Name: "organizations",
Columns: OrganizationsColumns,
PrimaryKey: []*schema.Column{OrganizationsColumns[0]},
Indexes: []*schema.Index{
{
Name: "organization_name",
Unique: true,
Columns: []*schema.Column{OrganizationsColumns[1]},
Annotation: &entsql.IndexAnnotation{
Where: "deleted_at IS NULL",
},
},
},
}
// ProjectsColumns holds the columns for the "projects" table.
ProjectsColumns = []*schema.Column{
Expand Down
75 changes: 74 additions & 1 deletion app/controlplane/pkg/data/ent/mutation.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion app/controlplane/pkg/data/ent/organization.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions app/controlplane/pkg/data/ent/organization/organization.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading