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.
81 lines
3.6 KiB
Go
81 lines
3.6 KiB
Go
package manager
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
|
"github.com/netbirdio/netbird/management/server/activity"
|
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
|
nbstore "github.com/netbirdio/netbird/management/server/store"
|
|
"github.com/netbirdio/netbird/shared/auth"
|
|
)
|
|
|
|
func TestDeleteDomain_ServiceDependencies(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
domainName string
|
|
serviceHost string
|
|
accountID string
|
|
enabled bool
|
|
protected bool
|
|
}{
|
|
{"exact", "example.com", "example.com", accountA, true, true},
|
|
{"subdomain", "example.com", "deep.app.example.com", accountA, true, true},
|
|
{"disabled", "example.com", "app.example.com", accountA, false, true},
|
|
// A service is authorized by its own account's registration, so another
|
|
// account's service under this namespace is not a dependency of it.
|
|
{"other account", "example.com", "app.example.com", accountB, true, false},
|
|
{"case and trailing dot", "example.com", "APP.EXAMPLE.COM.", accountA, true, true},
|
|
{"suffix boundary", "example.com", "notexample.com", accountA, true, false},
|
|
{"literal underscore", "a_b.example.com", "app.a_b.example.com", accountA, true, true},
|
|
{"underscore wildcard", "a_b.example.com", "app.axb.example.com", accountA, true, false},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
env := setupDomainTest(t)
|
|
events := captureDomainEvents(env)
|
|
d, err := env.store.CreateCustomDomain(ctx, accountA, tt.domainName, testCluster, true)
|
|
require.NoError(t, err)
|
|
svc := &rpservice.Service{
|
|
ID: "dependent", AccountID: tt.accountID, Domain: tt.serviceHost,
|
|
Enabled: tt.enabled, ProxyCluster: testCluster,
|
|
}
|
|
require.NoError(t, env.store.CreateService(ctx, svc))
|
|
router := mux.NewRouter()
|
|
RegisterEndpoints(router, env.manager)
|
|
deleteDomain := func() *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodDelete, "/domains/"+d.ID, nil)
|
|
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{AccountId: accountA, UserId: accountAUser})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, req)
|
|
return response
|
|
}
|
|
|
|
response := deleteDomain()
|
|
if tt.protected {
|
|
require.Equal(t, http.StatusPreconditionFailed, response.Code, "dependent services must block deletion: %s", response.Body.String())
|
|
assert.NotContains(t, response.Body.String(), tt.accountID, "the error must not reveal the service's account")
|
|
assert.NotNil(t, storedDomain(t, env.store, accountA, d.Domain), "the namespace must remain reserved")
|
|
assert.Empty(t, events.get(), "rejected deletion must not emit DomainDeleted")
|
|
stored, err := env.store.GetServiceByID(ctx, nbstore.LockingStrengthNone, tt.accountID, svc.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, svc.Enabled, stored.Enabled, "rejected deletion must preserve the service")
|
|
require.NoError(t, env.store.DeleteService(ctx, tt.accountID, svc.ID))
|
|
response = deleteDomain()
|
|
}
|
|
require.Equal(t, http.StatusNoContent, response.Code, "deletion must succeed without dependencies: %s", response.Body.String())
|
|
assert.Nil(t, storedDomain(t, env.store, accountA, d.Domain), "the registration must be deleted")
|
|
captured := events.get()
|
|
require.Len(t, captured, 1, "only successful deletion may emit an event")
|
|
assert.Equal(t, activity.DomainDeleted, captured[0].Activity, "the event must describe the successful deletion")
|
|
})
|
|
}
|
|
}
|