Files
zitadel/backend/v3/domain/org_delete.go
T
c4221a9c0f refactor: invoker implementations and clean up code (#10962)
This pull request refactors the command execution and transaction
handling logic within the `domain` package. The primary goal is to
simplify the invoker chain, clarify transaction management, and improve
the overall structure for better maintainability and readability.

### Key Changes:

- **Simplified Invoker Chain**: The previous chain of invokers has been
refactored. A new `transactionInvoker` is introduced to explicitly
manage database transactions for commands that require them. This
responsibility is no longer spread across different parts of the
invocation flow.
- **`Transactional` Interface**: A new `Transactional` interface has
been introduced. Commands that need to run within a database transaction
now implement this interface. This provides a clear and explicit
contract for transactional operations.
- **Consolidated Transaction Logic**: The logic for starting,
committing, and rolling back transactions is now centralized within the
`transactionInvoker` and the `eventStoreInvoker`. The `EnsureTx` method
on `InvokeOpts` has been removed, and transaction handling is now
managed automatically by the invocation framework.
- **Improved Event Handling**: The `eventStoreInvoker` and its
`eventCollector` have been rewritten to be more robust and to correctly
handle nested and batched commands, ensuring that all events are
collected and published within the same transaction.
- **Code Cleanup**: A significant amount of now-unused code, including
the old `invoker_test.go`, has been removed. Various comments and
variable declarations have also been cleaned up.
- **Updated Command Implementations**: All commands in the `domain`
package (e.g., `ActivateOrgCommand`, `DeactivateOrgCommand`,
`DeleteOrgCommand`, `UpdateOrgCommand`) have been updated to use the new
`Transactional` interface where appropriate, removing manual transaction
management.
- **Enhanced Testing**: Tests have been added and updated to cover the
new transaction and event handling logic, ensuring the refactoring is
sound.
- **Use `PrimaryKeyCondition`**: Use the
`repository.PrimaryKeyCondition(...)` method instead of using
`database.And(...)` where possible.

These changes lead to a cleaner, more decoupled architecture for command
handling in ZITADEL.

---------

Co-authored-by: Marco A. <[email protected]>
2025-10-27 14:04:06 +00:00

182 lines
5.1 KiB
Go

package domain
import (
"context"
"errors"
"strings"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/authz"
internal_domain "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/zerrors"
)
type DeleteOrgCommand struct {
OrganizationName string `json:"organization_name"`
Domains []*OrganizationDomain
ID string `json:"id"`
}
func NewDeleteOrgCommand(organizationID string) *DeleteOrgCommand {
return &DeleteOrgCommand{ID: organizationID}
}
// RequiresTransaction implements [Transactional].
func (cmd *DeleteOrgCommand) RequiresTransaction() {}
// Events implements [Commander].
//
// TODO(IAM-Marco): Finish implementation when policies, org settings, idp links and entities repositories
// are implemented
func (cmd *DeleteOrgCommand) Events(ctx context.Context, opts *InvokeOpts) ([]eventstore.Command, error) {
usernames := []string{}
// userRepo := opts.usersRepo(opts.DB())
// users, err := userRepo.List(ctx, database.WithCondition(opts.organizationRepo(opts.DB()).IDCondition(d.ID)))
// if err != nil {
// return nil, err
// }
// for _, u := range users {
// usernames = append(usernames, u.UserID)
// }
// domainPolicyRepo := opts.domainPolicyRepo(opts.DB())
// policy, err := domainPolicyRepo.Get(ctx, instanceID, d.ID)
// if err != nil {
// return nil, err
// }
// orgSettingsRepo := opts.organizationSettingsRepo(opts.DB())
// orgSettings, err := orgSettingsRepo.Get(ctx, d.ID)
// if err != nil {
// return nil, err
// }
// areUsernamesOrganizationScoped := policy.UserLoginMustBeDomain || orgSettings.UsernamesUnique
areUsernamesOrganizationScoped := false
domainNames := make([]string, len(cmd.Domains))
for i, domain := range cmd.Domains {
domainNames[i] = domain.Domain
}
externalIDPLinks := []*internal_domain.UserIDPLink{}
// idpLinksRepo := opts.idpLinksRepo(opts.DB())
// idpLinks, err := idpLinksRepo.List(ctx, d.ID)
// if err != nil {
// return nil, err
// }
// for _, link := range idpLinks {
// // Convert repo to internal_domain
// externalIDPLinks = append(externalIDPLinks, link)
// }
samlEntityIDs := []string{}
// entityIDsRepo := opts.entityIDsRepo(opts.DB())
// entityIDs, err := entityIDsRepo.List(ctx, d.ID)
// if err != nil {
// return nil, err
// }
// for _, ei := range entityIDs {
// samlEntityIDs = append(samlEntityIDs, ei.ID)
// }
return []eventstore.Command{
org.NewOrgRemovedEvent(
ctx,
&org.NewAggregate(cmd.ID).Aggregate,
cmd.OrganizationName,
usernames,
areUsernamesOrganizationScoped,
domainNames,
externalIDPLinks,
samlEntityIDs,
),
}, nil
}
// Execute implements [Commander].
func (cmd *DeleteOrgCommand) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
instance := authz.GetInstance(ctx)
orgRepo := opts.organizationRepo.LoadDomains()
orgToDelete, err := orgRepo.Get(ctx, opts.DB(), database.WithCondition(
orgRepo.PrimaryKeyCondition(instance.InstanceID(), cmd.ID),
))
if err != nil {
return err
}
cmd.OrganizationName = orgToDelete.Name
cmd.Domains = orgToDelete.Domains
deletedRows, err := orgRepo.Delete(ctx, opts.DB(),
orgRepo.PrimaryKeyCondition(authz.GetInstance(ctx).InstanceID(), cmd.ID),
)
if err != nil {
return err
}
if deletedRows > 1 {
err = zerrors.ThrowInternalf(nil, "DOM-5cE9u6", "expecting 1 row deleted, got %d", deletedRows)
return err
}
if deletedRows < 1 {
err = zerrors.ThrowNotFound(nil, "DOM-ur6Qyv", "organization not found")
}
return err
}
// String implements [Commander].
func (DeleteOrgCommand) String() string {
return "DeleteOrgCommand"
}
// Validate implements [Commander].
func (cmd *DeleteOrgCommand) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
instance := authz.GetInstance(ctx)
cmd.ID = strings.TrimSpace(cmd.ID)
if cmd.ID == instance.DefaultOrganisationID() {
return zerrors.ThrowPreconditionFailed(nil, "DOM-LCkE69", "Errors.Org.DefaultOrgNotDeletable")
}
// Check if the ZITADEL project exists on the input organization
projectRepo := opts.projectRepo
_, getErr := projectRepo.Get(ctx, opts.DB(),
database.WithCondition(database.And(
projectRepo.IDCondition(instance.ProjectID()),
projectRepo.OrganizationIDCondition(cmd.ID),
projectRepo.InstanceIDCondition(instance.InstanceID()),
)),
)
if getErr == nil {
return zerrors.ThrowPreconditionFailed(nil, "DOM-X7YXxC", "Errors.Org.ZitadelOrgNotDeletable")
}
// [database.NoRowFoundError] error means the project does not exist, return other errors in case it's not that
if !errors.Is(getErr, &database.NoRowFoundError{}) {
err = getErr
return err
}
orgRepo := opts.organizationRepo
_, errGetOrg := orgRepo.Get(ctx, opts.DB(),
database.WithCondition(
orgRepo.PrimaryKeyCondition(instance.InstanceID(), cmd.ID),
))
if errGetOrg != nil {
if errors.Is(errGetOrg, &database.NoRowFoundError{}) {
err = zerrors.ThrowNotFound(errGetOrg, "DOM-8KYOH3", "Errors.Org.NotFound")
}
return err
}
return err
}
var (
_ Commander = (*DeleteOrgCommand)(nil)
_ Transactional = (*DeleteOrgCommand)(nil)
)