[management] Clean up after account deletion (#7812)

Deleting an account left state behind that DeleteAccount's store
associations don't reach. The Agent Network tables outlived the account,
keeping its gateway domain claimed and its provider API keys stored. The
proxies kept serving its gateway until they next resynced. Cloud-side
state, such as managed proxy deployments, had no way to be cleaned up at
all.

Account deletion now runs registered hooks after the permission check
and before any users or data are removed. A failing hook aborts the
deletion. Agent Network registers one that tells the proxies to drop the
account's gateway mappings. The account's settings, providers, policies,
guardrails and budget rules are deleted in the account's transaction.
Consumption counters, and the access logs of deleted accounts, are left
to the background cleanup; usage records are kept.
This commit is contained in:
Brad Ison
2026-09-30 18:25:38 +02:00
committed by GitHub
parent 6c453a0f97
commit fd1a0203c7
19 changed files with 1023 additions and 27 deletions
@@ -18,6 +18,7 @@ import (
"gorm.io/gorm/clause"
nbdns "github.com/netbirdio/netbird/dns"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/route"
@@ -162,6 +163,10 @@ func (s *SqlStore) DeleteAccount(ctx context.Context, account *types.Account) er
return result.Error
}
if err := deleteAgentNetworkAccountConfig(tx, account.Id); err != nil {
return err
}
result = tx.Select(clause.Associations).Delete(account)
if result.Error != nil {
return result.Error
@@ -179,6 +184,29 @@ func (s *SqlStore) DeleteAccount(ctx context.Context, account *types.Account) er
return err
}
// deleteAgentNetworkAccountConfig removes the account's agent network configuration. These
// tables are not account associations, so deleting the account does not reach them. The
// settings row holds the account's globally unique gateway domain and the provider rows
// hold its upstream API keys. Tables that grow with traffic are left out: consumption
// counters and access logs are swept in the background, and usage records are kept.
func deleteAgentNetworkAccountConfig(tx *gorm.DB, accountID string) error {
// Dependents first: policies point at providers and guardrails, and settings
// go last, as DeleteSettings refuses while providers exist.
models := []any{
&agentNetworkTypes.Policy{},
&agentNetworkTypes.Provider{},
&agentNetworkTypes.Guardrail{},
&agentNetworkTypes.AccountBudgetRule{},
&agentNetworkTypes.Settings{},
}
for _, model := range models {
if err := tx.Delete(model, "account_id = ?", accountID).Error; err != nil {
return fmt.Errorf("delete %T rows: %w", model, err)
}
}
return nil
}
func (s *SqlStore) UpdateAccountDomainAttributes(ctx context.Context, accountID string, domain string, category string, isPrimaryDomain bool) error {
accountCopy := types.Account{
Domain: domain,