[management] Scope the change to the private-capability check

The PR grew past its purpose. What it needs to do is refuse to bootstrap an
agent network endpoint onto a cluster that cannot serve it, which is the
private capability check on the picked cluster. Everything that accreted
around it — canonicalising proxy addresses at connect, refusing another
account's cluster or a host another account pinned, withdrawing a claim
lost to a concurrent one, folding casing on migrated settings rows — is
security work in its own right and moves to follow-up PRs, where each can
be reviewed against its own threat rather than as a rider on this one.

This restores main's version of every file outside that purpose and reduces
the validation to: a cluster the account can see must have a live embedded
proxy, and a cluster management holds no row for stays pinnable
(address-first). The e2e test and the fixture seeds are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
This commit is contained in:
mlsmaycon
2026-09-12 16:08:07 +00:00
co-authored by Claude Fable 5.1
parent 4ed71f8987
commit 5502ea08ac
17 changed files with 41 additions and 1323 deletions
@@ -3,7 +3,6 @@ package migration
import (
"context"
"fmt"
"strings"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
@@ -67,19 +66,12 @@ func MigrateAgentNetworkSettingsToDomain(ctx context.Context, db *gorm.DB) error
}
if hasCluster {
// The legacy bootstrap stored the cluster as the caller spelled
// it, trimmed but never folded, while every reader of these
// columns matches exactly against canonical lowercase: proxy
// addresses are canonicalised at connect, and the proxy's host
// map is keyed by the domain verbatim. Fold here so the reshaped
// row is addressable, rather than copying a spelling nothing
// will match.
concat := "LOWER(subdomain || '.' || cluster)"
concat := "subdomain || '.' || cluster"
if tx.Name() == "mysql" {
concat = "LOWER(CONCAT(subdomain, '.', cluster))"
concat = "CONCAT(subdomain, '.', cluster)"
}
res := tx.Exec(fmt.Sprintf(
"UPDATE agent_network_settings SET domain = %s, proxy_address = LOWER(cluster) WHERE (domain IS NULL OR domain = '') AND cluster <> '' AND subdomain <> ''",
"UPDATE agent_network_settings SET domain = %s, proxy_address = cluster WHERE (domain IS NULL OR domain = '') AND cluster <> '' AND subdomain <> ''",
concat,
))
if res.Error != nil {
@@ -96,9 +88,6 @@ func MigrateAgentNetworkSettingsToDomain(ctx context.Context, db *gorm.DB) error
unmigratable,
)
}
if err := failOnDuplicateAgentNetworkDomains(tx); err != nil {
return err
}
if res.RowsAffected > 0 {
log.WithContext(ctx).Infof("migrated %d agent_network_settings row(s) to domain/proxy_address", res.RowsAffected)
@@ -121,83 +110,3 @@ func MigrateAgentNetworkSettingsToDomain(ctx context.Context, db *gorm.DB) error
return nil
})
}
// agentNetworkSettingsIdentity is the post-reshape view of the two identity
// columns, enough for the normaliser to address the table without importing
// the current model.
type agentNetworkSettingsIdentity struct {
AccountID string `gorm:"primaryKey"`
Domain string `gorm:"type:varchar(255)"`
ProxyAddress string `gorm:"type:varchar(255)"`
}
func (agentNetworkSettingsIdentity) TableName() string { return "agent_network_settings" }
// NormalizeAgentNetworkSettingsIdentity lowercases domain and proxy_address on
// rows already reshaped by a release whose backfill copied the legacy cluster
// spelling verbatim.
//
// Every reader of these columns matches exactly against canonical lowercase:
// the gateway-pin check a proxy registration runs and cluster-scoped mapping
// synthesis look proxy_address up by the canonical address, the domain lookup
// is followed by an exact Go compare, and the proxy's host map is keyed by the
// domain verbatim. A row that kept capitals is invisible to all of them, so
// the value is repaired where it is stored rather than folded on every read.
//
// MySQL needs the predicate spelled byte-wise: under its default
// case-insensitive collation `domain <> LOWER(domain)` is false for every row,
// which would leave the rows unrepaired while the Go-side compares still miss
// them. Idempotent: the predicate selects only rows that would change, one
// pass over a table holding one row per account. Runs after the reshape, so
// the columns exist whenever the table does.
func NormalizeAgentNetworkSettingsIdentity(ctx context.Context, db *gorm.DB) error {
model := &agentNetworkSettingsIdentity{}
migrator := db.Migrator()
if !migrator.HasTable(model) || !migrator.HasColumn(model, "Domain") || !migrator.HasColumn(model, "ProxyAddress") {
return nil
}
if err := failOnDuplicateAgentNetworkDomains(db); err != nil {
return err
}
predicate := "domain <> LOWER(domain) OR proxy_address <> LOWER(proxy_address)"
if db.Name() == "mysql" {
predicate = "BINARY domain <> BINARY LOWER(domain) OR BINARY proxy_address <> BINARY LOWER(proxy_address)"
}
res := db.Exec("UPDATE agent_network_settings SET domain = LOWER(domain), proxy_address = LOWER(proxy_address) WHERE " + predicate)
if res.Error != nil {
return fmt.Errorf("normalize agent_network_settings identity casing: %w", res.Error)
}
if res.RowsAffected > 0 {
log.WithContext(ctx).Infof("normalized casing on %d agent_network_settings row(s)", res.RowsAffected)
}
return nil
}
// failOnDuplicateAgentNetworkDomains refuses to continue when two settings
// rows would fold onto one endpoint hostname. Two accounts cannot share an
// endpoint, the unique index would refuse the fold with a driver message that
// names no row, and there is no right answer as to which account keeps the
// name, so the migration stops and says which hostname needs a human.
func failOnDuplicateAgentNetworkDomains(db *gorm.DB) error {
var rows []struct{ Domain string }
err := db.Raw("SELECT LOWER(domain) AS domain FROM agent_network_settings GROUP BY LOWER(domain) HAVING COUNT(*) > 1").
Scan(&rows).Error
if err != nil {
return fmt.Errorf("check agent_network_settings for endpoints differing only by case: %w", err)
}
if len(rows) == 0 {
return nil
}
duplicates := make([]string, 0, len(rows))
for _, row := range rows {
duplicates = append(duplicates, row.Domain)
}
return fmt.Errorf(
"agent_network_settings holds endpoints that differ only by case (%s); resolve them manually before upgrading",
strings.Join(duplicates, ", "),
)
}
+3 -100
View File
@@ -757,11 +757,8 @@ func TestMigrateAgentNetworkSettingsToDomain_BackfillsAndDropsLegacyColumns(t *t
db := setupDatabase(t)
require.NoError(t, db.Migrator().DropTable(&legacyAgentNetworkSettings{}))
require.NoError(t, db.AutoMigrate(&legacyAgentNetworkSettings{}))
// The cluster is spelled the way the legacy bootstrap kept it: as the
// caller typed it, trimmed but never folded. The subdomain was always
// server-assigned lowercase.
require.NoError(t, db.Create(&legacyAgentNetworkSettings{
AccountID: "acct-1", Cluster: "EU.Proxy.NetBird.io", Subdomain: "violet", EnableLogCollection: true,
AccountID: "acct-1", Cluster: "eu.proxy.netbird.io", Subdomain: "violet", EnableLogCollection: true,
}).Error)
require.NoError(t, db.Create(&legacyAgentNetworkSettings{
AccountID: "acct-2", Cluster: "us.proxy.netbird.io", Subdomain: "violet",
@@ -773,10 +770,8 @@ func TestMigrateAgentNetworkSettingsToDomain_BackfillsAndDropsLegacyColumns(t *t
var one, two agentNetworkTypes.Settings
require.NoError(t, db.First(&one, "account_id = ?", "acct-1").Error)
assert.Equal(t, "violet.eu.proxy.netbird.io", one.Domain,
"domain must combine subdomain and cluster, folded to the canonical lowercase every reader compares against")
assert.Equal(t, "eu.proxy.netbird.io", one.ProxyAddress,
"proxy address must carry the cluster in canonical lowercase, matching what proxies register under")
assert.Equal(t, "violet.eu.proxy.netbird.io", one.Domain, "domain must combine subdomain and cluster")
assert.Equal(t, "eu.proxy.netbird.io", one.ProxyAddress, "proxy address must carry the cluster")
assert.True(t, one.EnableLogCollection, "non-identity fields must ride through")
require.NoError(t, db.First(&two, "account_id = ?", "acct-2").Error)
assert.Equal(t, "violet.us.proxy.netbird.io", two.Domain,
@@ -863,95 +858,3 @@ func TestMigrateAgentNetworkSettingsToDomain_ResumesAfterPartialDrop(t *testing.
assert.Equal(t, "violet.eu.proxy.netbird.io", row.Domain, "migrated values must be untouched")
assert.Equal(t, "eu.proxy.netbird.io", row.ProxyAddress, "migrated values must be untouched")
}
// TestNormalizeAgentNetworkSettingsIdentity_LowercasesReshapedRows covers rows
// a released reshape already copied verbatim: capitals kept from the legacy
// cluster spelling are folded in place, canonical rows are left alone, and
// non-identity fields ride through.
func TestNormalizeAgentNetworkSettingsIdentity_LowercasesReshapedRows(t *testing.T) {
ctx := context.Background()
db := setupDatabase(t)
require.NoError(t, db.Migrator().DropTable(&agentNetworkTypes.Settings{}))
require.NoError(t, db.AutoMigrate(&agentNetworkTypes.Settings{}))
require.NoError(t, db.Create(&agentNetworkTypes.Settings{
AccountID: "acct-legacy", Domain: "Violet.EU.Proxy.NetBird.io", ProxyAddress: "EU.Proxy.NetBird.io", EnableLogCollection: true,
}).Error)
require.NoError(t, db.Create(&agentNetworkTypes.Settings{
AccountID: "acct-canonical", Domain: "amber.us.proxy.netbird.io", ProxyAddress: "us.proxy.netbird.io",
}).Error)
require.NoError(t, migration.NormalizeAgentNetworkSettingsIdentity(ctx, db))
var legacy, canonical agentNetworkTypes.Settings
require.NoError(t, db.First(&legacy, "account_id = ?", "acct-legacy").Error)
assert.Equal(t, "violet.eu.proxy.netbird.io", legacy.Domain, "a mixed-case endpoint must be folded where it is stored")
assert.Equal(t, "eu.proxy.netbird.io", legacy.ProxyAddress, "a mixed-case pin must be folded so exact lookups find it")
assert.True(t, legacy.EnableLogCollection, "non-identity fields must ride through")
require.NoError(t, db.First(&canonical, "account_id = ?", "acct-canonical").Error)
assert.Equal(t, "amber.us.proxy.netbird.io", canonical.Domain, "a canonical row must be left as it is")
assert.Equal(t, "us.proxy.netbird.io", canonical.ProxyAddress)
require.NoError(t, migration.NormalizeAgentNetworkSettingsIdentity(ctx, db),
"a second run over a normalised table must be a no-op, not an error")
}
// TestNormalizeAgentNetworkSettingsIdentity_SkipsMissingTable pins that a
// store which never had agent network settings is left untouched.
func TestNormalizeAgentNetworkSettingsIdentity_SkipsMissingTable(t *testing.T) {
ctx := context.Background()
db := setupDatabase(t)
require.NoError(t, db.Migrator().DropTable(&agentNetworkTypes.Settings{}))
require.NoError(t, migration.NormalizeAgentNetworkSettingsIdentity(ctx, db),
"no table must be a no-op, not an error")
assert.False(t, db.Migrator().HasTable(&agentNetworkTypes.Settings{}), "the normaliser must not create the table")
}
// TestNormalizeAgentNetworkSettingsIdentity_RefusesCaseOnlyCollision pins the
// loud failure: two rows that would fold onto one endpoint stop the migration
// with the hostname named, and neither row is touched, rather than letting the
// unique index refuse the fold with a driver message that names no row.
func TestNormalizeAgentNetworkSettingsIdentity_RefusesCaseOnlyCollision(t *testing.T) {
ctx := context.Background()
db := setupDatabase(t)
if db.Name() == "mysql" {
t.Skip("MySQL's default collation refuses two rows differing only by case at insert; the collision cannot exist there")
}
require.NoError(t, db.Migrator().DropTable(&agentNetworkTypes.Settings{}))
require.NoError(t, db.AutoMigrate(&agentNetworkTypes.Settings{}))
require.NoError(t, db.Create(&agentNetworkTypes.Settings{
AccountID: "acct-1", Domain: "Violet.eu.proxy.netbird.io", ProxyAddress: "eu.proxy.netbird.io",
}).Error)
require.NoError(t, db.Create(&agentNetworkTypes.Settings{
AccountID: "acct-2", Domain: "violet.eu.proxy.netbird.io", ProxyAddress: "eu.proxy.netbird.io",
}).Error)
err := migration.NormalizeAgentNetworkSettingsIdentity(ctx, db)
require.Error(t, err, "two rows folding onto one endpoint must stop the migration")
assert.Contains(t, err.Error(), "violet.eu.proxy.netbird.io", "the failure must name the colliding hostname")
var one agentNetworkTypes.Settings
require.NoError(t, db.First(&one, "account_id = ?", "acct-1").Error)
assert.Equal(t, "Violet.eu.proxy.netbird.io", one.Domain, "a refused normalisation must leave every row as it was")
}
// TestMigrateAgentNetworkSettingsToDomain_RefusesCaseOnlyCollision pins the
// same loud failure on the reshape: legacy rows whose identities differ only
// by case would fold onto one endpoint, and the reshape must say so rather
// than leave AutoMigrate to fail on the unique index.
func TestMigrateAgentNetworkSettingsToDomain_RefusesCaseOnlyCollision(t *testing.T) {
ctx := context.Background()
db := setupDatabase(t)
require.NoError(t, db.Migrator().DropTable(&legacyAgentNetworkSettings{}))
require.NoError(t, db.AutoMigrate(&legacyAgentNetworkSettings{}))
require.NoError(t, db.Create(&legacyAgentNetworkSettings{
AccountID: "acct-1", Cluster: "EU.proxy.netbird.io", Subdomain: "violet",
}).Error)
require.NoError(t, db.Create(&legacyAgentNetworkSettings{
AccountID: "acct-2", Cluster: "eu.proxy.netbird.io", Subdomain: "violet",
}).Error)
err := migration.MigrateAgentNetworkSettingsToDomain(ctx, db)
require.Error(t, err, "legacy rows folding onto one endpoint must stop the reshape")
assert.Contains(t, err.Error(), "violet.eu.proxy.netbird.io", "the failure must name the colliding hostname")
}
+5 -59
View File
@@ -6292,25 +6292,6 @@ func (s *SqlStore) DisconnectProxy(ctx context.Context, proxyID, sessionID strin
return nil
}
// DeleteProxy removes the proxy's row, but only while it still carries the
// given session: a registration withdrawing its own claim must not take out a
// newer session's row for the same proxy. A row already superseded or gone is
// not an error — the claim it would have withdrawn is no longer this session's
// to withdraw.
func (s *SqlStore) DeleteProxy(ctx context.Context, proxyID, sessionID string) error {
result := s.db.
Where("id = ? AND session_id = ?", proxyID, sessionID).
Delete(&proxy.Proxy{})
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to delete proxy %s session %s: %v", proxyID, sessionID, result.Error)
return status.Errorf(status.Internal, "failed to delete proxy")
}
if result.RowsAffected == 0 {
log.WithContext(ctx).Debugf("proxy %s session %s: no row deleted (already gone or superseded by a newer session)", proxyID, sessionID)
}
return nil
}
// GetAllProxies returns all reverse proxy instance rows.
func (s *SqlStore) GetAllProxies(ctx context.Context) ([]*proxy.Proxy, error) {
var proxies []*proxy.Proxy
@@ -6435,13 +6416,11 @@ func (s *SqlStore) CountProxiesByAccountID(ctx context.Context, accountID string
// queries. Backs the agent-network settings delete guard: settings cannot be
// deleted while a proxy declares the endpoint hostname as its address.
//
// The comparison folds case on both sides. Addresses are canonicalized where
// they are written now (canonicalProxyAddress on the proxy-connect path), so
// this mostly matters for a row written before that: hostnames are
// case-insensitive per RFC 4343, and on a case-sensitive collation a proxy
// stored as "GW.Example.com" would otherwise slip past the guard. This runs
// only on the settings delete path, so folding costs nothing worth indexing
// around.
// The comparison folds case on both sides: the caller passes a normalized
// (lowercase) hostname, but proxies declare their cluster address verbatim
// and Connect stores it unchanged, so on case-sensitive collations a proxy
// declaring "GW.Example.com" would otherwise slip past the guard. Hostnames
// are case-insensitive per RFC 4343; the guard must be too.
func (s *SqlStore) HasActiveProxyAtClusterAddress(ctx context.Context, clusterAddress string) (bool, error) {
var count int64
result := s.db.
@@ -6455,39 +6434,6 @@ func (s *SqlStore) HasActiveProxyAtClusterAddress(ctx context.Context, clusterAd
return count > 0, nil
}
// IsClusterAddressConflicting reports whether the address is already declared
// by a proxy outside the account — a shared proxy or another account's. The
// match is exact, and stays exact so it uses the cluster_address index:
// addresses are canonicalised where they are written (canonicalProxyAddress on
// the proxy-connect path), so one host has one spelling in this column.
// HasForeignAccountProxyAtHost reports whether a proxy owned by another
// account declares this host, folding case on both sides.
//
// Shared proxies (account_id IS NULL) are deliberately not foreign: they are
// what most accounts pin their gateway to. What this catches is two accounts
// claiming one hostname, which IsClusterAddressConflicting prevents going
// forward but cannot see for a row written before addresses were
// canonicalized.
//
// It folds case where IsClusterAddressConflicting stays exact because the
// callers differ in cost and in what they can assume. That one runs on every
// account-scoped proxy connect, where both sides are canonical and the match
// must stay exact to use the cluster_address index. This one runs once per
// account, when an agent network bootstraps, and is the only thing standing
// between that account and pinning its immutable endpoint to a cluster
// somebody else runs — worth a scan on a path taken once.
func (s *SqlStore) HasForeignAccountProxyAtHost(ctx context.Context, host, accountID string) (bool, error) {
var count int64
result := s.db.
Model(&proxy.Proxy{}).
Where("LOWER(cluster_address) = LOWER(?) AND account_id IS NOT NULL AND account_id != ?", host, accountID).
Count(&count)
if result.Error != nil {
return false, status.Errorf(status.Internal, "check proxy host ownership: %v", result.Error)
}
return count > 0, nil
}
func (s *SqlStore) IsClusterAddressConflicting(ctx context.Context, clusterAddress, accountID string) (bool, error) {
var count int64
result := s.db.
@@ -315,32 +315,6 @@ func (s *SqlStore) GetAllAgentNetworkSettings(ctx context.Context, lockStrength
return settings, nil
}
// HasGatewayPinnedByOtherAccount reports whether an account other than the
// given one has its agent network gateway pinned to this host.
//
// A pin is a claim on the host, the same way a proxy row is: the pinned
// endpoint is served by whichever proxy declares that address, and an
// account-scoped proxy only ever receives its own account's mappings. A proxy
// from a different account taking the address therefore cannot serve the pin
// and silently strands it. The pin is immutable, so the account that holds it
// cannot move out of the way — the later claimant is the one to refuse.
//
// Both sides are canonical (settings normalize on write, proxy addresses
// canonicalize at connect), so the match is exact and uses the proxy_address
// index.
func (s *SqlStore) HasGatewayPinnedByOtherAccount(ctx context.Context, host, accountID string) (bool, error) {
var count int64
result := s.db.
Model(&agentNetworkTypes.Settings{}).
Where("proxy_address = ? AND account_id != ?", host, accountID).
Count(&count)
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to check agent network gateway pins by proxy address: %v", result.Error)
return false, status.Errorf(status.Internal, "check agent network gateway pins")
}
return count > 0, nil
}
// GetAgentNetworkSettingsByProxyAddress returns every Settings row whose
// gateway is served by the proxy declaring the given cluster address. Used by
// cluster-scoped synthesis to find the accounts a shared proxy serves.
@@ -1,73 +0,0 @@
package store
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
)
// TestHasGatewayPinnedByOtherAccount_RealStore drives the query a proxy
// registration asks before claiming a cluster address, against a real sqlite
// store.
//
// A gateway pin is a claim on the host: it is immutable, it is served by
// whichever proxy declares that address, and an account-scoped proxy only ever
// receives its own account's mappings — so a proxy from a different account
// taking the address strands the pin. The account's own pin is the opposite
// case and must stay claimable, because pinning first and deploying the proxy
// after is the documented order.
func TestHasGatewayPinnedByOtherAccount_RealStore(t *testing.T) {
ctx := context.Background()
s, cleanup, err := NewTestStoreFromSQL(ctx, "", t.TempDir())
require.NoError(t, err, "real sqlite test store must come up")
defer cleanup()
const (
pinnedHost = "gw.account1.example.com"
freeHost = "nobody.example.com"
)
settings := agentNetworkTypes.DefaultSettings("account1")
settings.Domain = pinnedHost
settings.ProxyAddress = pinnedHost
require.NoError(t, s.CreateAgentNetworkSettings(ctx, settings), "seeding the pin must succeed")
t.Run("another account is refused the host", func(t *testing.T) {
pinned, err := s.HasGatewayPinnedByOtherAccount(ctx, pinnedHost, "account2")
require.NoError(t, err)
assert.True(t, pinned, "a host another account pinned its gateway to is claimed")
})
t.Run("the pinning account may still claim it", func(t *testing.T) {
pinned, err := s.HasGatewayPinnedByOtherAccount(ctx, pinnedHost, "account1")
require.NoError(t, err)
assert.False(t, pinned, "an account must be able to deploy the proxy for its own pin")
})
t.Run("an unpinned host is free", func(t *testing.T) {
pinned, err := s.HasGatewayPinnedByOtherAccount(ctx, freeHost, "account2")
require.NoError(t, err)
assert.False(t, pinned, "a host no gateway is pinned to stays claimable")
})
t.Run("a labeled pin claims the cluster, not just the endpoint", func(t *testing.T) {
// A labeled bootstrap hangs <label>.<cluster> beneath the address while
// pinning the cluster itself, so the claim follows proxy_address.
labeled := agentNetworkTypes.DefaultSettings("account3")
labeled.ProxyAddress = "byop.account3.example.com"
labeled.Domain = "violet." + labeled.ProxyAddress
require.NoError(t, s.CreateAgentNetworkSettings(ctx, labeled))
pinned, err := s.HasGatewayPinnedByOtherAccount(ctx, labeled.ProxyAddress, "account2")
require.NoError(t, err)
assert.True(t, pinned, "the pinned cluster address is the claim, not the labeled endpoint")
pinned, err = s.HasGatewayPinnedByOtherAccount(ctx, labeled.Domain, "account2")
require.NoError(t, err)
assert.False(t, pinned, "the labeled endpoint itself is not a cluster claim")
})
}
@@ -154,47 +154,3 @@ func TestSqlStore_GetAllProxies_Empty(t *testing.T) {
assert.Empty(t, all)
})
}
// TestSqlStore_DeleteProxy guards the withdrawal a registration makes when
// its claim on a cluster address is lost after the row was written:
//
// 1. The delete is session-guarded, like DisconnectProxy — a stale session
// withdrawing itself must not take out the row a newer session of the
// same proxy has since written.
// 2. A row that is already gone, or already superseded, is not an error;
// the claim it would have withdrawn is no longer this session's.
// 3. Other proxies at the same address are untouched: only the one row is
// withdrawn, not the cluster.
func TestSqlStore_DeleteProxy(t *testing.T) {
if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" {
t.Skip("skip CI tests on darwin and windows")
}
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
ctx := context.Background()
accountID := "acct-withdraw"
now := time.Now()
for _, p := range []*rpproxy.Proxy{
{ID: "p-withdrawn", SessionID: "sess-new", ClusterAddress: "byop.example.com", LastSeen: now, Status: rpproxy.StatusConnected, AccountID: &accountID},
{ID: "p-neighbour", SessionID: "sess-1", ClusterAddress: "byop.example.com", LastSeen: now, Status: rpproxy.StatusConnected, AccountID: &accountID},
} {
require.NoError(t, store.SaveProxy(ctx, p))
}
require.NoError(t, store.DeleteProxy(ctx, "p-withdrawn", "sess-old"),
"a delete under a superseded session must be a no-op, not an error")
remaining, err := store.GetAllProxies(ctx)
require.NoError(t, err)
assert.Len(t, remaining, 2, "a superseded session must not withdraw the newer session's row")
require.NoError(t, store.DeleteProxy(ctx, "p-withdrawn", "sess-new"))
remaining, err = store.GetAllProxies(ctx)
require.NoError(t, err)
require.Len(t, remaining, 1, "the withdrawing session's own row must be gone")
assert.Equal(t, "p-neighbour", remaining[0].ID, "the other proxy at the address must be untouched")
require.NoError(t, store.DeleteProxy(ctx, "p-withdrawn", "sess-new"),
"withdrawing a row that is already gone must be a no-op, not an error")
})
}
-6
View File
@@ -325,7 +325,6 @@ type Store interface {
SaveProxy(ctx context.Context, proxy *proxy.Proxy) error
DisconnectProxy(ctx context.Context, proxyID, sessionID string) error
DeleteProxy(ctx context.Context, proxyID, sessionID string) error
UpdateProxyHeartbeat(ctx context.Context, p *proxy.Proxy) error
GetActiveProxyClusterAddresses(ctx context.Context) ([]string, error)
GetActiveProxyClusterAddressesForAccount(ctx context.Context, accountID string) ([]string, error)
@@ -341,7 +340,6 @@ type Store interface {
CountProxiesByAccountID(ctx context.Context, accountID string) (int64, error)
IsClusterAddressConflicting(ctx context.Context, clusterAddress, accountID string) (bool, error)
HasActiveProxyAtClusterAddress(ctx context.Context, clusterAddress string) (bool, error)
HasForeignAccountProxyAtHost(ctx context.Context, host, accountID string) (bool, error)
DeleteAccountCluster(ctx context.Context, clusterAddress, accountID string) error
GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)
@@ -375,7 +373,6 @@ type Store interface {
GetAgentNetworkSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*agentNetworkTypes.Settings, error)
GetAllAgentNetworkSettings(ctx context.Context, lockStrength LockingStrength) ([]*agentNetworkTypes.Settings, error)
GetAgentNetworkSettingsByProxyAddress(ctx context.Context, lockStrength LockingStrength, proxyAddress string) ([]*agentNetworkTypes.Settings, error)
HasGatewayPinnedByOtherAccount(ctx context.Context, host, accountID string) (bool, error)
GetAgentNetworkSettingsByDomain(ctx context.Context, lockStrength LockingStrength, domain string) (*agentNetworkTypes.Settings, error)
CreateAgentNetworkSettings(ctx context.Context, settings *agentNetworkTypes.Settings) error
SaveAgentNetworkSettings(ctx context.Context, settings *agentNetworkTypes.Settings) error
@@ -629,9 +626,6 @@ func getMigrationsPreAuto(ctx context.Context) []migrationFunc {
func(db *gorm.DB) error {
return migration.MigrateAgentNetworkSettingsToDomain(ctx, db)
},
func(db *gorm.DB) error {
return migration.NormalizeAgentNetworkSettingsIdentity(ctx, db)
},
}
}
-44
View File
@@ -754,20 +754,6 @@ func (mr *MockStoreMockRecorder) DeletePostureChecks(ctx, accountID, postureChec
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePostureChecks", reflect.TypeOf((*MockStore)(nil).DeletePostureChecks), ctx, accountID, postureChecksID)
}
// DeleteProxy mocks base method.
func (m *MockStore) DeleteProxy(ctx context.Context, proxyID, sessionID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteProxy", ctx, proxyID, sessionID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteProxy indicates an expected call of DeleteProxy.
func (mr *MockStoreMockRecorder) DeleteProxy(ctx, proxyID, sessionID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProxy", reflect.TypeOf((*MockStore)(nil).DeleteProxy), ctx, proxyID, sessionID)
}
// DeleteRoute mocks base method.
func (m *MockStore) DeleteRoute(ctx context.Context, accountID, routeID string) error {
m.ctrl.T.Helper()
@@ -3079,36 +3065,6 @@ func (mr *MockStoreMockRecorder) HasActiveProxyAtClusterAddress(ctx, clusterAddr
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasActiveProxyAtClusterAddress", reflect.TypeOf((*MockStore)(nil).HasActiveProxyAtClusterAddress), ctx, clusterAddress)
}
// HasForeignAccountProxyAtHost mocks base method.
func (m *MockStore) HasForeignAccountProxyAtHost(ctx context.Context, host, accountID string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HasForeignAccountProxyAtHost", ctx, host, accountID)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HasForeignAccountProxyAtHost indicates an expected call of HasForeignAccountProxyAtHost.
func (mr *MockStoreMockRecorder) HasForeignAccountProxyAtHost(ctx, host, accountID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasForeignAccountProxyAtHost", reflect.TypeOf((*MockStore)(nil).HasForeignAccountProxyAtHost), ctx, host, accountID)
}
// HasGatewayPinnedByOtherAccount mocks base method.
func (m *MockStore) HasGatewayPinnedByOtherAccount(ctx context.Context, host, accountID string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HasGatewayPinnedByOtherAccount", ctx, host, accountID)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HasGatewayPinnedByOtherAccount indicates an expected call of HasGatewayPinnedByOtherAccount.
func (mr *MockStoreMockRecorder) HasGatewayPinnedByOtherAccount(ctx, host, accountID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasGatewayPinnedByOtherAccount", reflect.TypeOf((*MockStore)(nil).HasGatewayPinnedByOtherAccount), ctx, host, accountID)
}
// IncrementAgentNetworkConsumption mocks base method.
func (m *MockStore) IncrementAgentNetworkConsumption(ctx context.Context, accountID string, kind types.ConsumptionDimension, dimID string, windowSeconds int64, windowStart time.Time, tokensIn, tokensOut int64, costUSD float64) error {
m.ctrl.T.Helper()