mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-28 17:49:08 +02:00
Deleting a custom domain released its name while services still pointed at it, leaving them on a namespace the account no longer held. Deletion now refuses with 412 when a service in the same account uses the domain or a subdomain, including disabled ones. Service writes revalidate authorization inside their transaction and hold a shared lock on the matching registrations, so a delete racing a create cannot strand either. The dependency lookup is account-scoped: registrations are unique by name, so another account can hold team.example.com under example.com and its services are authorized by its own registration.
64 lines
2.6 KiB
Go
64 lines
2.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
|
|
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
|
"github.com/netbirdio/netbird/shared/management/status"
|
|
)
|
|
|
|
// GetExpiredCustomDomains lists pending registrations in stable batches across accounts.
|
|
func (s *SqlStore) GetExpiredCustomDomains(ctx context.Context, now time.Time, afterID domain.ID, limit int) ([]*domain.Domain, error) {
|
|
var domains []*domain.Domain
|
|
result := s.db.WithContext(ctx).
|
|
Where("validated = ? AND validation_expires_at <= ? AND id > ?", false, now, string(afterID)).
|
|
Order("id").Limit(limit).Find(&domains)
|
|
if result.Error != nil {
|
|
return nil, fmt.Errorf("list expired custom domains: %w", result.Error)
|
|
}
|
|
return domains, nil
|
|
}
|
|
|
|
// DeleteExpiredCustomDomain deletes an expired registration only if no service uses its namespace.
|
|
func (s *SqlStore) DeleteExpiredCustomDomain(ctx context.Context, d *domain.Domain, now time.Time) (bool, error) {
|
|
db := s.db.WithContext(ctx)
|
|
services := customDomainServices(db, d)
|
|
result := db.Where(accountAndIDQueryCondition, d.AccountID, d.ID).
|
|
Where("domain = ? AND validated = ? AND validation_expires_at <= ?", d.Domain, false, now).
|
|
Where("NOT EXISTS (?)", services.Select("1")).Delete(&domain.Domain{})
|
|
if result.Error != nil {
|
|
return false, fmt.Errorf("delete expired custom domain: %w", result.Error)
|
|
}
|
|
if result.RowsAffected > 0 {
|
|
return true, nil
|
|
}
|
|
var count int64
|
|
if err := customDomainServices(db, d).Count(&count).Error; err != nil {
|
|
return false, fmt.Errorf("check expired custom domain services: %w", err)
|
|
}
|
|
if count > 0 {
|
|
return false, status.Errorf(status.PreconditionFailed, "expired custom domain still has dependent services")
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func customDomainServices(db *gorm.DB, d *domain.Domain) *gorm.DB {
|
|
name := strings.ToLower(strings.TrimSuffix(d.Domain, "."))
|
|
// Shared domain validation permits underscores, and older rows may contain
|
|
// other LIKE metacharacters.
|
|
escaped := strings.NewReplacer("!", "!!", "%", "!%", "_", "!_").Replace(name)
|
|
// Registrations are unique by name, so another account can hold a subdomain
|
|
// of this one and serve from it. Its services derive their cluster from that
|
|
// account's own registration and are not dependents of this one.
|
|
return db.Model(&rpservice.Service{}).Where(accountIDCondition, d.AccountID).Where(
|
|
"LOWER(domain) IN ? OR LOWER(domain) LIKE ? ESCAPE '!' OR LOWER(domain) LIKE ? ESCAPE '!'",
|
|
[]string{name, name + "."}, "%."+escaped, "%."+escaped+".",
|
|
)
|
|
}
|