diff --git a/app/controlplane/Makefile b/app/controlplane/Makefile index e1d62136d..dd55aad8d 100644 --- a/app/controlplane/Makefile +++ b/app/controlplane/Makefile @@ -45,6 +45,12 @@ migration_sync: check-atlas-tool migration_hash migration_lint: check-atlas-tool migration_hash atlas migrate lint --dir ${local_migrations_dir} --dev-url "docker://postgres/15/test?search_path=public" --latest 1 --config file://atlas.hcl --env dev +.PHONY: migration_rebase +# rebase migrations +# example: make migration_rebase 20250327153948 +migration_rebase: check-atlas-tool + atlas migrate rebase --dir ${local_migrations_dir} $(filter-out $@,$(MAKECMDGOALS)) + .PHONY: migration_new # generate an empty migration file migration_new: check-atlas-tool migration_hash diff --git a/app/controlplane/pkg/biz/organization.go b/app/controlplane/pkg/biz/organization.go index 9890cfefd..12a4f9eda 100644 --- a/app/controlplane/pkg/biz/organization.go +++ b/app/controlplane/pkg/biz/organization.go @@ -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 { @@ -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) } diff --git a/app/controlplane/pkg/biz/organization_integration_test.go b/app/controlplane/pkg/biz/organization_integration_test.go index 5bfd0d17a..d782ab011 100644 --- a/app/controlplane/pkg/biz/organization_integration_test.go +++ b/app/controlplane/pkg/biz/organization_integration_test.go @@ -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) @@ -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) diff --git a/app/controlplane/pkg/data/data.go b/app/controlplane/pkg/data/data.go index 2d2069bdd..3c1cb560f 100644 --- a/app/controlplane/pkg/data/data.go +++ b/app/controlplane/pkg/data/data.go @@ -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 diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250827093032.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250827093032.sql new file mode 100644 index 000000000..927d4fd98 --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250827093032.sql @@ -0,0 +1,6 @@ +-- Modify "organizations" table +ALTER TABLE "organizations" ADD COLUMN "deleted_at" timestamptz NULL; +-- Drop index "organizations_name_key" from table: "organizations" +DROP INDEX "organizations_name_key"; +-- Create index "organization_name" to table: "organizations" +CREATE UNIQUE INDEX "organization_name" ON "organizations" ("name") WHERE (deleted_at IS NULL); diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index 03aeaba8c..390d98adb 100644 --- a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum +++ b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:4z4jm6qLVBYu2xAWiD/YxRpstDC0Boyza8y/i5cc1SE= +h1:juodzbTCyqY5JwQbyFWEtF5UeTSUEiafH2wny+s2E2c= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -110,3 +110,4 @@ h1:4z4jm6qLVBYu2xAWiD/YxRpstDC0Boyza8y/i5cc1SE= 20250812111458.sql h1:15yQlZoBymYR5GEjGLtV/j4ZZjg06u6eEzcRRl7vax4= 20250820090420.sql h1:xmJucXMVs+JyXWmyHu7Rv31hhgtAONDTv1mT/sTaJKk= 20250820171503.sql h1:SsLD5Tf6woeFE7/FLI9XVQpnEgx4CJ9d7fWwNOZvOrA= +20250827093032.sql h1:K+XDWewSLoGBM+zjkBMag3mMQFFQyoQ9SePzfRxC694= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index b4db1be31..121e3ec9e 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -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}, } @@ -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{ diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index 023822fd9..504143e54 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -8485,6 +8485,7 @@ type OrganizationMutation struct { name *string created_at *time.Time updated_at *time.Time + deleted_at *time.Time block_on_policy_violation *bool policies_allowed_hostnames *[]string appendpolicies_allowed_hostnames []string @@ -8730,6 +8731,55 @@ func (m *OrganizationMutation) ResetUpdatedAt() { m.updated_at = nil } +// SetDeletedAt sets the "deleted_at" field. +func (m *OrganizationMutation) SetDeletedAt(t time.Time) { + m.deleted_at = &t +} + +// DeletedAt returns the value of the "deleted_at" field in the mutation. +func (m *OrganizationMutation) DeletedAt() (r time.Time, exists bool) { + v := m.deleted_at + if v == nil { + return + } + return *v, true +} + +// OldDeletedAt returns the old "deleted_at" field's value of the Organization entity. +// If the Organization object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *OrganizationMutation) OldDeletedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDeletedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDeletedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDeletedAt: %w", err) + } + return oldValue.DeletedAt, nil +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (m *OrganizationMutation) ClearDeletedAt() { + m.deleted_at = nil + m.clearedFields[organization.FieldDeletedAt] = struct{}{} +} + +// DeletedAtCleared returns if the "deleted_at" field was cleared in this mutation. +func (m *OrganizationMutation) DeletedAtCleared() bool { + _, ok := m.clearedFields[organization.FieldDeletedAt] + return ok +} + +// ResetDeletedAt resets all changes to the "deleted_at" field. +func (m *OrganizationMutation) ResetDeletedAt() { + m.deleted_at = nil + delete(m.clearedFields, organization.FieldDeletedAt) +} + // SetBlockOnPolicyViolation sets the "block_on_policy_violation" field. func (m *OrganizationMutation) SetBlockOnPolicyViolation(b bool) { m.block_on_policy_violation = &b @@ -9297,7 +9347,7 @@ func (m *OrganizationMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *OrganizationMutation) Fields() []string { - fields := make([]string, 0, 5) + fields := make([]string, 0, 6) if m.name != nil { fields = append(fields, organization.FieldName) } @@ -9307,6 +9357,9 @@ func (m *OrganizationMutation) Fields() []string { if m.updated_at != nil { fields = append(fields, organization.FieldUpdatedAt) } + if m.deleted_at != nil { + fields = append(fields, organization.FieldDeletedAt) + } if m.block_on_policy_violation != nil { fields = append(fields, organization.FieldBlockOnPolicyViolation) } @@ -9327,6 +9380,8 @@ func (m *OrganizationMutation) Field(name string) (ent.Value, bool) { return m.CreatedAt() case organization.FieldUpdatedAt: return m.UpdatedAt() + case organization.FieldDeletedAt: + return m.DeletedAt() case organization.FieldBlockOnPolicyViolation: return m.BlockOnPolicyViolation() case organization.FieldPoliciesAllowedHostnames: @@ -9346,6 +9401,8 @@ func (m *OrganizationMutation) OldField(ctx context.Context, name string) (ent.V return m.OldCreatedAt(ctx) case organization.FieldUpdatedAt: return m.OldUpdatedAt(ctx) + case organization.FieldDeletedAt: + return m.OldDeletedAt(ctx) case organization.FieldBlockOnPolicyViolation: return m.OldBlockOnPolicyViolation(ctx) case organization.FieldPoliciesAllowedHostnames: @@ -9380,6 +9437,13 @@ func (m *OrganizationMutation) SetField(name string, value ent.Value) error { } m.SetUpdatedAt(v) return nil + case organization.FieldDeletedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDeletedAt(v) + return nil case organization.FieldBlockOnPolicyViolation: v, ok := value.(bool) if !ok { @@ -9424,6 +9488,9 @@ func (m *OrganizationMutation) AddField(name string, value ent.Value) error { // mutation. func (m *OrganizationMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(organization.FieldDeletedAt) { + fields = append(fields, organization.FieldDeletedAt) + } if m.FieldCleared(organization.FieldPoliciesAllowedHostnames) { fields = append(fields, organization.FieldPoliciesAllowedHostnames) } @@ -9441,6 +9508,9 @@ func (m *OrganizationMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *OrganizationMutation) ClearField(name string) error { switch name { + case organization.FieldDeletedAt: + m.ClearDeletedAt() + return nil case organization.FieldPoliciesAllowedHostnames: m.ClearPoliciesAllowedHostnames() return nil @@ -9461,6 +9531,9 @@ func (m *OrganizationMutation) ResetField(name string) error { case organization.FieldUpdatedAt: m.ResetUpdatedAt() return nil + case organization.FieldDeletedAt: + m.ResetDeletedAt() + return nil case organization.FieldBlockOnPolicyViolation: m.ResetBlockOnPolicyViolation() return nil diff --git a/app/controlplane/pkg/data/ent/organization.go b/app/controlplane/pkg/data/ent/organization.go index 7425e4372..23478a8b0 100644 --- a/app/controlplane/pkg/data/ent/organization.go +++ b/app/controlplane/pkg/data/ent/organization.go @@ -25,6 +25,8 @@ type Organization struct { CreatedAt time.Time `json:"created_at,omitempty"` // UpdatedAt holds the value of the "updated_at" field. UpdatedAt time.Time `json:"updated_at,omitempty"` + // DeletedAt holds the value of the "deleted_at" field. + DeletedAt time.Time `json:"deleted_at,omitempty"` // BlockOnPolicyViolation holds the value of the "block_on_policy_violation" field. BlockOnPolicyViolation bool `json:"block_on_policy_violation,omitempty"` // PoliciesAllowedHostnames holds the value of the "policies_allowed_hostnames" field. @@ -141,7 +143,7 @@ func (*Organization) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case organization.FieldName: values[i] = new(sql.NullString) - case organization.FieldCreatedAt, organization.FieldUpdatedAt: + case organization.FieldCreatedAt, organization.FieldUpdatedAt, organization.FieldDeletedAt: values[i] = new(sql.NullTime) case organization.FieldID: values[i] = new(uuid.UUID) @@ -184,6 +186,12 @@ func (o *Organization) assignValues(columns []string, values []any) error { } else if value.Valid { o.UpdatedAt = value.Time } + case organization.FieldDeletedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field deleted_at", values[i]) + } else if value.Valid { + o.DeletedAt = value.Time + } case organization.FieldBlockOnPolicyViolation: if value, ok := values[i].(*sql.NullBool); !ok { return fmt.Errorf("unexpected type %T for field block_on_policy_violation", values[i]) @@ -283,6 +291,9 @@ func (o *Organization) String() string { builder.WriteString("updated_at=") builder.WriteString(o.UpdatedAt.Format(time.ANSIC)) builder.WriteString(", ") + builder.WriteString("deleted_at=") + builder.WriteString(o.DeletedAt.Format(time.ANSIC)) + builder.WriteString(", ") builder.WriteString("block_on_policy_violation=") builder.WriteString(fmt.Sprintf("%v", o.BlockOnPolicyViolation)) builder.WriteString(", ") diff --git a/app/controlplane/pkg/data/ent/organization/organization.go b/app/controlplane/pkg/data/ent/organization/organization.go index 621b61ccd..1eb7b9a72 100644 --- a/app/controlplane/pkg/data/ent/organization/organization.go +++ b/app/controlplane/pkg/data/ent/organization/organization.go @@ -21,6 +21,8 @@ const ( FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. FieldUpdatedAt = "updated_at" + // FieldDeletedAt holds the string denoting the deleted_at field in the database. + FieldDeletedAt = "deleted_at" // FieldBlockOnPolicyViolation holds the string denoting the block_on_policy_violation field in the database. FieldBlockOnPolicyViolation = "block_on_policy_violation" // FieldPoliciesAllowedHostnames holds the string denoting the policies_allowed_hostnames field in the database. @@ -107,6 +109,7 @@ var Columns = []string{ FieldName, FieldCreatedAt, FieldUpdatedAt, + FieldDeletedAt, FieldBlockOnPolicyViolation, FieldPoliciesAllowedHostnames, } @@ -155,6 +158,11 @@ func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() } +// ByDeletedAt orders the results by the deleted_at field. +func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDeletedAt, opts...).ToFunc() +} + // ByBlockOnPolicyViolation orders the results by the block_on_policy_violation field. func ByBlockOnPolicyViolation(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldBlockOnPolicyViolation, opts...).ToFunc() diff --git a/app/controlplane/pkg/data/ent/organization/where.go b/app/controlplane/pkg/data/ent/organization/where.go index 61d77a4d7..71921b4a8 100644 --- a/app/controlplane/pkg/data/ent/organization/where.go +++ b/app/controlplane/pkg/data/ent/organization/where.go @@ -71,6 +71,11 @@ func UpdatedAt(v time.Time) predicate.Organization { return predicate.Organization(sql.FieldEQ(FieldUpdatedAt, v)) } +// DeletedAt applies equality check predicate on the "deleted_at" field. It's identical to DeletedAtEQ. +func DeletedAt(v time.Time) predicate.Organization { + return predicate.Organization(sql.FieldEQ(FieldDeletedAt, v)) +} + // BlockOnPolicyViolation applies equality check predicate on the "block_on_policy_violation" field. It's identical to BlockOnPolicyViolationEQ. func BlockOnPolicyViolation(v bool) predicate.Organization { return predicate.Organization(sql.FieldEQ(FieldBlockOnPolicyViolation, v)) @@ -221,6 +226,56 @@ func UpdatedAtLTE(v time.Time) predicate.Organization { return predicate.Organization(sql.FieldLTE(FieldUpdatedAt, v)) } +// DeletedAtEQ applies the EQ predicate on the "deleted_at" field. +func DeletedAtEQ(v time.Time) predicate.Organization { + return predicate.Organization(sql.FieldEQ(FieldDeletedAt, v)) +} + +// DeletedAtNEQ applies the NEQ predicate on the "deleted_at" field. +func DeletedAtNEQ(v time.Time) predicate.Organization { + return predicate.Organization(sql.FieldNEQ(FieldDeletedAt, v)) +} + +// DeletedAtIn applies the In predicate on the "deleted_at" field. +func DeletedAtIn(vs ...time.Time) predicate.Organization { + return predicate.Organization(sql.FieldIn(FieldDeletedAt, vs...)) +} + +// DeletedAtNotIn applies the NotIn predicate on the "deleted_at" field. +func DeletedAtNotIn(vs ...time.Time) predicate.Organization { + return predicate.Organization(sql.FieldNotIn(FieldDeletedAt, vs...)) +} + +// DeletedAtGT applies the GT predicate on the "deleted_at" field. +func DeletedAtGT(v time.Time) predicate.Organization { + return predicate.Organization(sql.FieldGT(FieldDeletedAt, v)) +} + +// DeletedAtGTE applies the GTE predicate on the "deleted_at" field. +func DeletedAtGTE(v time.Time) predicate.Organization { + return predicate.Organization(sql.FieldGTE(FieldDeletedAt, v)) +} + +// DeletedAtLT applies the LT predicate on the "deleted_at" field. +func DeletedAtLT(v time.Time) predicate.Organization { + return predicate.Organization(sql.FieldLT(FieldDeletedAt, v)) +} + +// DeletedAtLTE applies the LTE predicate on the "deleted_at" field. +func DeletedAtLTE(v time.Time) predicate.Organization { + return predicate.Organization(sql.FieldLTE(FieldDeletedAt, v)) +} + +// DeletedAtIsNil applies the IsNil predicate on the "deleted_at" field. +func DeletedAtIsNil() predicate.Organization { + return predicate.Organization(sql.FieldIsNull(FieldDeletedAt)) +} + +// DeletedAtNotNil applies the NotNil predicate on the "deleted_at" field. +func DeletedAtNotNil() predicate.Organization { + return predicate.Organization(sql.FieldNotNull(FieldDeletedAt)) +} + // BlockOnPolicyViolationEQ applies the EQ predicate on the "block_on_policy_violation" field. func BlockOnPolicyViolationEQ(v bool) predicate.Organization { return predicate.Organization(sql.FieldEQ(FieldBlockOnPolicyViolation, v)) diff --git a/app/controlplane/pkg/data/ent/organization_create.go b/app/controlplane/pkg/data/ent/organization_create.go index 0a680daab..b28cde42a 100644 --- a/app/controlplane/pkg/data/ent/organization_create.go +++ b/app/controlplane/pkg/data/ent/organization_create.go @@ -66,6 +66,20 @@ func (oc *OrganizationCreate) SetNillableUpdatedAt(t *time.Time) *OrganizationCr return oc } +// SetDeletedAt sets the "deleted_at" field. +func (oc *OrganizationCreate) SetDeletedAt(t time.Time) *OrganizationCreate { + oc.mutation.SetDeletedAt(t) + return oc +} + +// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. +func (oc *OrganizationCreate) SetNillableDeletedAt(t *time.Time) *OrganizationCreate { + if t != nil { + oc.SetDeletedAt(*t) + } + return oc +} + // SetBlockOnPolicyViolation sets the "block_on_policy_violation" field. func (oc *OrganizationCreate) SetBlockOnPolicyViolation(b bool) *OrganizationCreate { oc.mutation.SetBlockOnPolicyViolation(b) @@ -335,6 +349,10 @@ func (oc *OrganizationCreate) createSpec() (*Organization, *sqlgraph.CreateSpec) _spec.SetField(organization.FieldUpdatedAt, field.TypeTime, value) _node.UpdatedAt = value } + if value, ok := oc.mutation.DeletedAt(); ok { + _spec.SetField(organization.FieldDeletedAt, field.TypeTime, value) + _node.DeletedAt = value + } if value, ok := oc.mutation.BlockOnPolicyViolation(); ok { _spec.SetField(organization.FieldBlockOnPolicyViolation, field.TypeBool, value) _node.BlockOnPolicyViolation = value @@ -547,6 +565,24 @@ func (u *OrganizationUpsert) UpdateUpdatedAt() *OrganizationUpsert { return u } +// SetDeletedAt sets the "deleted_at" field. +func (u *OrganizationUpsert) SetDeletedAt(v time.Time) *OrganizationUpsert { + u.Set(organization.FieldDeletedAt, v) + return u +} + +// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. +func (u *OrganizationUpsert) UpdateDeletedAt() *OrganizationUpsert { + u.SetExcluded(organization.FieldDeletedAt) + return u +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (u *OrganizationUpsert) ClearDeletedAt() *OrganizationUpsert { + u.SetNull(organization.FieldDeletedAt) + return u +} + // SetBlockOnPolicyViolation sets the "block_on_policy_violation" field. func (u *OrganizationUpsert) SetBlockOnPolicyViolation(v bool) *OrganizationUpsert { u.Set(organization.FieldBlockOnPolicyViolation, v) @@ -656,6 +692,27 @@ func (u *OrganizationUpsertOne) UpdateUpdatedAt() *OrganizationUpsertOne { }) } +// SetDeletedAt sets the "deleted_at" field. +func (u *OrganizationUpsertOne) SetDeletedAt(v time.Time) *OrganizationUpsertOne { + return u.Update(func(s *OrganizationUpsert) { + s.SetDeletedAt(v) + }) +} + +// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. +func (u *OrganizationUpsertOne) UpdateDeletedAt() *OrganizationUpsertOne { + return u.Update(func(s *OrganizationUpsert) { + s.UpdateDeletedAt() + }) +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (u *OrganizationUpsertOne) ClearDeletedAt() *OrganizationUpsertOne { + return u.Update(func(s *OrganizationUpsert) { + s.ClearDeletedAt() + }) +} + // SetBlockOnPolicyViolation sets the "block_on_policy_violation" field. func (u *OrganizationUpsertOne) SetBlockOnPolicyViolation(v bool) *OrganizationUpsertOne { return u.Update(func(s *OrganizationUpsert) { @@ -937,6 +994,27 @@ func (u *OrganizationUpsertBulk) UpdateUpdatedAt() *OrganizationUpsertBulk { }) } +// SetDeletedAt sets the "deleted_at" field. +func (u *OrganizationUpsertBulk) SetDeletedAt(v time.Time) *OrganizationUpsertBulk { + return u.Update(func(s *OrganizationUpsert) { + s.SetDeletedAt(v) + }) +} + +// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. +func (u *OrganizationUpsertBulk) UpdateDeletedAt() *OrganizationUpsertBulk { + return u.Update(func(s *OrganizationUpsert) { + s.UpdateDeletedAt() + }) +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (u *OrganizationUpsertBulk) ClearDeletedAt() *OrganizationUpsertBulk { + return u.Update(func(s *OrganizationUpsert) { + s.ClearDeletedAt() + }) +} + // SetBlockOnPolicyViolation sets the "block_on_policy_violation" field. func (u *OrganizationUpsertBulk) SetBlockOnPolicyViolation(v bool) *OrganizationUpsertBulk { return u.Update(func(s *OrganizationUpsert) { diff --git a/app/controlplane/pkg/data/ent/organization_update.go b/app/controlplane/pkg/data/ent/organization_update.go index 6e2e5f2d7..656f20400 100644 --- a/app/controlplane/pkg/data/ent/organization_update.go +++ b/app/controlplane/pkg/data/ent/organization_update.go @@ -67,6 +67,26 @@ func (ou *OrganizationUpdate) SetNillableUpdatedAt(t *time.Time) *OrganizationUp return ou } +// SetDeletedAt sets the "deleted_at" field. +func (ou *OrganizationUpdate) SetDeletedAt(t time.Time) *OrganizationUpdate { + ou.mutation.SetDeletedAt(t) + return ou +} + +// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. +func (ou *OrganizationUpdate) SetNillableDeletedAt(t *time.Time) *OrganizationUpdate { + if t != nil { + ou.SetDeletedAt(*t) + } + return ou +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (ou *OrganizationUpdate) ClearDeletedAt() *OrganizationUpdate { + ou.mutation.ClearDeletedAt() + return ou +} + // SetBlockOnPolicyViolation sets the "block_on_policy_violation" field. func (ou *OrganizationUpdate) SetBlockOnPolicyViolation(b bool) *OrganizationUpdate { ou.mutation.SetBlockOnPolicyViolation(b) @@ -440,6 +460,12 @@ func (ou *OrganizationUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := ou.mutation.UpdatedAt(); ok { _spec.SetField(organization.FieldUpdatedAt, field.TypeTime, value) } + if value, ok := ou.mutation.DeletedAt(); ok { + _spec.SetField(organization.FieldDeletedAt, field.TypeTime, value) + } + if ou.mutation.DeletedAtCleared() { + _spec.ClearField(organization.FieldDeletedAt, field.TypeTime) + } if value, ok := ou.mutation.BlockOnPolicyViolation(); ok { _spec.SetField(organization.FieldBlockOnPolicyViolation, field.TypeBool, value) } @@ -864,6 +890,26 @@ func (ouo *OrganizationUpdateOne) SetNillableUpdatedAt(t *time.Time) *Organizati return ouo } +// SetDeletedAt sets the "deleted_at" field. +func (ouo *OrganizationUpdateOne) SetDeletedAt(t time.Time) *OrganizationUpdateOne { + ouo.mutation.SetDeletedAt(t) + return ouo +} + +// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. +func (ouo *OrganizationUpdateOne) SetNillableDeletedAt(t *time.Time) *OrganizationUpdateOne { + if t != nil { + ouo.SetDeletedAt(*t) + } + return ouo +} + +// ClearDeletedAt clears the value of the "deleted_at" field. +func (ouo *OrganizationUpdateOne) ClearDeletedAt() *OrganizationUpdateOne { + ouo.mutation.ClearDeletedAt() + return ouo +} + // SetBlockOnPolicyViolation sets the "block_on_policy_violation" field. func (ouo *OrganizationUpdateOne) SetBlockOnPolicyViolation(b bool) *OrganizationUpdateOne { ouo.mutation.SetBlockOnPolicyViolation(b) @@ -1267,6 +1313,12 @@ func (ouo *OrganizationUpdateOne) sqlSave(ctx context.Context) (_node *Organizat if value, ok := ouo.mutation.UpdatedAt(); ok { _spec.SetField(organization.FieldUpdatedAt, field.TypeTime, value) } + if value, ok := ouo.mutation.DeletedAt(); ok { + _spec.SetField(organization.FieldDeletedAt, field.TypeTime, value) + } + if ouo.mutation.DeletedAtCleared() { + _spec.ClearField(organization.FieldDeletedAt, field.TypeTime) + } if value, ok := ouo.mutation.BlockOnPolicyViolation(); ok { _spec.SetField(organization.FieldBlockOnPolicyViolation, field.TypeBool, value) } diff --git a/app/controlplane/pkg/data/ent/runtime.go b/app/controlplane/pkg/data/ent/runtime.go index 207ebd38a..6e849ab0d 100644 --- a/app/controlplane/pkg/data/ent/runtime.go +++ b/app/controlplane/pkg/data/ent/runtime.go @@ -196,7 +196,7 @@ func init() { // organization.DefaultUpdatedAt holds the default value on creation for the updated_at field. organization.DefaultUpdatedAt = organizationDescUpdatedAt.Default.(func() time.Time) // organizationDescBlockOnPolicyViolation is the schema descriptor for block_on_policy_violation field. - organizationDescBlockOnPolicyViolation := organizationFields[4].Descriptor() + organizationDescBlockOnPolicyViolation := organizationFields[5].Descriptor() // organization.DefaultBlockOnPolicyViolation holds the default value on creation for the block_on_policy_violation field. organization.DefaultBlockOnPolicyViolation = organizationDescBlockOnPolicyViolation.Default.(bool) // organizationDescID is the schema descriptor for id field. diff --git a/app/controlplane/pkg/data/ent/schema-viz.html b/app/controlplane/pkg/data/ent/schema-viz.html index 486cb3cc5..4f263d233 100644 --- a/app/controlplane/pkg/data/ent/schema-viz.html +++ b/app/controlplane/pkg/data/ent/schema-viz.html @@ -70,7 +70,7 @@ } - const entGraph = JSON.parse("{\"nodes\":[{\"id\":\"APIToken\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"expires_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"},{\"name\":\"last_used_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Attestation\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"bundle\",\"type\":\"[]byte\"},{\"name\":\"workflowrun_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"CASBackend\",\"fields\":[{\"name\":\"location\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"provider\",\"type\":\"biz.CASBackendProvider\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"validation_status\",\"type\":\"biz.CASBackendValidationStatus\"},{\"name\":\"validated_at\",\"type\":\"time.Time\"},{\"name\":\"default\",\"type\":\"bool\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"fallback\",\"type\":\"bool\"},{\"name\":\"max_blob_size_bytes\",\"type\":\"int64\"}]},{\"id\":\"CASMapping\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_run_id\",\"type\":\"uuid.UUID\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Group\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"member_count\",\"type\":\"int\"}]},{\"id\":\"GroupMembership\",\"fields\":[{\"name\":\"group_id\",\"type\":\"uuid.UUID\"},{\"name\":\"user_id\",\"type\":\"uuid.UUID\"},{\"name\":\"maintainer\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"Integration\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"IntegrationAttachment\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Membership\",\"fields\":[{\"name\":\"current\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"role\",\"type\":\"authz.Role\"},{\"name\":\"membership_type\",\"type\":\"authz.MembershipType\"},{\"name\":\"member_id\",\"type\":\"uuid.UUID\"},{\"name\":\"resource_type\",\"type\":\"authz.ResourceType\"},{\"name\":\"resource_id\",\"type\":\"uuid.UUID\"},{\"name\":\"parent_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"OrgInvitation\",\"fields\":[{\"name\":\"receiver_email\",\"type\":\"string\"},{\"name\":\"status\",\"type\":\"biz.OrgInvitationStatus\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"sender_id\",\"type\":\"uuid.UUID\"},{\"name\":\"role\",\"type\":\"authz.Role\"},{\"name\":\"context\",\"type\":\"biz.OrgInvitationContext\"}]},{\"id\":\"Organization\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"block_on_policy_violation\",\"type\":\"bool\"},{\"name\":\"policies_allowed_hostnames\",\"type\":\"[]string\"}]},{\"id\":\"Project\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"ProjectVersion\",\"fields\":[{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"prerelease\",\"type\":\"bool\"},{\"name\":\"workflow_run_count\",\"type\":\"int\"},{\"name\":\"released_at\",\"type\":\"time.Time\"},{\"name\":\"latest\",\"type\":\"bool\"}]},{\"id\":\"Referrer\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"downloadable\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"metadata\",\"type\":\"map[string]string\"},{\"name\":\"annotations\",\"type\":\"map[string]string\"}]},{\"id\":\"RobotAccount\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"}]},{\"id\":\"User\",\"fields\":[{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"has_restricted_access\",\"type\":\"bool\"},{\"name\":\"first_name\",\"type\":\"string\"},{\"name\":\"last_name\",\"type\":\"string\"}]},{\"id\":\"Workflow\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"project_old\",\"type\":\"string\"},{\"name\":\"team\",\"type\":\"string\"},{\"name\":\"runs_count\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"public\",\"type\":\"bool\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"latest_run\",\"type\":\"uuid.UUID\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"metadata\",\"type\":\"map[string]interface {}\"}]},{\"id\":\"WorkflowContract\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"scoped_resource_type\",\"type\":\"biz.ContractScope\"},{\"name\":\"scoped_resource_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"WorkflowContractVersion\",\"fields\":[{\"name\":\"body\",\"type\":\"[]byte\"},{\"name\":\"raw_body\",\"type\":\"[]byte\"},{\"name\":\"raw_body_format\",\"type\":\"unmarshal.RawFormat\"},{\"name\":\"revision\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowRun\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"finished_at\",\"type\":\"time.Time\"},{\"name\":\"state\",\"type\":\"biz.WorkflowRunStatus\"},{\"name\":\"reason\",\"type\":\"string\"},{\"name\":\"run_url\",\"type\":\"string\"},{\"name\":\"runner_type\",\"type\":\"string\"},{\"name\":\"attestation\",\"type\":\"*dsse.Envelope\"},{\"name\":\"attestation_digest\",\"type\":\"string\"},{\"name\":\"attestation_state\",\"type\":\"[]byte\"},{\"name\":\"contract_revision_used\",\"type\":\"int\"},{\"name\":\"contract_revision_latest\",\"type\":\"int\"},{\"name\":\"version_id\",\"type\":\"uuid.UUID\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]}],\"edges\":[{\"from\":\"APIToken\",\"to\":\"Project\",\"label\":\"project\"},{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"CASMapping\",\"to\":\"Project\",\"label\":\"project\"},{\"from\":\"GroupMembership\",\"to\":\"Group\",\"label\":\"group\"},{\"from\":\"GroupMembership\",\"to\":\"User\",\"label\":\"user\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Integration\",\"label\":\"integration\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Workflow\",\"label\":\"workflow\"},{\"from\":\"Membership\",\"to\":\"Membership\",\"label\":\"children\"},{\"from\":\"OrgInvitation\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"OrgInvitation\",\"to\":\"User\",\"label\":\"sender\"},{\"from\":\"Organization\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Organization\",\"to\":\"WorkflowContract\",\"label\":\"workflow_contracts\"},{\"from\":\"Organization\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Organization\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"Organization\",\"to\":\"Integration\",\"label\":\"integrations\"},{\"from\":\"Organization\",\"to\":\"APIToken\",\"label\":\"api_tokens\"},{\"from\":\"Organization\",\"to\":\"Project\",\"label\":\"projects\"},{\"from\":\"Organization\",\"to\":\"Group\",\"label\":\"groups\"},{\"from\":\"Project\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Project\",\"to\":\"ProjectVersion\",\"label\":\"versions\"},{\"from\":\"ProjectVersion\",\"to\":\"WorkflowRun\",\"label\":\"runs\"},{\"from\":\"Referrer\",\"to\":\"Referrer\",\"label\":\"references\"},{\"from\":\"Referrer\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"User\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Workflow\",\"to\":\"RobotAccount\",\"label\":\"robotaccounts\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"Workflow\",\"to\":\"WorkflowContract\",\"label\":\"contract\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"latest_workflow_run\"},{\"from\":\"WorkflowContract\",\"to\":\"WorkflowContractVersion\",\"label\":\"versions\"},{\"from\":\"WorkflowRun\",\"to\":\"WorkflowContractVersion\",\"label\":\"contract_version\"},{\"from\":\"WorkflowRun\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"WorkflowRun\",\"to\":\"Attestation\",\"label\":\"attestation_bundle\"}]}"); + const entGraph = JSON.parse("{\"nodes\":[{\"id\":\"APIToken\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"expires_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"},{\"name\":\"last_used_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Attestation\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"bundle\",\"type\":\"[]byte\"},{\"name\":\"workflowrun_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"CASBackend\",\"fields\":[{\"name\":\"location\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"provider\",\"type\":\"biz.CASBackendProvider\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"validation_status\",\"type\":\"biz.CASBackendValidationStatus\"},{\"name\":\"validated_at\",\"type\":\"time.Time\"},{\"name\":\"default\",\"type\":\"bool\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"fallback\",\"type\":\"bool\"},{\"name\":\"max_blob_size_bytes\",\"type\":\"int64\"}]},{\"id\":\"CASMapping\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_run_id\",\"type\":\"uuid.UUID\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Group\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"member_count\",\"type\":\"int\"}]},{\"id\":\"GroupMembership\",\"fields\":[{\"name\":\"group_id\",\"type\":\"uuid.UUID\"},{\"name\":\"user_id\",\"type\":\"uuid.UUID\"},{\"name\":\"maintainer\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"Integration\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"IntegrationAttachment\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Membership\",\"fields\":[{\"name\":\"current\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"role\",\"type\":\"authz.Role\"},{\"name\":\"membership_type\",\"type\":\"authz.MembershipType\"},{\"name\":\"member_id\",\"type\":\"uuid.UUID\"},{\"name\":\"resource_type\",\"type\":\"authz.ResourceType\"},{\"name\":\"resource_id\",\"type\":\"uuid.UUID\"},{\"name\":\"parent_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"OrgInvitation\",\"fields\":[{\"name\":\"receiver_email\",\"type\":\"string\"},{\"name\":\"status\",\"type\":\"biz.OrgInvitationStatus\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"sender_id\",\"type\":\"uuid.UUID\"},{\"name\":\"role\",\"type\":\"authz.Role\"},{\"name\":\"context\",\"type\":\"biz.OrgInvitationContext\"}]},{\"id\":\"Organization\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"block_on_policy_violation\",\"type\":\"bool\"},{\"name\":\"policies_allowed_hostnames\",\"type\":\"[]string\"}]},{\"id\":\"Project\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"ProjectVersion\",\"fields\":[{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"prerelease\",\"type\":\"bool\"},{\"name\":\"workflow_run_count\",\"type\":\"int\"},{\"name\":\"released_at\",\"type\":\"time.Time\"},{\"name\":\"latest\",\"type\":\"bool\"}]},{\"id\":\"Referrer\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"downloadable\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"metadata\",\"type\":\"map[string]string\"},{\"name\":\"annotations\",\"type\":\"map[string]string\"}]},{\"id\":\"RobotAccount\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"}]},{\"id\":\"User\",\"fields\":[{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"has_restricted_access\",\"type\":\"bool\"},{\"name\":\"first_name\",\"type\":\"string\"},{\"name\":\"last_name\",\"type\":\"string\"}]},{\"id\":\"Workflow\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"project_old\",\"type\":\"string\"},{\"name\":\"team\",\"type\":\"string\"},{\"name\":\"runs_count\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"public\",\"type\":\"bool\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"latest_run\",\"type\":\"uuid.UUID\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"metadata\",\"type\":\"map[string]interface {}\"}]},{\"id\":\"WorkflowContract\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"scoped_resource_type\",\"type\":\"biz.ContractScope\"},{\"name\":\"scoped_resource_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"WorkflowContractVersion\",\"fields\":[{\"name\":\"body\",\"type\":\"[]byte\"},{\"name\":\"raw_body\",\"type\":\"[]byte\"},{\"name\":\"raw_body_format\",\"type\":\"unmarshal.RawFormat\"},{\"name\":\"revision\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowRun\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"finished_at\",\"type\":\"time.Time\"},{\"name\":\"state\",\"type\":\"biz.WorkflowRunStatus\"},{\"name\":\"reason\",\"type\":\"string\"},{\"name\":\"run_url\",\"type\":\"string\"},{\"name\":\"runner_type\",\"type\":\"string\"},{\"name\":\"attestation\",\"type\":\"*dsse.Envelope\"},{\"name\":\"attestation_digest\",\"type\":\"string\"},{\"name\":\"attestation_state\",\"type\":\"[]byte\"},{\"name\":\"contract_revision_used\",\"type\":\"int\"},{\"name\":\"contract_revision_latest\",\"type\":\"int\"},{\"name\":\"version_id\",\"type\":\"uuid.UUID\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]}],\"edges\":[{\"from\":\"APIToken\",\"to\":\"Project\",\"label\":\"project\"},{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"CASMapping\",\"to\":\"Project\",\"label\":\"project\"},{\"from\":\"GroupMembership\",\"to\":\"Group\",\"label\":\"group\"},{\"from\":\"GroupMembership\",\"to\":\"User\",\"label\":\"user\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Integration\",\"label\":\"integration\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Workflow\",\"label\":\"workflow\"},{\"from\":\"Membership\",\"to\":\"Membership\",\"label\":\"children\"},{\"from\":\"OrgInvitation\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"OrgInvitation\",\"to\":\"User\",\"label\":\"sender\"},{\"from\":\"Organization\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Organization\",\"to\":\"WorkflowContract\",\"label\":\"workflow_contracts\"},{\"from\":\"Organization\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Organization\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"Organization\",\"to\":\"Integration\",\"label\":\"integrations\"},{\"from\":\"Organization\",\"to\":\"APIToken\",\"label\":\"api_tokens\"},{\"from\":\"Organization\",\"to\":\"Project\",\"label\":\"projects\"},{\"from\":\"Organization\",\"to\":\"Group\",\"label\":\"groups\"},{\"from\":\"Project\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Project\",\"to\":\"ProjectVersion\",\"label\":\"versions\"},{\"from\":\"ProjectVersion\",\"to\":\"WorkflowRun\",\"label\":\"runs\"},{\"from\":\"Referrer\",\"to\":\"Referrer\",\"label\":\"references\"},{\"from\":\"Referrer\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"User\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Workflow\",\"to\":\"RobotAccount\",\"label\":\"robotaccounts\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"Workflow\",\"to\":\"WorkflowContract\",\"label\":\"contract\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"latest_workflow_run\"},{\"from\":\"WorkflowContract\",\"to\":\"WorkflowContractVersion\",\"label\":\"versions\"},{\"from\":\"WorkflowRun\",\"to\":\"WorkflowContractVersion\",\"label\":\"contract_version\"},{\"from\":\"WorkflowRun\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"WorkflowRun\",\"to\":\"Attestation\",\"label\":\"attestation_bundle\"}]}"); const nodes = new vis.DataSet((entGraph.nodes || []).map(n => ({ id: n.id, diff --git a/app/controlplane/pkg/data/ent/schema/organization.go b/app/controlplane/pkg/data/ent/schema/organization.go index d3c82d103..0500588bc 100644 --- a/app/controlplane/pkg/data/ent/schema/organization.go +++ b/app/controlplane/pkg/data/ent/schema/organization.go @@ -22,6 +22,7 @@ import ( "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" "github.com/google/uuid" ) @@ -34,7 +35,7 @@ type Organization struct { func (Organization) Fields() []ent.Field { return []ent.Field{ field.UUID("id", uuid.UUID{}).Default(uuid.New).Unique(), - field.String("name").Unique(), + field.String("name"), field.Time("created_at"). Default(time.Now). Immutable(). @@ -46,6 +47,7 @@ func (Organization) Fields() []ent.Field { Annotations(&entsql.Annotation{ Default: "CURRENT_TIMESTAMP", }), + field.Time("deleted_at").Optional(), field.Bool("block_on_policy_violation").Default(false), // array of hostnames that are allowed to be used in the policies field.Strings("policies_allowed_hostnames").Optional(), @@ -66,3 +68,11 @@ func (Organization) Edges() []ent.Edge { edge.To("groups", Group.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), } } + +func (Organization) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("name").Unique().Annotations( + entsql.IndexWhere("deleted_at IS NULL"), + ), + } +} diff --git a/app/controlplane/pkg/data/membership.go b/app/controlplane/pkg/data/membership.go index 66f4a913f..db5f1c3ec 100644 --- a/app/controlplane/pkg/data/membership.go +++ b/app/controlplane/pkg/data/membership.go @@ -230,7 +230,7 @@ func (r *MembershipRepo) FindByOrgIDAndUserEmail(ctx context.Context, orgID uuid } func (r *MembershipRepo) FindByOrgNameAndUser(ctx context.Context, orgName string, userID uuid.UUID) (*biz.Membership, error) { - org, err := r.data.DB.Organization.Query().Where(organization.Name(orgName)).First(ctx) + org, err := r.data.DB.Organization.Query().Where(organization.Name(orgName), organization.DeletedAtIsNil()).First(ctx) if err != nil { if ent.IsNotFound(err) { return nil, biz.NewErrNotFound(fmt.Sprintf("organization %s not found", orgName)) diff --git a/app/controlplane/pkg/data/organization.go b/app/controlplane/pkg/data/organization.go index b60606bd2..15f5b4eea 100644 --- a/app/controlplane/pkg/data/organization.go +++ b/app/controlplane/pkg/data/organization.go @@ -55,7 +55,7 @@ func (r *OrganizationRepo) Create(ctx context.Context, name string) (*biz.Organi } func (r *OrganizationRepo) FindByID(ctx context.Context, id uuid.UUID) (*biz.Organization, error) { - org, err := r.data.DB.Organization.Get(ctx, id) + org, err := r.data.DB.Organization.Query().Where(organization.ID(id), organization.DeletedAtIsNil()).Only(ctx) if err != nil && !ent.IsNotFound(err) { return nil, err } else if org == nil { @@ -67,7 +67,7 @@ func (r *OrganizationRepo) FindByID(ctx context.Context, id uuid.UUID) (*biz.Org // FindByName finds an organization by name. func (r *OrganizationRepo) FindByName(ctx context.Context, name string) (*biz.Organization, error) { - org, err := r.data.DB.Organization.Query().Where(organization.NameEQ(name)).Only(ctx) + org, err := r.data.DB.Organization.Query().Where(organization.NameEQ(name), organization.DeletedAtIsNil()).Only(ctx) if err != nil && !ent.IsNotFound(err) { return nil, err } else if org == nil { @@ -79,6 +79,7 @@ func (r *OrganizationRepo) FindByName(ctx context.Context, name string) (*biz.Or func (r *OrganizationRepo) Update(ctx context.Context, id uuid.UUID, blockOnPolicyViolation *bool, policiesAllowedHostnames []string) (*biz.Organization, error) { opts := r.data.DB.Organization.UpdateOneID(id). + Where(organization.DeletedAtIsNil()). SetNillableBlockOnPolicyViolation(blockOnPolicyViolation). SetUpdatedAt(time.Now()) @@ -94,9 +95,13 @@ func (r *OrganizationRepo) Update(ctx context.Context, id uuid.UUID, blockOnPoli return r.FindByID(ctx, org.ID) } -// Delete deletes an organization by ID. +// Delete soft-deletes an organization by ID. func (r *OrganizationRepo) Delete(ctx context.Context, id uuid.UUID) error { - return r.data.DB.Organization.DeleteOneID(id).Exec(ctx) + return r.data.DB.Organization.UpdateOneID(id). + Where(organization.DeletedAtIsNil()). + SetDeletedAt(time.Now()). + SetUpdatedAt(time.Now()). + Exec(ctx) } func entOrgToBizOrg(eu *ent.Organization) *biz.Organization { diff --git a/app/controlplane/pkg/data/project.go b/app/controlplane/pkg/data/project.go index 2156df3e8..d8640f6bd 100644 --- a/app/controlplane/pkg/data/project.go +++ b/app/controlplane/pkg/data/project.go @@ -51,9 +51,7 @@ func NewProjectsRepo(data *Data, logger log.Logger) biz.ProjectsRepo { // FindProjectByOrgIDAndName gets a project by organization ID and project name func (r *ProjectRepo) FindProjectByOrgIDAndName(ctx context.Context, orgID uuid.UUID, projectName string) (*biz.Project, error) { - pro, err := r.data.DB.Organization.Query().Where( - organization.ID(orgID), - ).QueryProjects().Where( + pro, err := orgScopedQuery(r.data.DB, orgID).QueryProjects().Where( project.Name(projectName), project.DeletedAtIsNil(), ).Only(ctx) @@ -70,9 +68,7 @@ func (r *ProjectRepo) FindProjectByOrgIDAndName(ctx context.Context, orgID uuid. // FindProjectByOrgIDAndID gets a project by organization ID and project ID func (r *ProjectRepo) FindProjectByOrgIDAndID(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID) (*biz.Project, error) { - pro, err := r.data.DB.Organization.Query().Where( - organization.ID(orgID), - ).QueryProjects().Where( + pro, err := orgScopedQuery(r.data.DB, orgID).QueryProjects().Where( project.ID(projectID), project.DeletedAtIsNil(), ).Only(ctx) diff --git a/app/controlplane/pkg/data/robotaccount.go b/app/controlplane/pkg/data/robotaccount.go index a5034534e..e641116ad 100644 --- a/app/controlplane/pkg/data/robotaccount.go +++ b/app/controlplane/pkg/data/robotaccount.go @@ -21,7 +21,6 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/robotaccount" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" "github.com/go-kratos/kratos/v2/log" @@ -86,7 +85,7 @@ func (r *RobotAccountRepo) FindByID(ctx context.Context, id uuid.UUID) (*biz.Rob func (r *RobotAccountRepo) Revoke(ctx context.Context, orgID, id uuid.UUID) error { // Find a non-revoked robot account in the scope of the organization - acc, err := r.data.DB.Organization.Query().Where(organization.ID(orgID)). + acc, err := orgScopedQuery(r.data.DB, orgID). QueryWorkflows(). QueryRobotaccounts().Where(robotaccount.ID(id)).Where(robotaccount.RevokedAtIsNil()). First(ctx)