[management] Refuse services on unvalidated custom domains (#7341)

Require validated custom domains when creating or updating reverse proxy services. 
Propagate validation errors during updates and return HTTP 409 for duplicate domain claims.

Add regression tests for domain validation, ownership, and service creation and updates.
This commit is contained in:
Maycon Santos
2026-09-10 11:57:14 +02:00
committed by GitHub
parent 27991aab98
commit 21b4a83cea
9 changed files with 612 additions and 23 deletions
+29
View File
@@ -5686,6 +5686,23 @@ func (s *SqlStore) ListCustomDomains(ctx context.Context, accountID string) ([]*
return domains, nil
}
// GetCustomDomainByName returns the custom domain row holding the given name,
// regardless of which account owns it.
func (s *SqlStore) GetCustomDomainByName(ctx context.Context, domainName string) (*domain.Domain, error) {
customDomain := &domain.Domain{}
result := s.db.Take(customDomain, "domain = ?", domainName)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "custom domain %s not found", domainName)
}
log.WithContext(ctx).Errorf("failed to get custom domain by name from store: %v", result.Error)
return nil, status.Errorf(status.Internal, "failed to get custom domain from store")
}
return customDomain, nil
}
func (s *SqlStore) CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*domain.Domain, error) {
newDomain := &domain.Domain{
ID: xid.New().String(), // Generate our own ID because gorm doesn't always configure the database to handle this for us.
@@ -5697,6 +5714,18 @@ func (s *SqlStore) CreateCustomDomain(ctx context.Context, accountID string, dom
}
result := s.db.Create(newDomain)
if result.Error != nil {
// The unique index is the last guard when two requests clear the
// manager's availability check at the same time. The one that loses the
// insert is a conflict, not an internal failure.
var count int64
if err := s.db.Model(&domain.Domain{}).Where("domain = ?", domainName).Count(&count).Error; err == nil && count > 0 {
// The insert error is logged even on this path: the name being taken
// is what the caller has to act on, but if the insert also failed for
// an unrelated reason the operator still needs to see it.
log.WithContext(ctx).Warnf("create reverse proxy custom domain %s rejected, name already registered: %v", domainName, result.Error)
return nil, status.Errorf(status.AlreadyExists, "domain %s is already registered", domainName)
}
log.WithContext(ctx).Errorf("failed to create reverse proxy custom domain to store: %v", result.Error)
return nil, status.Errorf(status.Internal, "failed to create reverse proxy custom domain to store")
}
+1
View File
@@ -302,6 +302,7 @@ type Store interface {
GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error)
ListFreeDomains(ctx context.Context, accountID string) ([]string, error)
ListCustomDomains(ctx context.Context, accountID string) ([]*domain.Domain, error)
GetCustomDomainByName(ctx context.Context, domainName string) (*domain.Domain, error)
CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*domain.Domain, error)
UpdateCustomDomain(ctx context.Context, accountID string, d *domain.Domain) (*domain.Domain, error)
DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error
+15
View File
@@ -1941,6 +1941,21 @@ func (mr *MockStoreMockRecorder) GetCustomDomain(ctx, accountID, domainID any) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCustomDomain", reflect.TypeOf((*MockStore)(nil).GetCustomDomain), ctx, accountID, domainID)
}
// GetCustomDomainByName mocks base method.
func (m *MockStore) GetCustomDomainByName(ctx context.Context, domainName string) (*domain.Domain, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCustomDomainByName", ctx, domainName)
ret0, _ := ret[0].(*domain.Domain)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCustomDomainByName indicates an expected call of GetCustomDomainByName.
func (mr *MockStoreMockRecorder) GetCustomDomainByName(ctx, domainName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCustomDomainByName", reflect.TypeOf((*MockStore)(nil).GetCustomDomainByName), ctx, domainName)
}
// GetCustomDomainsCounts mocks base method.
func (m *MockStore) GetCustomDomainsCounts(ctx context.Context) (int64, int64, error) {
m.ctrl.T.Helper()