Compare commits

..

1 Commits

Author SHA1 Message Date
mlsmaycon
792a6cd524 Check if login failed 2026-08-04 03:31:27 +02:00
19 changed files with 70 additions and 867 deletions

View File

@@ -16,17 +16,17 @@ func TestProfileAccountPathFor(t *testing.T) {
{
name: "default profile",
configPath: "/data/data/io.netbird.client/files/netbird.cfg",
want: filepath.FromSlash("/data/data/io.netbird.client/files/netbird.account.json"),
want: "/data/data/io.netbird.client/files/netbird.account.json",
},
{
name: "id profile",
configPath: "/data/data/io.netbird.client/files/profiles/4c5f5c8198c3989cffb5b5394f5a7ae0.json",
want: filepath.FromSlash("/data/data/io.netbird.client/files/profiles/4c5f5c8198c3989cffb5b5394f5a7ae0.account.json"),
want: "/data/data/io.netbird.client/files/profiles/4c5f5c8198c3989cffb5b5394f5a7ae0.account.json",
},
{
name: "legacy name-keyed profile is handled the same way",
configPath: "/data/data/io.netbird.client/files/profiles/work.json",
want: filepath.FromSlash("/data/data/io.netbird.client/files/profiles/work.account.json"),
want: "/data/data/io.netbird.client/files/profiles/work.account.json",
},
{
name: "empty path is rejected",

View File

@@ -8,6 +8,8 @@ import (
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto"
@@ -25,9 +27,9 @@ func TestLogin_ManagementUnreachableIsReturnedInsteadOfDemandingSSO(t *testing.T
unreachable := errors.New("create connection: dial context: context deadline exceeded")
attempts := 0
s.isLoginRequiredFn = func(context.Context) (bool, error) {
s.loginAttemptFn = func(context.Context, string, string) (internal.StatusType, error) {
attempts++
return false, unreachable
return internal.StatusLoginFailed, unreachable
}
resp, err := s.Login(userCtx(), &proto.LoginRequest{Username: &username})
@@ -53,12 +55,15 @@ func TestLogin_AuthRefusalStartsSSOFlow(t *testing.T) {
s.rootCtx = internal.CtxInitState(context.Background())
breakProfilePrivateKey(t, cfgPath)
s.isLoginRequiredFn = func(context.Context) (bool, error) {
return true, nil
refused := gstatus.Error(codes.PermissionDenied, "peer is not registered")
s.loginAttemptFn = func(context.Context, string, string) (internal.StatusType, error) {
return internal.StatusNeedsLogin, refused
}
_, err := s.Login(userCtx(), &proto.LoginRequest{Username: &username})
require.Error(t, err)
require.NotErrorIs(t, err, refused,
"the refusal was handed back to the caller instead of starting the SSO flow")
status, stateErr := internal.CtxGetState(s.rootCtx).Status()
require.NoError(t, stateErr)
@@ -66,32 +71,6 @@ func TestLogin_AuthRefusalStartsSSOFlow(t *testing.T) {
"the SSO flow setup was never reached with the broken key")
}
func TestLogin_SetupKeyStillRunsWhenPeerNeedsLogin(t *testing.T) {
s, _, _, username, _ := setupServerWithProfile(t)
s.rootCtx = internal.CtxInitState(context.Background())
s.isLoginRequiredFn = func(context.Context) (bool, error) {
return true, nil
}
var keysTried []string
s.loginAttemptFn = func(_ context.Context, setupKey, _ string) (internal.StatusType, error) {
keysTried = append(keysTried, setupKey)
return "", nil
}
setupKey := "A2C8E32F-AEB2-4B45-8FD3-8A0C1B2D3E4F"
resp, err := s.Login(userCtx(), &proto.LoginRequest{Username: &username, SetupKey: setupKey})
require.NoError(t, err, "the probe's outcome leaked out as the login result")
require.NotNil(t, resp)
require.Equal(t, []string{setupKey}, keysTried, "the setup key never reached the login attempt")
require.Nil(t, s.oauthAuthFlow.flow, "a setup-key login started an SSO flow")
status, err := internal.CtxGetState(s.rootCtx).Status()
require.NoError(t, err)
require.Equal(t, internal.StatusIdle, status)
}
// breakProfilePrivateKey replaces the profile's private key with an unparseable
// one, which makes any attempt to build a Management client fail on the spot.
func breakProfilePrivateKey(t *testing.T, cfgPath string) {

View File

@@ -140,8 +140,6 @@ type Server struct {
// it to drive the login outcomes that need a server on the other end;
// production leaves it nil, and every login goes through loginAttempt.
loginAttemptFn func(ctx context.Context, setupKey, jwtToken string) (internal.StatusType, error)
isLoginRequiredFn func(ctx context.Context) (bool, error)
}
type oauthAuthFlow struct {
@@ -386,21 +384,6 @@ func (s *Server) attemptLogin(ctx context.Context, setupKey, jwtToken string) (i
return s.loginAttempt(ctx, setupKey, jwtToken)
}
func (s *Server) isLoginRequired(ctx context.Context) (bool, error) {
if s.isLoginRequiredFn != nil {
return s.isLoginRequiredFn(ctx)
}
authClient, err := auth.NewAuth(ctx, s.config.PrivateKey, s.config.ManagementURL, s.config)
if err != nil {
log.Errorf("failed to create auth client: %v", err)
return false, err
}
defer authClient.Close()
return authClient.IsLoginRequired(ctx)
}
// loginAttempt attempts to login using the provided information. It returns
// StatusNeedsLogin when Management refused the peer's credentials and
// StatusLoginFailed for every other failure, so callers can tell an
@@ -657,22 +640,22 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
s.config = config
s.mutex.Unlock()
// A probe that errors leaves the login undecided: Management unreachable, a
loginStatus, err := s.attemptLogin(ctx, "", "")
if err == nil {
state.Set(internal.StatusIdle)
return &proto.LoginResponse{}, nil
}
// Only an authentication refusal means the peer has to (re-)authenticate.
// Any other failure leaves the login undecided: Management unreachable, a
// restart mid-request, an internal error. Those are returned for the caller
// to retry, because turning them into an SSO prompt asks the user to solve
// something that is not theirs to solve, and a browser login cannot succeed
// while Management is unreachable anyway. Only Management refusing the
// peer's key is a decision, and IsLoginRequired reports that as
// needsLogin=true rather than an error.
needsLogin, err := s.isLoginRequired(ctx)
if err != nil {
state.Set(internal.StatusLoginFailed)
// while Management is unreachable anyway.
if loginStatus != internal.StatusNeedsLogin && loginStatus != internal.StatusLoginFailed {
state.Set(loginStatus)
return nil, err
}
if !needsLogin {
state.Set(internal.StatusIdle)
return &proto.LoginResponse{}, nil
}
if msg.SetupKey == "" {
hint := ""
@@ -1815,9 +1798,6 @@ func (s *Server) RequestExtendAuthSession(
if connectClient == nil {
return nil, gstatus.Errorf(codes.FailedPrecondition, "client is not running")
}
if connectClient.Engine() == nil {
return nil, gstatus.Errorf(codes.FailedPrecondition, "session can no longer be extended, log in again to reconnect")
}
hint := ""
if msg.Hint != nil {

View File

@@ -11,7 +11,7 @@ import { DialogHeading } from "@/components/dialog/DialogHeading";
import { SquareIcon } from "@/components/SquareIcon";
import { Connection, Profiles as ProfilesSvc, Session, WindowManager } from "@bindings/services";
import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow";
import { EVENT_BROWSER_LOGIN_CANCEL, EVENT_TRIGGER_LOGIN } from "@/lib/connection";
import { EVENT_BROWSER_LOGIN_CANCEL } from "@/lib/connection";
import { errorDialog, formatErrorMessage } from "@/lib/errors.ts";
import { formatRemaining } from "@/lib/formatters";
@@ -131,21 +131,6 @@ export default function SessionExpirationDialog() {
}
}, [busy, t]);
const authenticate = useCallback(async () => {
if (busy) return;
setBusy(true);
try {
await Events.Emit(EVENT_TRIGGER_LOGIN);
await WindowManager.CloseSessionExpiration();
} catch (e) {
setBusy(false);
await errorDialog({
Title: t("connect.error.loginTitle"),
Message: formatErrorMessage(e),
});
}
}, [busy, t]);
const logout = useCallback(async () => {
if (busy) return;
setBusy(true);
@@ -200,7 +185,7 @@ export default function SessionExpirationDialog() {
variant={"primary"}
size={"md"}
className={"w-full"}
onClick={expired ? authenticate : stay}
onClick={stay}
disabled={busy}
>
{expired ? t("sessionExpiration.authenticate") : t("sessionExpiration.stay")}

View File

@@ -4,26 +4,17 @@ package main
// bindTrayClick wires the tray icon's left-click handler on Linux.
//
// Expected behaviour per tray host:
//
// Host Left click Right click
// KDE Plasma, Waybar main window (Activate) menu (host-rendered)
// GNOME Shell + AppIndicator menu only menu only
// Minimal WMs via XEmbed host main window (Activate) XEmbed GTK popup
//
// OnClick fires only on org.kde.StatusNotifierItem.Activate — a real left
// click. KDE/Waybar send it over D-Bus; the in-process XEmbed host
// (xembed_host_linux.go) maps a Button1 press to the same Activate call.
//
// GNOME Shell + AppIndicator never sends Activate: it renders the dbusmenu
// on ANY click and only reports the menu opening via dbusmenu
// Event("opened"). Upstream Wails treated that event as a click, so on GNOME
// both buttons raised the main window on top of the menu, and on KDE/Waybar
// a right click raised it over the freshly opened menu. The netbirdio/wails
// fork (go.mod replace) drops that heuristic: a menu open never fires
// OnClick. On GNOME the main window is reached via the "Open NetBird" menu
// entry; left-click-opens-window is not achievable there anyway, since the
// host always opens the menu itself.
// Both Linux click paths converge on Wails' linuxSystemTray.Activate, which
// fires the registered clickHandler:
// - Real SNI hosts (KDE Plasma, Waybar, GNOME Shell + AppIndicator) invoke
// org.kde.StatusNotifierItem.Activate over D-Bus on left-click.
// - The in-process StatusNotifierWatcher + XEmbed host used on minimal WMs
// (Fluxbox, i3, dwm, OpenBox) maps a Button1 press to that same Activate
// call itself (xembed_host_linux.go), so it routes through the same hook.
// Registering OnClick here therefore covers both paths with one handler — no
// changes to the watcher or XEmbed C code are needed. Left-click now opens the
// main window; right-click still opens the menu via Wails' default
// SecondaryActivate→OpenMenu handler (and the XEmbed GTK popup on minimal WMs).
//
// We do NOT register OnDoubleClick: Wails' Linux SNI backend never fires it
// (unlike Windows). And we deliberately skip AttachWindow — it plus Wails3's

View File

@@ -27,10 +27,11 @@ const (
finalWarningCountdownSeconds = 120
)
// handleSessionExpired notifies and brings the window forward so the user can reconnect.
// handleSessionExpired notifies and brings the window forward so the frontend's /login route drives renewal.
func (t *Tray) handleSessionExpired() {
t.notify(t.loc.T("notify.sessionExpired.title"), t.loc.T("notify.sessionExpired.body"), notifyIDSessionExpired)
if t.window != nil {
t.window.SetURL("/#/login")
t.window.Show()
t.window.Focus()
}
@@ -307,7 +308,11 @@ func (t *Tray) openSessionExtendFlow() {
}
seconds := int(time.Until(deadline).Seconds())
if seconds <= 0 {
t.app.Event.Emit(services.EventTriggerLogin)
if t.window != nil {
t.window.SetURL("/#/login")
t.window.Show()
t.window.Focus()
}
return
}
if t.svc.WindowManager == nil {

2
go.mod
View File

@@ -339,5 +339,3 @@ replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.1-0.20260716205
replace github.com/dexidp/dex/api/v2 => github.com/netbirdio/dex/api/v2 v2.0.0-20260512110716-8d70ad8647c1
replace github.com/mailru/easyjson => github.com/netbirdio/easyjson v0.9.0
replace github.com/wailsapp/wails/v3 => github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260803205919-ad21e92381f4

4
go.sum
View File

@@ -490,8 +490,6 @@ github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9ax
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ=
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260803205919-ad21e92381f4 h1:UKztc3QjWvzU5DZk+uYaOWN0x62NSe/pkxuPvzqZIy4=
github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260803205919-ad21e92381f4/go.mod h1:BzATbK71VFikMMMCo434wAi0QcaI03P+xeaWgDvQvjw=
github.com/netbirdio/wireguard-go v0.0.0-20260628102922-2834bebf6c1a h1:3CWK+yTvRKOcC0Q8VCTGy4l60TEb27CQVS7LkMxwjmw=
github.com/netbirdio/wireguard-go v0.0.0-20260628102922-2834bebf6c1a/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -662,6 +660,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wailsapp/wails/v3 v3.0.0-beta.3 h1:BrcZunEBVucncRx+xgkk9TzlXU4qc0ygJuEhKAAGaeA=
github.com/wailsapp/wails/v3 v3.0.0-beta.3/go.mod h1:BzATbK71VFikMMMCo434wAi0QcaI03P+xeaWgDvQvjw=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=

View File

@@ -66,8 +66,8 @@ func TestExtractClusterFromFreeDomain(t *testing.T) {
func TestExtractClusterFromCustomDomains(t *testing.T) {
customDomains := []*domain.Domain{
{Domain: "example.com", TargetCluster: "eu1.proxy.netbird.io", Validated: true},
{Domain: "proxy.corp.io", TargetCluster: "us1.proxy.netbird.io", Validated: true},
{Domain: "example.com", TargetCluster: "eu1.proxy.netbird.io"},
{Domain: "proxy.corp.io", TargetCluster: "us1.proxy.netbird.io"},
}
tests := []struct {
@@ -120,49 +120,19 @@ func TestExtractClusterFromCustomDomains(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cluster, match := extractClusterFromCustomDomains(tc.domain, customDomains)
if !tc.wantOK {
assert.Equal(t, customDomainNoMatch, match, "unrelated domain should not match any custom domain")
return
cluster, ok := extractClusterFromCustomDomains(tc.domain, customDomains)
assert.Equal(t, tc.wantOK, ok)
if ok {
assert.Equal(t, tc.wantVal, cluster)
}
assert.Equal(t, customDomainValidated, match, "validated custom domain should resolve a cluster")
assert.Equal(t, tc.wantVal, cluster)
})
}
}
// An unvalidated row must never yield a cluster: the account has not shown it
// controls the name, so no service may be bound to it.
func TestExtractClusterFromCustomDomains_UnvalidatedDomainRefused(t *testing.T) {
customDomains := []*domain.Domain{
{Domain: "example.com", TargetCluster: "eu1.proxy.netbird.io", Validated: false},
}
for _, serviceDomain := range []string{"example.com", "app.example.com"} {
t.Run(serviceDomain, func(t *testing.T) {
cluster, match := extractClusterFromCustomDomains(serviceDomain, customDomains)
assert.Equal(t, customDomainUnvalidated, match, "unvalidated row must be reported as such")
assert.Empty(t, cluster, "unvalidated row must not resolve a cluster")
})
}
}
// A more specific unvalidated row must not shadow a validated parent domain.
func TestExtractClusterFromCustomDomains_ValidatedParentWinsOverUnvalidatedChild(t *testing.T) {
customDomains := []*domain.Domain{
{Domain: "example.com", TargetCluster: "cluster-generic", Validated: true},
{Domain: "app.example.com", TargetCluster: "cluster-app", Validated: false},
}
cluster, match := extractClusterFromCustomDomains("app.example.com", customDomains)
assert.Equal(t, customDomainValidated, match)
assert.Equal(t, "cluster-generic", cluster, "validated parent domain should provide the cluster")
}
func TestExtractClusterFromCustomDomains_OverlappingDomains(t *testing.T) {
customDomains := []*domain.Domain{
{Domain: "example.com", TargetCluster: "cluster-generic", Validated: true},
{Domain: "app.example.com", TargetCluster: "cluster-app", Validated: true},
{Domain: "example.com", TargetCluster: "cluster-generic"},
{Domain: "app.example.com", TargetCluster: "cluster-app"},
}
tests := []struct {
@@ -194,8 +164,8 @@ func TestExtractClusterFromCustomDomains_OverlappingDomains(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cluster, match := extractClusterFromCustomDomains(tc.domain, customDomains)
assert.Equal(t, customDomainValidated, match)
cluster, ok := extractClusterFromCustomDomains(tc.domain, customDomains)
assert.True(t, ok)
assert.Equal(t, tc.wantVal, cluster)
})
}

View File

@@ -22,7 +22,6 @@ type store interface {
GetAccount(ctx context.Context, accountID string) (*types.Account, error)
GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error)
GetCustomDomainByName(ctx context.Context, domainName string) (*domain.Domain, error)
ListFreeDomains(ctx context.Context, accountID string) ([]string, error)
ListCustomDomains(ctx context.Context, accountID string) ([]*domain.Domain, error)
CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*domain.Domain, error)
@@ -147,10 +146,6 @@ func (m Manager) CreateDomain(ctx context.Context, accountID, userID, domainName
return nil, fmt.Errorf("target cluster %s is not available", targetCluster)
}
if err := m.checkDomainAvailable(ctx, domainName); err != nil {
return nil, err
}
// Attempt an initial validation against the specified cluster only
var validated bool
if m.validator.IsValid(ctx, domainName, []string{targetCluster}) {
@@ -167,23 +162,6 @@ func (m Manager) CreateDomain(ctx context.Context, accountID, userID, domainName
return d, nil
}
// checkDomainAvailable reports whether the domain is free to claim. The unique
// index on the column is the real guard; this turns the violation into a
// conflict the caller can act on instead of a database error, and says nothing
// about which account holds the domain.
func (m Manager) checkDomainAvailable(ctx context.Context, domainName string) error {
_, err := m.store.GetCustomDomainByName(ctx, domainName)
if err == nil {
return status.Errorf(status.AlreadyExists, "domain %s is already registered", domainName)
}
if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound {
return nil
}
return fmt.Errorf("look up domain: %w", err)
}
func (m Manager) DeleteDomain(ctx context.Context, accountID, userID, domainID string) error {
ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
if err != nil {
@@ -316,12 +294,9 @@ func (m Manager) DeriveClusterFromDomain(ctx context.Context, accountID, domain
return "", fmt.Errorf("list custom domains: %w", err)
}
targetCluster, match := extractClusterFromCustomDomains(domain, customDomains)
switch match {
case customDomainValidated:
targetCluster, valid := extractClusterFromCustomDomains(domain, customDomains)
if valid {
return targetCluster, nil
case customDomainUnvalidated:
return "", status.Errorf(status.PreconditionFailed, "domain %s is not validated", domain)
}
return "", fmt.Errorf("domain %s does not match any available proxy cluster", domain)
@@ -355,46 +330,19 @@ func (m Manager) getClusterAllowList(ctx context.Context, accountID string) ([]s
return merged, nil
}
// customDomainMatch describes how a service domain relates to the account's
// custom domain rows.
type customDomainMatch int
const (
customDomainNoMatch customDomainMatch = iota
customDomainUnvalidated
customDomainValidated
)
// extractClusterFromCustomDomains finds the longest custom domain covering the
// service domain and reports its target cluster. Only a validated row yields a
// cluster: until the CNAME check has passed the account has not shown it
// controls the name, so no traffic may be routed for it.
func extractClusterFromCustomDomains(serviceDomain string, customDomains []*domain.Domain) (string, customDomainMatch) {
func extractClusterFromCustomDomains(serviceDomain string, customDomains []*domain.Domain) (string, bool) {
bestCluster := ""
bestLen := -1
matched := false
for _, cd := range customDomains {
if serviceDomain != cd.Domain && !strings.HasSuffix(serviceDomain, "."+cd.Domain) {
continue
}
matched = true
if !cd.Validated {
continue
}
if l := len(cd.Domain); l > bestLen {
bestLen = l
bestCluster = cd.TargetCluster
}
}
switch {
case bestLen >= 0:
return bestCluster, customDomainValidated
case matched:
return "", customDomainUnvalidated
default:
return "", customDomainNoMatch
}
return bestCluster, bestLen >= 0
}
// ExtractClusterFromFreeDomain extracts the cluster address from a free domain.

View File

@@ -1,249 +0,0 @@
package manager
import (
"context"
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/metric/noop"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
proxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy/manager"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/permissions"
nbstore "github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/status"
)
const (
testCluster = "eu.proxy.test"
accountA = "account-a"
accountAUser = "account-a-admin"
accountB = "account-b"
accountBUser = "account-b-admin"
)
// stubResolver answers CNAME lookups from a table the test controls, so a
// domain can point at the cluster or nowhere without touching a real resolver.
type stubResolver struct {
mu sync.Mutex
cnames map[string]string
}
func (r *stubResolver) LookupCNAME(_ context.Context, host string) (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
cname, ok := r.cnames[host]
if !ok {
return "", fmt.Errorf("lookup %s: no such host", host)
}
return cname + ".", nil
}
func (r *stubResolver) set(host, cname string) {
r.mu.Lock()
defer r.mu.Unlock()
r.cnames[host] = cname
}
type domainTestEnv struct {
manager Manager
store nbstore.Store
resolver *stubResolver
}
// setupDomainTest builds the domain manager on a real SQLite store with two
// accounts and one active public proxy cluster.
func setupDomainTest(t *testing.T) *domainTestEnv {
t.Helper()
ctx := context.Background()
testStore, cleanup, err := nbstore.NewTestStoreFromSQL(ctx, "", t.TempDir())
require.NoError(t, err)
t.Cleanup(cleanup)
for accountID, userID := range map[string]string{accountA: accountAUser, accountB: accountBUser} {
require.NoError(t, testStore.SaveAccount(ctx, &types.Account{
Id: accountID,
CreatedBy: userID,
Settings: &types.Settings{},
Users: map[string]*types.User{
userID: {
Id: userID,
AccountID: accountID,
Role: types.UserRoleAdmin,
},
},
}))
}
proxyMgr, err := proxymanager.NewManager(testStore, noop.NewMeterProvider().Meter(""))
require.NoError(t, err)
_, err = proxyMgr.Connect(ctx, "proxy-1", "session-1", testCluster, "127.0.0.1", nil, nil)
require.NoError(t, err)
resolver := &stubResolver{cnames: make(map[string]string)}
mgr := Manager{
store: testStore,
proxyManager: proxyMgr,
validator: domain.Validator{Resolver: resolver},
permissionsManager: permissions.NewManager(testStore),
accountManager: &mock_server.MockAccountManager{
StoreEventFunc: func(context.Context, string, string, string, activity.ActivityDescriber, map[string]any) {},
},
}
return &domainTestEnv{manager: mgr, store: testStore, resolver: resolver}
}
// storedDomain reads a domain row back through the store so assertions are made
// on what was persisted rather than on the value the manager returned.
func storedDomain(t *testing.T, s nbstore.Store, accountID, domainName string) *domain.Domain {
t.Helper()
domains, err := s.ListCustomDomains(context.Background(), accountID)
require.NoError(t, err)
for _, d := range domains {
if d.Domain == domainName {
return d
}
}
return nil
}
// A domain whose CNAME check fails is stored unvalidated and must not resolve a
// cluster, which is what service creation gates on.
func TestCreateDomain_FailedLookupIsNotServable(t *testing.T) {
ctx := context.Background()
env := setupDomainTest(t)
created, err := env.manager.CreateDomain(ctx, accountA, accountAUser, "apps.example.com", testCluster)
require.NoError(t, err)
assert.False(t, created.Validated, "a domain whose CNAME lookup fails must not be created validated")
stored := storedDomain(t, env.store, accountA, "apps.example.com")
require.NotNil(t, stored, "domain row should exist")
assert.False(t, stored.Validated, "persisted row must be unvalidated")
cluster, err := env.manager.DeriveClusterFromDomain(ctx, accountA, "apps.example.com")
require.Error(t, err, "an unvalidated domain must not resolve a cluster")
assert.Empty(t, cluster)
assert.Contains(t, err.Error(), "not validated", "error should tell the caller what to fix")
sErr, ok := status.FromError(err)
require.True(t, ok, "error should be a typed status error")
assert.Equal(t, status.PreconditionFailed, sErr.Type())
_, err = env.manager.DeriveClusterFromDomain(ctx, accountA, "sub.apps.example.com")
assert.Error(t, err, "subdomains of an unvalidated custom domain are not servable either")
}
// A second account claiming a registered domain gets a clean conflict, not a
// database error surfaced as a 500.
func TestCreateDomain_DuplicateIsAConflict(t *testing.T) {
ctx := context.Background()
env := setupDomainTest(t)
_, err := env.manager.CreateDomain(ctx, accountA, accountAUser, "shared.example.com", testCluster)
require.NoError(t, err)
_, err = env.manager.CreateDomain(ctx, accountB, accountBUser, "shared.example.com", testCluster)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok, "conflict must be a typed status error, not a raw database error")
assert.Equal(t, status.AlreadyExists, sErr.Type(), "conflict should map to 409, not 500")
assert.NotContains(t, sErr.Message, accountA, "the response must not reveal the holding account")
assert.Nil(t, storedDomain(t, env.store, accountB, "shared.example.com"), "no row should be written on conflict")
}
// The same account re-adding one of its own domains is a conflict too.
func TestCreateDomain_SameAccountDuplicateIsAConflict(t *testing.T) {
ctx := context.Background()
env := setupDomainTest(t)
_, err := env.manager.CreateDomain(ctx, accountA, accountAUser, "dup.example.com", testCluster)
require.NoError(t, err)
_, err = env.manager.CreateDomain(ctx, accountA, accountAUser, "dup.example.com", testCluster)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.AlreadyExists, sErr.Type())
}
// The negative control: a validated domain still derives its cluster, for the
// bare name and for subdomains, exactly as before.
func TestCreateDomain_ValidatedDomainDerivesCluster(t *testing.T) {
ctx := context.Background()
env := setupDomainTest(t)
env.resolver.set("validation.valid.example.com", testCluster)
created, err := env.manager.CreateDomain(ctx, accountA, accountAUser, "valid.example.com", testCluster)
require.NoError(t, err)
require.True(t, created.Validated, "a matching CNAME should validate on create")
cluster, err := env.manager.DeriveClusterFromDomain(ctx, accountA, "valid.example.com")
require.NoError(t, err)
assert.Equal(t, testCluster, cluster)
cluster, err = env.manager.DeriveClusterFromDomain(ctx, accountA, "app.valid.example.com")
require.NoError(t, err)
assert.Equal(t, testCluster, cluster, "subdomains of a validated custom domain resolve too")
}
// Validating a domain flips the gate: the same lookup that failed before now
// resolves a cluster.
func TestValidateDomain_UnlocksClusterDerivation(t *testing.T) {
ctx := context.Background()
env := setupDomainTest(t)
created, err := env.manager.CreateDomain(ctx, accountA, accountAUser, "later.example.com", testCluster)
require.NoError(t, err)
require.False(t, created.Validated)
_, err = env.manager.DeriveClusterFromDomain(ctx, accountA, "later.example.com")
require.Error(t, err)
env.resolver.set("validation.later.example.com", testCluster)
env.manager.ValidateDomain(ctx, accountA, accountAUser, created.ID)
require.True(t, storedDomain(t, env.store, accountA, "later.example.com").Validated)
cluster, err := env.manager.DeriveClusterFromDomain(ctx, accountA, "later.example.com")
require.NoError(t, err)
assert.Equal(t, testCluster, cluster)
}
// Free cluster domains are unaffected by the custom domain gate.
func TestDeriveClusterFromDomain_FreeDomainUnaffected(t *testing.T) {
ctx := context.Background()
env := setupDomainTest(t)
cluster, err := env.manager.DeriveClusterFromDomain(ctx, accountA, "myapp.abc123."+testCluster)
require.NoError(t, err)
assert.Equal(t, testCluster, cluster)
}
// The manager pre-check exists to turn a conflict into a 409, but the unique
// index on the column is what actually guarantees the domain is claimed once.
func TestStore_DuplicateDomainRejectedByIndex(t *testing.T) {
ctx := context.Background()
env := setupDomainTest(t)
_, err := env.store.CreateCustomDomain(ctx, accountA, "indexed.example.com", testCluster, false)
require.NoError(t, err)
_, err = env.store.CreateCustomDomain(ctx, accountB, "indexed.example.com", testCluster, false)
assert.Error(t, err, "the unique index must reject the same domain in a second account")
}

View File

@@ -1,127 +0,0 @@
package manager
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/metric/noop"
domainmanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain/manager"
proxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy/manager"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/shared/management/status"
)
const validationTestCluster = "eu.proxy.test"
// withRealDomainManager swaps the stub cluster deriver for the real domain
// manager backed by the same store, so service creation is gated by the actual
// domain rows rather than by a test double that always agrees.
func withRealDomainManager(t *testing.T, mgr *Manager, testStore store.Store) {
t.Helper()
ctx := context.Background()
proxyMgr, err := proxymanager.NewManager(testStore, noop.NewMeterProvider().Meter(""))
require.NoError(t, err)
_, err = proxyMgr.Connect(ctx, "proxy-1", "session-1", validationTestCluster, "127.0.0.1", nil, nil)
require.NoError(t, err)
accountMgr := &mock_server.MockAccountManager{
StoreEventFunc: func(context.Context, string, string, string, activity.ActivityDescriber, map[string]any) {},
}
mgr.clusterDeriver = domainmanager.NewManager(testStore, proxyMgr, permissions.NewManager(testStore), accountMgr)
}
func newTestService(domain string) *rpservice.Service {
return &rpservice.Service{
Name: "test-service",
Domain: domain,
Enabled: true,
Mode: rpservice.ModeHTTP,
Targets: []*rpservice.Target{{
Host: "10.0.0.1",
Port: 8080,
Protocol: "http",
TargetId: testPeerID,
TargetType: "peer",
Enabled: true,
}},
}
}
// A service must not bind to a domain the account has not validated, and
// nothing may be persisted for the attempt.
func TestCreateService_RefusesUnvalidatedDomain(t *testing.T) {
ctx := context.Background()
mgr, testStore := setupIntegrationTest(t)
withRealDomainManager(t, mgr, testStore)
_, err := testStore.CreateCustomDomain(ctx, testAccountID, "unproven.example.com", validationTestCluster, false)
require.NoError(t, err)
_, err = mgr.CreateService(ctx, testAccountID, testUserID, newTestService("unproven.example.com"))
require.Error(t, err, "an unvalidated domain must not bind a service")
assert.Contains(t, err.Error(), "not validated", "the API error should name the actual problem")
sErr, ok := status.FromError(err)
require.True(t, ok, "error should be a typed status error")
assert.Equal(t, status.PreconditionFailed, sErr.Type())
services, err := testStore.GetAccountServices(ctx, store.LockingStrengthNone, testAccountID)
require.NoError(t, err)
assert.Empty(t, services, "no service row should be written for a refused domain")
}
// The negative control: a validated domain still binds a service and derives
// its cluster exactly as before.
func TestCreateService_ValidatedDomainBindsService(t *testing.T) {
ctx := context.Background()
mgr, testStore := setupIntegrationTest(t)
withRealDomainManager(t, mgr, testStore)
_, err := testStore.CreateCustomDomain(ctx, testAccountID, "proven.example.com", validationTestCluster, true)
require.NoError(t, err)
created, err := mgr.CreateService(ctx, testAccountID, testUserID, newTestService("app.proven.example.com"))
require.NoError(t, err)
assert.Equal(t, validationTestCluster, created.ProxyCluster, "service should bind to the domain's target cluster")
services, err := testStore.GetAccountServices(ctx, store.LockingStrengthNone, testAccountID)
require.NoError(t, err)
require.Len(t, services, 1, "the service should be persisted")
assert.Equal(t, "app.proven.example.com", services[0].Domain)
}
// An update must not be a way around the creation gate: moving a live service
// onto an unvalidated domain has to fail rather than silently keep the old
// cluster and start serving the new hostname.
func TestUpdateService_RefusesMoveToUnvalidatedDomain(t *testing.T) {
ctx := context.Background()
mgr, testStore := setupIntegrationTest(t)
withRealDomainManager(t, mgr, testStore)
_, err := testStore.CreateCustomDomain(ctx, testAccountID, "proven.example.com", validationTestCluster, true)
require.NoError(t, err)
_, err = testStore.CreateCustomDomain(ctx, testAccountID, "unproven.example.com", validationTestCluster, false)
require.NoError(t, err)
created, err := mgr.CreateService(ctx, testAccountID, testUserID, newTestService("app.proven.example.com"))
require.NoError(t, err)
moved := *created
moved.Domain = "app.unproven.example.com"
_, err = mgr.UpdateService(ctx, testAccountID, testUserID, &moved)
require.Error(t, err, "moving to an unvalidated domain must fail")
assert.Contains(t, err.Error(), "not validated")
stored, err := testStore.GetServiceByID(ctx, store.LockingStrengthNone, testAccountID, created.ID)
require.NoError(t, err)
assert.Equal(t, "app.proven.example.com", stored.Domain, "the service must keep its original domain")
}

View File

@@ -606,19 +606,16 @@ func (m *Manager) resolveEffectiveCluster(ctx context.Context, accountID string,
return existing.ProxyCluster, nil
}
if m.clusterDeriver == nil {
return existing.ProxyCluster, nil
if m.clusterDeriver != nil {
derived, err := m.clusterDeriver.DeriveClusterFromDomain(ctx, accountID, svc.Domain)
if err != nil {
log.WithError(err).Warnf("could not derive cluster from domain %s", svc.Domain)
} else {
return derived, nil
}
}
// Falling back to the old cluster here would let an update move a service
// onto a domain the account has not validated, bypassing the check that
// creation makes.
derived, err := m.clusterDeriver.DeriveClusterFromDomain(ctx, accountID, svc.Domain)
if err != nil {
return "", status.Errorf(status.PreconditionFailed, "could not derive cluster from domain %s: %v", svc.Domain, err)
}
return derived, nil
return existing.ProxyCluster, nil
}
func (m *Manager) executeServiceUpdate(ctx context.Context, transaction store.Store, accountID string, service *service.Service, updateInfo *serviceUpdateInfo, customPorts *bool, effectiveCluster string) error {

View File

@@ -6,8 +6,6 @@ import (
"fmt"
"slices"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
@@ -746,14 +744,6 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty
return &GroupLinkError{"network router", linkedRouter.ID}
}
if isLinked, linkedService := isGroupLinkedToReverseProxyService(ctx, transaction, group.AccountID, group.ID); isLinked {
return &GroupLinkError{"reverse proxy service", linkedService.Domain}
}
if isLinked, linkedPolicy := isGroupLinkedToAgentNetworkPolicy(ctx, transaction, group.AccountID, group.ID); isLinked {
return &GroupLinkError{"agent network policy", linkedPolicy.Name}
}
return checkGroupLinkedToSettings(ctx, transaction, group)
}
@@ -885,46 +875,6 @@ func isGroupLinkedToNetworkRouter(ctx context.Context, transaction store.Store,
return false, nil
}
// isGroupLinkedToReverseProxyService checks if a group is used as an access group
// of a private reverse proxy service or as a bearer-auth distribution group.
func isGroupLinkedToReverseProxyService(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *service.Service) {
services, err := transaction.GetAccountServices(ctx, store.LockingStrengthNone, accountID)
if err != nil {
log.WithContext(ctx).Errorf("error retrieving reverse proxy services while checking group linkage: %v", err)
return false, nil
}
for _, svc := range services {
if svc.Private && slices.Contains(svc.AccessGroups, groupID) {
return true, svc
}
if svc.Auth.BearerAuth != nil && svc.Auth.BearerAuth.Enabled && slices.Contains(svc.Auth.BearerAuth.DistributionGroups, groupID) {
return true, svc
}
}
return false, nil
}
// isGroupLinkedToAgentNetworkPolicy checks if a group is used as a source group by any
// agent network policy in the account.
func isGroupLinkedToAgentNetworkPolicy(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *agentNetworkTypes.Policy) {
policies, err := transaction.GetAccountAgentNetworkPolicies(ctx, store.LockingStrengthNone, accountID)
if err != nil {
log.WithContext(ctx).Errorf("error retrieving agent network policies while checking group linkage: %v", err)
return false, nil
}
for _, policy := range policies {
if policy == nil {
continue
}
if slices.Contains(policy.SourceGroups, groupID) {
return true, policy
}
}
return false, nil
}
// areGroupChangesAffectPeers checks if any changes to the specified groups will affect peers.
// It fetches each collection once and checks all groupIDs against them in memory.
func areGroupChangesAffectPeers(ctx context.Context, transaction store.Store, accountID string, groupIDs []string) (bool, error) {

View File

@@ -18,8 +18,6 @@ import (
"golang.org/x/exp/maps"
nbdns "github.com/netbirdio/netbird/dns"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/server/groups"
"github.com/netbirdio/netbird/management/server/networks"
"github.com/netbirdio/netbird/management/server/networks/resources"
@@ -127,21 +125,6 @@ func TestDefaultAccountManager_DeleteGroup(t *testing.T) {
"grp-for-integration",
"only service users with admin power can delete integration group",
},
{
"agent network policy",
"grp-for-agent-network-policy",
"agent network policy",
},
{
"reverse proxy private service access group",
"grp-for-rp-private",
"reverse proxy service",
},
{
"reverse proxy bearer distribution group",
"grp-for-rp-bearer",
"reverse proxy service",
},
}
for _, testCase := range testCases {
@@ -235,17 +218,6 @@ func TestDefaultAccountManager_DeleteGroups(t *testing.T) {
groupIDs: []string{"grp-for-integration"},
expectedReasons: []string{"only service users with admin power can delete integration group"},
},
{
name: "agent network policy",
groupIDs: []string{"grp-for-agent-network-policy"},
expectedReasons: []string{"agent network policy"},
},
{
name: "reverse proxy services",
groupIDs: []string{"grp-for-rp-private", "grp-for-rp-bearer"},
expectedReasons: []string{"reverse proxy service", "reverse proxy service"},
expectedNotDeleted: []string{"grp-for-rp-private", "grp-for-rp-bearer"},
},
{
name: "successfully delete multiple groups",
groupIDs: []string{"group-1", "group-2"},
@@ -313,65 +285,6 @@ func TestDefaultAccountManager_DeleteGroups(t *testing.T) {
}
}
func TestDefaultAccountManager_DeleteGroupUnlinkedFromReverseProxyService(t *testing.T) {
am, _, err := createManager(t)
require.NoError(t, err, "Failed to create account manager")
_, account, err := initTestGroupAccount(am)
require.NoError(t, err, "Failed to init testing account")
deletableGroups := []*types.Group{
{
ID: "grp-rp-bearer-disabled",
AccountID: account.Id,
Name: "Group only in a disabled bearer auth",
Issued: types.GroupIssuedAPI,
Peers: make([]string, 0),
},
{
ID: "grp-rp-nonprivate-access",
AccountID: account.Id,
Name: "Group only in a non-private service's access groups",
Issued: types.GroupIssuedAPI,
Peers: make([]string, 0),
},
}
for _, group := range deletableGroups {
require.NoError(t, am.CreateGroup(context.Background(), account.Id, groupAdminUserID, group))
}
// Disabled bearer auth and stale access groups on a non-private service
// are inert configuration and must not block group deletion.
services := []*rpservice.Service{
{
ID: "rp-svc-bearer-disabled",
AccountID: account.Id,
Domain: "bearer-disabled.services.example.com",
Auth: rpservice.AuthConfig{
BearerAuth: &rpservice.BearerAuthConfig{
Enabled: false,
DistributionGroups: []string{"grp-rp-bearer-disabled"},
},
},
},
{
ID: "rp-svc-nonprivate-access",
AccountID: account.Id,
Domain: "nonprivate.services.example.com",
Private: false,
AccessGroups: []string{"grp-rp-nonprivate-access"},
},
}
for _, svc := range services {
require.NoError(t, am.Store.CreateService(context.Background(), svc))
}
for _, group := range deletableGroups {
err = am.DeleteGroup(context.Background(), account.Id, groupAdminUserID, group.ID)
assert.NoError(t, err, "group %s is not referenced by an active reverse proxy gate and should be deletable", group.ID)
}
}
func TestDefaultAccountManager_DeleteGroupLinkedToFlowGroup(t *testing.T) {
am, _, err := createManager(t)
require.NoError(t, err)
@@ -493,30 +406,6 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t
Peers: make([]string, 0),
}
groupForAgentNetworkPolicy := &types.Group{
ID: "grp-for-agent-network-policy",
AccountID: "account-id",
Name: "Group for agent network policies",
Issued: types.GroupIssuedAPI,
Peers: make([]string, 0),
}
groupForRPPrivate := &types.Group{
ID: "grp-for-rp-private",
AccountID: "account-id",
Name: "Group for private reverse proxy service",
Issued: types.GroupIssuedAPI,
Peers: make([]string, 0),
}
groupForRPBearer := &types.Group{
ID: "grp-for-rp-bearer",
AccountID: "account-id",
Name: "Group for bearer reverse proxy service",
Issued: types.GroupIssuedAPI,
Peers: make([]string, 0),
}
routeResource := &route.Route{
ID: "example route",
Groups: []string{groupForRoute.ID},
@@ -572,66 +461,6 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForSetupKeys)
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForUsers)
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForIntegration)
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForAgentNetworkPolicy)
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForRPPrivate)
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForRPBearer)
agentNetworkPolicy := &agentNetworkTypes.Policy{
ID: "example agent network policy",
AccountID: accountID,
Name: "Example agent network policy",
Enabled: true,
SourceGroups: []string{groupForAgentNetworkPolicy.ID},
}
if err := am.Store.SaveAgentNetworkPolicy(context.Background(), agentNetworkPolicy); err != nil {
return nil, nil, err
}
// The decoy services are created first so the linkage check has to scan
// past services that do not reference the groups under test.
rpServices := []*rpservice.Service{
{
ID: "rp-svc-private-decoy",
AccountID: accountID,
Domain: "private-decoy.services.example.com",
Private: true,
AccessGroups: []string{"unrelated-group"},
},
{
ID: "rp-svc-bearer-decoy",
AccountID: accountID,
Domain: "bearer-decoy.services.example.com",
Auth: rpservice.AuthConfig{
BearerAuth: &rpservice.BearerAuthConfig{
Enabled: true,
DistributionGroups: []string{"unrelated-group"},
},
},
},
{
ID: "rp-svc-private",
AccountID: accountID,
Domain: "private.services.example.com",
Private: true,
AccessGroups: []string{groupForRPPrivate.ID},
},
{
ID: "rp-svc-bearer",
AccountID: accountID,
Domain: "bearer.services.example.com",
Auth: rpservice.AuthConfig{
BearerAuth: &rpservice.BearerAuthConfig{
Enabled: true,
DistributionGroups: []string{groupForRPBearer.ID},
},
},
},
}
for _, svc := range rpServices {
if err := am.Store.CreateService(context.Background(), svc); err != nil {
return nil, nil, err
}
}
acc, err := am.Store.GetAccount(context.Background(), account.Id)
if err != nil {

View File

@@ -5658,23 +5658,6 @@ 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.

View File

@@ -294,7 +294,6 @@ 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

View File

@@ -1892,21 +1892,6 @@ func (mr *MockStoreMockRecorder) GetCustomDomain(ctx, accountID, domainID interf
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 interface{}) *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()

View File

@@ -1707,34 +1707,14 @@ func (a *Account) injectPrivateServicePolicies(svc *service.Service, proxyPeers
if len(proxyPeers) == 0 {
return
}
// A service's AccessGroups can name groups that no longer exist — persisted
// services and the agent-network synthesiser both carry the ids verbatim from
// their own state. An unresolvable source authorises nothing, so drop it here
// rather than let the network-map assembly resolve it to a nil group.
sources := a.existingGroupIDs(svc.AccessGroups)
if len(sources) == 0 {
return
}
for _, proxyPeer := range proxyPeers {
a.Policies = append(a.Policies, a.createPrivateServicePolicy(svc, proxyPeer, sources))
a.Policies = append(a.Policies, a.createPrivateServicePolicy(svc, proxyPeer))
}
}
// existingGroupIDs returns the subset of groupIDs that resolve to a group in the account,
// preserving the input order.
func (a *Account) existingGroupIDs(groupIDs []string) []string {
out := make([]string, 0, len(groupIDs))
for _, groupID := range groupIDs {
if _, ok := a.Groups[groupID]; ok {
out = append(out, groupID)
}
}
return out
}
func (a *Account) createPrivateServicePolicy(svc *service.Service, proxyPeer *nbpeer.Peer, accessGroups []string) *Policy {
func (a *Account) createPrivateServicePolicy(svc *service.Service, proxyPeer *nbpeer.Peer) *Policy {
policyID := fmt.Sprintf("private-access-%s-%s", svc.ID, proxyPeer.ID)
sources := append([]string(nil), accessGroups...)
sources := append([]string(nil), svc.AccessGroups...)
return &Policy{
ID: policyID,
Name: fmt.Sprintf("Private Access to %s", svc.Name),