mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-21 04:49:10 +02:00
Merge origin/main into feature/fleetdm
Resolve OpenAPI spec conflict by keeping both FleetDM schemas and new IntegrationSyncFilters/IntegrationEnabled schemas. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -154,9 +154,11 @@ func (m *managerImpl) DeletePeers(ctx context.Context, accountID string, peerIDs
|
||||
return err
|
||||
}
|
||||
|
||||
eventsToStore = append(eventsToStore, func() {
|
||||
m.accountManager.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(dnsDomain))
|
||||
})
|
||||
if !(peer.ProxyMeta.Embedded || peer.Meta.KernelVersion == "wasm") {
|
||||
eventsToStore = append(eventsToStore, func() {
|
||||
m.accountManager.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(dnsDomain))
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -20,22 +20,23 @@ const (
|
||||
)
|
||||
|
||||
type AccessLogEntry struct {
|
||||
ID string `gorm:"primaryKey"`
|
||||
AccountID string `gorm:"index"`
|
||||
ServiceID string `gorm:"index"`
|
||||
Timestamp time.Time `gorm:"index"`
|
||||
GeoLocation peer.Location `gorm:"embedded;embeddedPrefix:location_"`
|
||||
Method string `gorm:"index"`
|
||||
Host string `gorm:"index"`
|
||||
Path string `gorm:"index"`
|
||||
Duration time.Duration `gorm:"index"`
|
||||
StatusCode int `gorm:"index"`
|
||||
Reason string
|
||||
UserId string `gorm:"index"`
|
||||
AuthMethodUsed string `gorm:"index"`
|
||||
BytesUpload int64 `gorm:"index"`
|
||||
BytesDownload int64 `gorm:"index"`
|
||||
Protocol AccessLogProtocol `gorm:"index"`
|
||||
ID string `gorm:"primaryKey"`
|
||||
AccountID string `gorm:"index"`
|
||||
ServiceID string `gorm:"index"`
|
||||
Timestamp time.Time `gorm:"index"`
|
||||
GeoLocation peer.Location `gorm:"embedded;embeddedPrefix:location_"`
|
||||
SubdivisionCode string
|
||||
Method string `gorm:"index"`
|
||||
Host string `gorm:"index"`
|
||||
Path string `gorm:"index"`
|
||||
Duration time.Duration `gorm:"index"`
|
||||
StatusCode int `gorm:"index"`
|
||||
Reason string
|
||||
UserId string `gorm:"index"`
|
||||
AuthMethodUsed string `gorm:"index"`
|
||||
BytesUpload int64 `gorm:"index"`
|
||||
BytesDownload int64 `gorm:"index"`
|
||||
Protocol AccessLogProtocol `gorm:"index"`
|
||||
}
|
||||
|
||||
// FromProto creates an AccessLogEntry from a proto.AccessLog
|
||||
@@ -105,6 +106,11 @@ func (a *AccessLogEntry) ToAPIResponse() *api.ProxyAccessLog {
|
||||
cityName = &a.GeoLocation.CityName
|
||||
}
|
||||
|
||||
var subdivisionCode *string
|
||||
if a.SubdivisionCode != "" {
|
||||
subdivisionCode = &a.SubdivisionCode
|
||||
}
|
||||
|
||||
var protocol *string
|
||||
if a.Protocol != "" {
|
||||
p := string(a.Protocol)
|
||||
@@ -112,22 +118,23 @@ func (a *AccessLogEntry) ToAPIResponse() *api.ProxyAccessLog {
|
||||
}
|
||||
|
||||
return &api.ProxyAccessLog{
|
||||
Id: a.ID,
|
||||
ServiceId: a.ServiceID,
|
||||
Timestamp: a.Timestamp,
|
||||
Method: a.Method,
|
||||
Host: a.Host,
|
||||
Path: a.Path,
|
||||
DurationMs: int(a.Duration.Milliseconds()),
|
||||
StatusCode: a.StatusCode,
|
||||
SourceIp: sourceIP,
|
||||
Reason: reason,
|
||||
UserId: userID,
|
||||
AuthMethodUsed: authMethod,
|
||||
CountryCode: countryCode,
|
||||
CityName: cityName,
|
||||
BytesUpload: a.BytesUpload,
|
||||
BytesDownload: a.BytesDownload,
|
||||
Protocol: protocol,
|
||||
Id: a.ID,
|
||||
ServiceId: a.ServiceID,
|
||||
Timestamp: a.Timestamp,
|
||||
Method: a.Method,
|
||||
Host: a.Host,
|
||||
Path: a.Path,
|
||||
DurationMs: int(a.Duration.Milliseconds()),
|
||||
StatusCode: a.StatusCode,
|
||||
SourceIp: sourceIP,
|
||||
Reason: reason,
|
||||
UserId: userID,
|
||||
AuthMethodUsed: authMethod,
|
||||
CountryCode: countryCode,
|
||||
CityName: cityName,
|
||||
SubdivisionCode: subdivisionCode,
|
||||
BytesUpload: a.BytesUpload,
|
||||
BytesDownload: a.BytesDownload,
|
||||
Protocol: protocol,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ func (m *managerImpl) SaveAccessLog(ctx context.Context, logEntry *accesslogs.Ac
|
||||
logEntry.GeoLocation.CountryCode = location.Country.ISOCode
|
||||
logEntry.GeoLocation.CityName = location.City.Names.En
|
||||
logEntry.GeoLocation.GeoNameID = location.City.GeonameID
|
||||
if len(location.Subdivisions) > 0 {
|
||||
logEntry.SubdivisionCode = location.Subdivisions[0].ISOCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ type Domain struct {
|
||||
// SupportsCustomPorts is populated at query time for free domains from the
|
||||
// proxy cluster capabilities. Not persisted.
|
||||
SupportsCustomPorts *bool `gorm:"-"`
|
||||
// RequireSubdomain is populated at query time. When true, the domain
|
||||
// cannot be used bare and a subdomain label must be prepended. Not persisted.
|
||||
RequireSubdomain *bool `gorm:"-"`
|
||||
}
|
||||
|
||||
// EventMeta returns activity event metadata for a domain
|
||||
|
||||
@@ -47,6 +47,7 @@ func domainToApi(d *domain.Domain) api.ReverseProxyDomain {
|
||||
Type: domainTypeToApi(d.Type),
|
||||
Validated: d.Validated,
|
||||
SupportsCustomPorts: d.SupportsCustomPorts,
|
||||
RequireSubdomain: d.RequireSubdomain,
|
||||
}
|
||||
if d.TargetCluster != "" {
|
||||
resp.TargetCluster = &d.TargetCluster
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
|
||||
)
|
||||
|
||||
func TestExtractClusterFromFreeDomain(t *testing.T) {
|
||||
clusters := []string{"eu1.proxy.netbird.io", "us1.proxy.netbird.io"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
wantOK bool
|
||||
wantVal string
|
||||
}{
|
||||
{
|
||||
name: "subdomain of cluster matches",
|
||||
domain: "myapp.eu1.proxy.netbird.io",
|
||||
wantOK: true,
|
||||
wantVal: "eu1.proxy.netbird.io",
|
||||
},
|
||||
{
|
||||
name: "deep subdomain of cluster matches",
|
||||
domain: "foo.bar.eu1.proxy.netbird.io",
|
||||
wantOK: true,
|
||||
wantVal: "eu1.proxy.netbird.io",
|
||||
},
|
||||
{
|
||||
name: "bare cluster domain matches",
|
||||
domain: "eu1.proxy.netbird.io",
|
||||
wantOK: true,
|
||||
wantVal: "eu1.proxy.netbird.io",
|
||||
},
|
||||
{
|
||||
name: "unrelated domain does not match",
|
||||
domain: "example.com",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "partial suffix does not match",
|
||||
domain: "fakeu1.proxy.netbird.io",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "second cluster matches",
|
||||
domain: "app.us1.proxy.netbird.io",
|
||||
wantOK: true,
|
||||
wantVal: "us1.proxy.netbird.io",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cluster, ok := ExtractClusterFromFreeDomain(tc.domain, clusters)
|
||||
assert.Equal(t, tc.wantOK, ok)
|
||||
if ok {
|
||||
assert.Equal(t, tc.wantVal, cluster)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractClusterFromCustomDomains(t *testing.T) {
|
||||
customDomains := []*domain.Domain{
|
||||
{Domain: "example.com", TargetCluster: "eu1.proxy.netbird.io"},
|
||||
{Domain: "proxy.corp.io", TargetCluster: "us1.proxy.netbird.io"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
wantOK bool
|
||||
wantVal string
|
||||
}{
|
||||
{
|
||||
name: "subdomain of custom domain matches",
|
||||
domain: "app.example.com",
|
||||
wantOK: true,
|
||||
wantVal: "eu1.proxy.netbird.io",
|
||||
},
|
||||
{
|
||||
name: "bare custom domain matches",
|
||||
domain: "example.com",
|
||||
wantOK: true,
|
||||
wantVal: "eu1.proxy.netbird.io",
|
||||
},
|
||||
{
|
||||
name: "deep subdomain of custom domain matches",
|
||||
domain: "a.b.example.com",
|
||||
wantOK: true,
|
||||
wantVal: "eu1.proxy.netbird.io",
|
||||
},
|
||||
{
|
||||
name: "subdomain of multi-level custom domain matches",
|
||||
domain: "app.proxy.corp.io",
|
||||
wantOK: true,
|
||||
wantVal: "us1.proxy.netbird.io",
|
||||
},
|
||||
{
|
||||
name: "bare multi-level custom domain matches",
|
||||
domain: "proxy.corp.io",
|
||||
wantOK: true,
|
||||
wantVal: "us1.proxy.netbird.io",
|
||||
},
|
||||
{
|
||||
name: "unrelated domain does not match",
|
||||
domain: "other.com",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "partial suffix does not match custom domain",
|
||||
domain: "fakeexample.com",
|
||||
wantOK: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cluster, ok := extractClusterFromCustomDomains(tc.domain, customDomains)
|
||||
assert.Equal(t, tc.wantOK, ok)
|
||||
if ok {
|
||||
assert.Equal(t, tc.wantVal, cluster)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractClusterFromCustomDomains_OverlappingDomains(t *testing.T) {
|
||||
customDomains := []*domain.Domain{
|
||||
{Domain: "example.com", TargetCluster: "cluster-generic"},
|
||||
{Domain: "app.example.com", TargetCluster: "cluster-app"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
wantVal string
|
||||
}{
|
||||
{
|
||||
name: "exact match on more specific domain",
|
||||
domain: "app.example.com",
|
||||
wantVal: "cluster-app",
|
||||
},
|
||||
{
|
||||
name: "subdomain of more specific domain",
|
||||
domain: "api.app.example.com",
|
||||
wantVal: "cluster-app",
|
||||
},
|
||||
{
|
||||
name: "subdomain of generic domain",
|
||||
domain: "other.example.com",
|
||||
wantVal: "cluster-generic",
|
||||
},
|
||||
{
|
||||
name: "bare generic domain",
|
||||
domain: "example.com",
|
||||
wantVal: "cluster-generic",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cluster, ok := extractClusterFromCustomDomains(tc.domain, customDomains)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tc.wantVal, cluster)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -31,18 +31,15 @@ type store interface {
|
||||
|
||||
type proxyManager interface {
|
||||
GetActiveClusterAddresses(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
type clusterCapabilities interface {
|
||||
ClusterSupportsCustomPorts(clusterAddr string) *bool
|
||||
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
store store
|
||||
validator domain.Validator
|
||||
proxyManager proxyManager
|
||||
clusterCapabilities clusterCapabilities
|
||||
permissionsManager permissions.Manager
|
||||
store store
|
||||
validator domain.Validator
|
||||
proxyManager proxyManager
|
||||
permissionsManager permissions.Manager
|
||||
accountManager account.Manager
|
||||
}
|
||||
|
||||
@@ -56,11 +53,6 @@ func NewManager(store store, proxyMgr proxyManager, permissionsManager permissio
|
||||
}
|
||||
}
|
||||
|
||||
// SetClusterCapabilities sets the cluster capabilities provider for domain queries.
|
||||
func (m *Manager) SetClusterCapabilities(caps clusterCapabilities) {
|
||||
m.clusterCapabilities = caps
|
||||
}
|
||||
|
||||
func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*domain.Domain, error) {
|
||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||
if err != nil {
|
||||
@@ -96,9 +88,8 @@ func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*d
|
||||
Type: domain.TypeFree,
|
||||
Validated: true,
|
||||
}
|
||||
if m.clusterCapabilities != nil {
|
||||
d.SupportsCustomPorts = m.clusterCapabilities.ClusterSupportsCustomPorts(cluster)
|
||||
}
|
||||
d.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, cluster)
|
||||
d.RequireSubdomain = m.proxyManager.ClusterRequireSubdomain(ctx, cluster)
|
||||
ret = append(ret, d)
|
||||
}
|
||||
|
||||
@@ -112,9 +103,11 @@ func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*d
|
||||
Type: domain.TypeCustom,
|
||||
Validated: d.Validated,
|
||||
}
|
||||
if m.clusterCapabilities != nil && d.TargetCluster != "" {
|
||||
cd.SupportsCustomPorts = m.clusterCapabilities.ClusterSupportsCustomPorts(d.TargetCluster)
|
||||
if d.TargetCluster != "" {
|
||||
cd.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, d.TargetCluster)
|
||||
}
|
||||
// Custom domains never require a subdomain by default since
|
||||
// the account owns them and should be able to use the bare domain.
|
||||
ret = append(ret, cd)
|
||||
}
|
||||
|
||||
@@ -302,13 +295,19 @@ func (m Manager) DeriveClusterFromDomain(ctx context.Context, accountID, domain
|
||||
return "", fmt.Errorf("domain %s does not match any available proxy cluster", domain)
|
||||
}
|
||||
|
||||
func extractClusterFromCustomDomains(domain string, customDomains []*domain.Domain) (string, bool) {
|
||||
for _, customDomain := range customDomains {
|
||||
if strings.HasSuffix(domain, "."+customDomain.Domain) {
|
||||
return customDomain.TargetCluster, true
|
||||
func extractClusterFromCustomDomains(serviceDomain string, customDomains []*domain.Domain) (string, bool) {
|
||||
bestCluster := ""
|
||||
bestLen := -1
|
||||
for _, cd := range customDomains {
|
||||
if serviceDomain != cd.Domain && !strings.HasSuffix(serviceDomain, "."+cd.Domain) {
|
||||
continue
|
||||
}
|
||||
if l := len(cd.Domain); l > bestLen {
|
||||
bestLen = l
|
||||
bestCluster = cd.TargetCluster
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
return bestCluster, bestLen >= 0
|
||||
}
|
||||
|
||||
// ExtractClusterFromFreeDomain extracts the cluster address from a free domain.
|
||||
|
||||
@@ -11,10 +11,13 @@ import (
|
||||
|
||||
// Manager defines the interface for proxy operations
|
||||
type Manager interface {
|
||||
Connect(ctx context.Context, proxyID, clusterAddress, ipAddress string) error
|
||||
Connect(ctx context.Context, proxyID, clusterAddress, ipAddress string, capabilities *Capabilities) error
|
||||
Disconnect(ctx context.Context, proxyID string) error
|
||||
Heartbeat(ctx context.Context, proxyID string) error
|
||||
Heartbeat(ctx context.Context, proxyID, clusterAddress, ipAddress string) error
|
||||
GetActiveClusterAddresses(ctx context.Context) ([]string, error)
|
||||
GetActiveClusters(ctx context.Context) ([]Cluster, error)
|
||||
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||
CleanupStale(ctx context.Context, inactivityDuration time.Duration) error
|
||||
}
|
||||
|
||||
@@ -33,5 +36,4 @@ type Controller interface {
|
||||
RegisterProxyToCluster(ctx context.Context, clusterAddr, proxyID string) error
|
||||
UnregisterProxyFromCluster(ctx context.Context, clusterAddr, proxyID string) error
|
||||
GetProxiesForCluster(clusterAddr string) []string
|
||||
ClusterSupportsCustomPorts(clusterAddr string) *bool
|
||||
}
|
||||
|
||||
@@ -72,11 +72,6 @@ func (c *GRPCController) UnregisterProxyFromCluster(ctx context.Context, cluster
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClusterSupportsCustomPorts returns whether any proxy in the cluster supports custom ports.
|
||||
func (c *GRPCController) ClusterSupportsCustomPorts(clusterAddr string) *bool {
|
||||
return c.proxyGRPCServer.ClusterSupportsCustomPorts(clusterAddr)
|
||||
}
|
||||
|
||||
// GetProxiesForCluster returns all proxy IDs registered for a specific cluster.
|
||||
func (c *GRPCController) GetProxiesForCluster(clusterAddr string) []string {
|
||||
proxySet, ok := c.clusterProxies.Load(clusterAddr)
|
||||
|
||||
@@ -13,8 +13,11 @@ import (
|
||||
// store defines the interface for proxy persistence operations
|
||||
type store interface {
|
||||
SaveProxy(ctx context.Context, p *proxy.Proxy) error
|
||||
UpdateProxyHeartbeat(ctx context.Context, proxyID string) error
|
||||
UpdateProxyHeartbeat(ctx context.Context, proxyID, clusterAddress, ipAddress string) error
|
||||
GetActiveProxyClusterAddresses(ctx context.Context) ([]string, error)
|
||||
GetActiveProxyClusters(ctx context.Context) ([]proxy.Cluster, error)
|
||||
GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||
GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||
CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error
|
||||
}
|
||||
|
||||
@@ -37,9 +40,14 @@ func NewManager(store store, meter metric.Meter) (*Manager, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Connect registers a new proxy connection in the database
|
||||
func (m Manager) Connect(ctx context.Context, proxyID, clusterAddress, ipAddress string) error {
|
||||
// Connect registers a new proxy connection in the database.
|
||||
// capabilities may be nil for old proxies that do not report them.
|
||||
func (m Manager) Connect(ctx context.Context, proxyID, clusterAddress, ipAddress string, capabilities *proxy.Capabilities) error {
|
||||
now := time.Now()
|
||||
var caps proxy.Capabilities
|
||||
if capabilities != nil {
|
||||
caps = *capabilities
|
||||
}
|
||||
p := &proxy.Proxy{
|
||||
ID: proxyID,
|
||||
ClusterAddress: clusterAddress,
|
||||
@@ -47,6 +55,7 @@ func (m Manager) Connect(ctx context.Context, proxyID, clusterAddress, ipAddress
|
||||
LastSeen: now,
|
||||
ConnectedAt: &now,
|
||||
Status: "connected",
|
||||
Capabilities: caps,
|
||||
}
|
||||
|
||||
if err := m.store.SaveProxy(ctx, p); err != nil {
|
||||
@@ -86,11 +95,13 @@ func (m Manager) Disconnect(ctx context.Context, proxyID string) error {
|
||||
}
|
||||
|
||||
// Heartbeat updates the proxy's last seen timestamp
|
||||
func (m Manager) Heartbeat(ctx context.Context, proxyID string) error {
|
||||
if err := m.store.UpdateProxyHeartbeat(ctx, proxyID); err != nil {
|
||||
func (m Manager) Heartbeat(ctx context.Context, proxyID, clusterAddress, ipAddress string) error {
|
||||
if err := m.store.UpdateProxyHeartbeat(ctx, proxyID, clusterAddress, ipAddress); err != nil {
|
||||
log.WithContext(ctx).Debugf("failed to update proxy %s heartbeat: %v", proxyID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Tracef("updated heartbeat for proxy %s", proxyID)
|
||||
m.metrics.IncrementProxyHeartbeatCount()
|
||||
return nil
|
||||
}
|
||||
@@ -105,6 +116,28 @@ func (m Manager) GetActiveClusterAddresses(ctx context.Context) ([]string, error
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
// GetActiveClusters returns all active proxy clusters with their connected proxy count.
|
||||
func (m Manager) GetActiveClusters(ctx context.Context) ([]proxy.Cluster, error) {
|
||||
clusters, err := m.store.GetActiveProxyClusters(ctx)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get active proxy clusters: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
// ClusterSupportsCustomPorts returns whether any active proxy in the cluster
|
||||
// supports custom ports. Returns nil when no proxy has reported capabilities.
|
||||
func (m Manager) ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool {
|
||||
return m.store.GetClusterSupportsCustomPorts(ctx, clusterAddr)
|
||||
}
|
||||
|
||||
// ClusterRequireSubdomain returns whether any active proxy in the cluster
|
||||
// requires a subdomain. Returns nil when no proxy has reported capabilities.
|
||||
func (m Manager) ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool {
|
||||
return m.store.GetClusterRequireSubdomain(ctx, clusterAddr)
|
||||
}
|
||||
|
||||
// CleanupStale removes proxies that haven't sent heartbeat in the specified duration
|
||||
func (m Manager) CleanupStale(ctx context.Context, inactivityDuration time.Duration) error {
|
||||
if err := m.store.CleanupStaleProxies(ctx, inactivityDuration); err != nil {
|
||||
|
||||
@@ -50,18 +50,46 @@ func (mr *MockManagerMockRecorder) CleanupStale(ctx, inactivityDuration interfac
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanupStale", reflect.TypeOf((*MockManager)(nil).CleanupStale), ctx, inactivityDuration)
|
||||
}
|
||||
|
||||
// Connect mocks base method.
|
||||
func (m *MockManager) Connect(ctx context.Context, proxyID, clusterAddress, ipAddress string) error {
|
||||
// ClusterSupportsCustomPorts mocks base method.
|
||||
func (m *MockManager) ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Connect", ctx, proxyID, clusterAddress, ipAddress)
|
||||
ret := m.ctrl.Call(m, "ClusterSupportsCustomPorts", ctx, clusterAddr)
|
||||
ret0, _ := ret[0].(*bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ClusterSupportsCustomPorts indicates an expected call of ClusterSupportsCustomPorts.
|
||||
func (mr *MockManagerMockRecorder) ClusterSupportsCustomPorts(ctx, clusterAddr interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSupportsCustomPorts", reflect.TypeOf((*MockManager)(nil).ClusterSupportsCustomPorts), ctx, clusterAddr)
|
||||
}
|
||||
|
||||
// ClusterRequireSubdomain mocks base method.
|
||||
func (m *MockManager) ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ClusterRequireSubdomain", ctx, clusterAddr)
|
||||
ret0, _ := ret[0].(*bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ClusterRequireSubdomain indicates an expected call of ClusterRequireSubdomain.
|
||||
func (mr *MockManagerMockRecorder) ClusterRequireSubdomain(ctx, clusterAddr interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterRequireSubdomain", reflect.TypeOf((*MockManager)(nil).ClusterRequireSubdomain), ctx, clusterAddr)
|
||||
}
|
||||
|
||||
// Connect mocks base method.
|
||||
func (m *MockManager) Connect(ctx context.Context, proxyID, clusterAddress, ipAddress string, capabilities *Capabilities) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Connect", ctx, proxyID, clusterAddress, ipAddress, capabilities)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Connect indicates an expected call of Connect.
|
||||
func (mr *MockManagerMockRecorder) Connect(ctx, proxyID, clusterAddress, ipAddress interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) Connect(ctx, proxyID, clusterAddress, ipAddress, capabilities interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockManager)(nil).Connect), ctx, proxyID, clusterAddress, ipAddress)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockManager)(nil).Connect), ctx, proxyID, clusterAddress, ipAddress, capabilities)
|
||||
}
|
||||
|
||||
// Disconnect mocks base method.
|
||||
@@ -93,18 +121,33 @@ func (mr *MockManagerMockRecorder) GetActiveClusterAddresses(ctx interface{}) *g
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveClusterAddresses", reflect.TypeOf((*MockManager)(nil).GetActiveClusterAddresses), ctx)
|
||||
}
|
||||
|
||||
// Heartbeat mocks base method.
|
||||
func (m *MockManager) Heartbeat(ctx context.Context, proxyID string) error {
|
||||
// GetActiveClusters mocks base method.
|
||||
func (m *MockManager) GetActiveClusters(ctx context.Context) ([]Cluster, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Heartbeat", ctx, proxyID)
|
||||
ret := m.ctrl.Call(m, "GetActiveClusters", ctx)
|
||||
ret0, _ := ret[0].([]Cluster)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetActiveClusters indicates an expected call of GetActiveClusters.
|
||||
func (mr *MockManagerMockRecorder) GetActiveClusters(ctx interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveClusters", reflect.TypeOf((*MockManager)(nil).GetActiveClusters), ctx)
|
||||
}
|
||||
|
||||
// Heartbeat mocks base method.
|
||||
func (m *MockManager) Heartbeat(ctx context.Context, proxyID, clusterAddress, ipAddress string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Heartbeat", ctx, proxyID, clusterAddress, ipAddress)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Heartbeat indicates an expected call of Heartbeat.
|
||||
func (mr *MockManagerMockRecorder) Heartbeat(ctx, proxyID interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) Heartbeat(ctx, proxyID, clusterAddress, ipAddress interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Heartbeat", reflect.TypeOf((*MockManager)(nil).Heartbeat), ctx, proxyID)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Heartbeat", reflect.TypeOf((*MockManager)(nil).Heartbeat), ctx, proxyID, clusterAddress, ipAddress)
|
||||
}
|
||||
|
||||
// MockController is a mock of Controller interface.
|
||||
@@ -144,20 +187,6 @@ func (mr *MockControllerMockRecorder) GetOIDCValidationConfig() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOIDCValidationConfig", reflect.TypeOf((*MockController)(nil).GetOIDCValidationConfig))
|
||||
}
|
||||
|
||||
// ClusterSupportsCustomPorts mocks base method.
|
||||
func (m *MockController) ClusterSupportsCustomPorts(clusterAddr string) *bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ClusterSupportsCustomPorts", clusterAddr)
|
||||
ret0, _ := ret[0].(*bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ClusterSupportsCustomPorts indicates an expected call of ClusterSupportsCustomPorts.
|
||||
func (mr *MockControllerMockRecorder) ClusterSupportsCustomPorts(clusterAddr interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSupportsCustomPorts", reflect.TypeOf((*MockController)(nil).ClusterSupportsCustomPorts), clusterAddr)
|
||||
}
|
||||
|
||||
// GetProxiesForCluster mocks base method.
|
||||
func (m *MockController) GetProxiesForCluster(clusterAddr string) []string {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -2,6 +2,17 @@ package proxy
|
||||
|
||||
import "time"
|
||||
|
||||
// Capabilities describes what a proxy can handle, as reported via gRPC.
|
||||
// Nil fields mean the proxy never reported this capability.
|
||||
type Capabilities struct {
|
||||
// SupportsCustomPorts indicates whether this proxy can bind arbitrary
|
||||
// ports for TCP/UDP services. TLS uses SNI routing and is not gated.
|
||||
SupportsCustomPorts *bool
|
||||
// RequireSubdomain indicates whether a subdomain label is required in
|
||||
// front of the cluster domain.
|
||||
RequireSubdomain *bool
|
||||
}
|
||||
|
||||
// Proxy represents a reverse proxy instance
|
||||
type Proxy struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(255)"`
|
||||
@@ -11,6 +22,7 @@ type Proxy struct {
|
||||
ConnectedAt *time.Time
|
||||
DisconnectedAt *time.Time
|
||||
Status string `gorm:"type:varchar(20);not null;index:idx_proxy_cluster_status"`
|
||||
Capabilities Capabilities `gorm:"embedded"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -18,3 +30,9 @@ type Proxy struct {
|
||||
func (Proxy) TableName() string {
|
||||
return "proxies"
|
||||
}
|
||||
|
||||
// Cluster represents a group of proxy nodes serving the same address.
|
||||
type Cluster struct {
|
||||
Address string
|
||||
ConnectedProxies int
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
)
|
||||
|
||||
type Manager interface {
|
||||
GetActiveClusters(ctx context.Context, accountID, userID string) ([]proxy.Cluster, error)
|
||||
GetAllServices(ctx context.Context, accountID, userID string) ([]*Service, error)
|
||||
GetService(ctx context.Context, accountID, userID, serviceID string) (*Service, error)
|
||||
CreateService(ctx context.Context, accountID, userID string, service *Service) (*Service, error)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
proxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
)
|
||||
|
||||
// MockManager is a mock of Manager interface.
|
||||
@@ -107,6 +108,21 @@ func (mr *MockManagerMockRecorder) GetAccountServices(ctx, accountID interface{}
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountServices", reflect.TypeOf((*MockManager)(nil).GetAccountServices), ctx, accountID)
|
||||
}
|
||||
|
||||
// GetActiveClusters mocks base method.
|
||||
func (m *MockManager) GetActiveClusters(ctx context.Context, accountID, userID string) ([]proxy.Cluster, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetActiveClusters", ctx, accountID, userID)
|
||||
ret0, _ := ret[0].([]proxy.Cluster)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetActiveClusters indicates an expected call of GetActiveClusters.
|
||||
func (mr *MockManagerMockRecorder) GetActiveClusters(ctx, accountID, userID interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveClusters", reflect.TypeOf((*MockManager)(nil).GetActiveClusters), ctx, accountID, userID)
|
||||
}
|
||||
|
||||
// GetAllServices mocks base method.
|
||||
func (m *MockManager) GetAllServices(ctx context.Context, accountID, userID string) ([]*Service, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -34,6 +34,7 @@ func RegisterEndpoints(manager rpservice.Manager, domainManager domainmanager.Ma
|
||||
|
||||
accesslogsmanager.RegisterEndpoints(router, accessLogsManager)
|
||||
|
||||
router.HandleFunc("/reverse-proxies/clusters", h.getClusters).Methods("GET", "OPTIONS")
|
||||
router.HandleFunc("/reverse-proxies/services", h.getAllServices).Methods("GET", "OPTIONS")
|
||||
router.HandleFunc("/reverse-proxies/services", h.createService).Methods("POST", "OPTIONS")
|
||||
router.HandleFunc("/reverse-proxies/services/{serviceId}", h.getService).Methods("GET", "OPTIONS")
|
||||
@@ -177,3 +178,27 @@ func (h *handler) deleteService(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
|
||||
}
|
||||
|
||||
func (h *handler) getClusters(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
clusters, err := h.manager.GetActiveClusters(r.Context(), userAuth.AccountId, userAuth.UserId)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
apiClusters := make([]api.ProxyCluster, 0, len(clusters))
|
||||
for _, c := range clusters {
|
||||
apiClusters = append(apiClusters, api.ProxyCluster{
|
||||
Address: c.Address,
|
||||
ConnectedProxies: c.ConnectedProxies,
|
||||
})
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, apiClusters)
|
||||
}
|
||||
|
||||
@@ -75,10 +75,13 @@ func setupL4Test(t *testing.T, customPortsSupported *bool) (*Manager, store.Stor
|
||||
require.NoError(t, err)
|
||||
|
||||
mockCtrl := proxy.NewMockController(ctrl)
|
||||
mockCtrl.EXPECT().ClusterSupportsCustomPorts(gomock.Any()).Return(customPortsSupported).AnyTimes()
|
||||
mockCtrl.EXPECT().SendServiceUpdateToCluster(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||
mockCtrl.EXPECT().GetOIDCValidationConfig().Return(proxy.OIDCValidationConfig{}).AnyTimes()
|
||||
|
||||
mockCaps := proxy.NewMockManager(ctrl)
|
||||
mockCaps.EXPECT().ClusterSupportsCustomPorts(gomock.Any(), testCluster).Return(customPortsSupported).AnyTimes()
|
||||
mockCaps.EXPECT().ClusterRequireSubdomain(gomock.Any(), testCluster).Return((*bool)(nil)).AnyTimes()
|
||||
|
||||
accountMgr := &mock_server.MockAccountManager{
|
||||
StoreEventFunc: func(_ context.Context, _, _, _ string, _ activity.ActivityDescriber, _ map[string]any) {},
|
||||
UpdateAccountPeersFunc: func(_ context.Context, _ string) {},
|
||||
@@ -92,6 +95,7 @@ func setupL4Test(t *testing.T, customPortsSupported *bool) (*Manager, store.Stor
|
||||
accountManager: accountMgr,
|
||||
permissionsManager: permissions.NewManager(testStore),
|
||||
proxyController: mockCtrl,
|
||||
capabilities: mockCaps,
|
||||
clusterDeriver: &testClusterDeriver{domains: []string{"test.netbird.io"}},
|
||||
}
|
||||
mgr.exposeReaper = &exposeReaper{manager: mgr}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -13,6 +14,8 @@ import (
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
|
||||
resourcetypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/sessionkey"
|
||||
@@ -72,22 +75,30 @@ type ClusterDeriver interface {
|
||||
GetClusterDomains() []string
|
||||
}
|
||||
|
||||
// CapabilityProvider queries proxy cluster capabilities from the database.
|
||||
type CapabilityProvider interface {
|
||||
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
store store.Store
|
||||
accountManager account.Manager
|
||||
permissionsManager permissions.Manager
|
||||
proxyController proxy.Controller
|
||||
capabilities CapabilityProvider
|
||||
clusterDeriver ClusterDeriver
|
||||
exposeReaper *exposeReaper
|
||||
}
|
||||
|
||||
// NewManager creates a new service manager.
|
||||
func NewManager(store store.Store, accountManager account.Manager, permissionsManager permissions.Manager, proxyController proxy.Controller, clusterDeriver ClusterDeriver) *Manager {
|
||||
func NewManager(store store.Store, accountManager account.Manager, permissionsManager permissions.Manager, proxyController proxy.Controller, capabilities CapabilityProvider, clusterDeriver ClusterDeriver) *Manager {
|
||||
mgr := &Manager{
|
||||
store: store,
|
||||
accountManager: accountManager,
|
||||
permissionsManager: permissionsManager,
|
||||
proxyController: proxyController,
|
||||
capabilities: capabilities,
|
||||
clusterDeriver: clusterDeriver,
|
||||
}
|
||||
mgr.exposeReaper = &exposeReaper{manager: mgr}
|
||||
@@ -99,6 +110,19 @@ func (m *Manager) StartExposeReaper(ctx context.Context) {
|
||||
m.exposeReaper.StartExposeReaper(ctx)
|
||||
}
|
||||
|
||||
// GetActiveClusters returns all active proxy clusters with their connected proxy count.
|
||||
func (m *Manager) GetActiveClusters(ctx context.Context, accountID, userID string) ([]proxy.Cluster, error) {
|
||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||
if err != nil {
|
||||
return nil, status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
return m.store.GetActiveProxyClusters(ctx)
|
||||
}
|
||||
|
||||
func (m *Manager) GetAllServices(ctx context.Context, accountID, userID string) ([]*service.Service, error) {
|
||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||
if err != nil {
|
||||
@@ -220,6 +244,10 @@ func (m *Manager) initializeServiceForCreate(ctx context.Context, accountID stri
|
||||
return status.Errorf(status.PreconditionFailed, "could not derive cluster from domain %s: %v", service.Domain, err)
|
||||
}
|
||||
service.ProxyCluster = proxyCluster
|
||||
|
||||
if err := m.validateSubdomainRequirement(ctx, service.Domain, proxyCluster); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
service.AccountID = accountID
|
||||
@@ -229,6 +257,12 @@ func (m *Manager) initializeServiceForCreate(ctx context.Context, accountID stri
|
||||
return fmt.Errorf("hash secrets: %w", err)
|
||||
}
|
||||
|
||||
for i, h := range service.Auth.HeaderAuths {
|
||||
if h != nil && h.Enabled && h.Value == "" {
|
||||
return status.Errorf(status.InvalidArgument, "header_auths[%d]: value is required", i)
|
||||
}
|
||||
}
|
||||
|
||||
keyPair, err := sessionkey.GenerateKeyPair()
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate session keys: %w", err)
|
||||
@@ -239,6 +273,20 @@ func (m *Manager) initializeServiceForCreate(ctx context.Context, accountID stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateSubdomainRequirement checks whether the domain can be used bare
|
||||
// (without a subdomain label) on the given cluster. If the cluster reports
|
||||
// require_subdomain=true and the domain equals the cluster domain, it rejects.
|
||||
func (m *Manager) validateSubdomainRequirement(ctx context.Context, domain, cluster string) error {
|
||||
if domain != cluster {
|
||||
return nil
|
||||
}
|
||||
requireSub := m.capabilities.ClusterRequireSubdomain(ctx, cluster)
|
||||
if requireSub != nil && *requireSub {
|
||||
return status.Errorf(status.InvalidArgument, "domain %s requires a subdomain label", domain)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) persistNewService(ctx context.Context, accountID string, svc *service.Service) error {
|
||||
return m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||
if svc.Domain != "" {
|
||||
@@ -272,7 +320,7 @@ func (m *Manager) ensureL4Port(ctx context.Context, tx store.Store, svc *service
|
||||
if !service.IsL4Protocol(svc.Mode) {
|
||||
return nil
|
||||
}
|
||||
customPorts := m.proxyController.ClusterSupportsCustomPorts(svc.ProxyCluster)
|
||||
customPorts := m.capabilities.ClusterSupportsCustomPorts(ctx, svc.ProxyCluster)
|
||||
if service.IsPortBasedProtocol(svc.Mode) && svc.ListenPort > 0 && (customPorts == nil || !*customPorts) {
|
||||
if svc.Source != service.SourceEphemeral {
|
||||
return status.Errorf(status.InvalidArgument, "custom ports not supported on cluster %s", svc.ProxyCluster)
|
||||
@@ -467,50 +515,65 @@ func (m *Manager) persistServiceUpdate(ctx context.Context, accountID string, se
|
||||
var updateInfo serviceUpdateInfo
|
||||
|
||||
err := m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||
existingService, err := transaction.GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, service.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateProtocolChange(existingService.Mode, service.Mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateInfo.oldCluster = existingService.ProxyCluster
|
||||
updateInfo.domainChanged = existingService.Domain != service.Domain
|
||||
|
||||
if updateInfo.domainChanged {
|
||||
if err := m.handleDomainChange(ctx, transaction, accountID, service); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
service.ProxyCluster = existingService.ProxyCluster
|
||||
}
|
||||
|
||||
m.preserveExistingAuthSecrets(service, existingService)
|
||||
m.preserveServiceMetadata(service, existingService)
|
||||
m.preserveListenPort(service, existingService)
|
||||
updateInfo.serviceEnabledChanged = existingService.Enabled != service.Enabled
|
||||
|
||||
if err := m.ensureL4Port(ctx, transaction, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.checkPortConflict(ctx, transaction, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateTargetReferences(ctx, transaction, accountID, service.Targets); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := transaction.UpdateService(ctx, service); err != nil {
|
||||
return fmt.Errorf("update service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return m.executeServiceUpdate(ctx, transaction, accountID, service, &updateInfo)
|
||||
})
|
||||
|
||||
return &updateInfo, err
|
||||
}
|
||||
|
||||
func (m *Manager) executeServiceUpdate(ctx context.Context, transaction store.Store, accountID string, service *service.Service, updateInfo *serviceUpdateInfo) error {
|
||||
existingService, err := transaction.GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, service.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existingService.Terminated {
|
||||
return status.Errorf(status.PermissionDenied, "service is terminated and cannot be updated")
|
||||
}
|
||||
|
||||
if err := validateProtocolChange(existingService.Mode, service.Mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateInfo.oldCluster = existingService.ProxyCluster
|
||||
updateInfo.domainChanged = existingService.Domain != service.Domain
|
||||
|
||||
if updateInfo.domainChanged {
|
||||
if err := m.handleDomainChange(ctx, transaction, accountID, service); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
service.ProxyCluster = existingService.ProxyCluster
|
||||
}
|
||||
|
||||
if err := m.validateSubdomainRequirement(ctx, service.Domain, service.ProxyCluster); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.preserveExistingAuthSecrets(service, existingService)
|
||||
if err := validateHeaderAuthValues(service.Auth.HeaderAuths); err != nil {
|
||||
return err
|
||||
}
|
||||
m.preserveServiceMetadata(service, existingService)
|
||||
m.preserveListenPort(service, existingService)
|
||||
updateInfo.serviceEnabledChanged = existingService.Enabled != service.Enabled
|
||||
|
||||
if err := m.ensureL4Port(ctx, transaction, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.checkPortConflict(ctx, transaction, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateTargetReferences(ctx, transaction, accountID, service.Targets); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := transaction.UpdateService(ctx, service); err != nil {
|
||||
return fmt.Errorf("update service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleDomainChange(ctx context.Context, transaction store.Store, accountID string, svc *service.Service) error {
|
||||
if err := m.checkDomainAvailable(ctx, transaction, svc.Domain, svc.ID); err != nil {
|
||||
return err
|
||||
@@ -544,18 +607,52 @@ func isHTTPFamily(mode string) bool {
|
||||
return mode == "" || mode == "http"
|
||||
}
|
||||
|
||||
func (m *Manager) preserveExistingAuthSecrets(service, existingService *service.Service) {
|
||||
if service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled &&
|
||||
func (m *Manager) preserveExistingAuthSecrets(svc, existingService *service.Service) {
|
||||
if svc.Auth.PasswordAuth != nil && svc.Auth.PasswordAuth.Enabled &&
|
||||
existingService.Auth.PasswordAuth != nil && existingService.Auth.PasswordAuth.Enabled &&
|
||||
service.Auth.PasswordAuth.Password == "" {
|
||||
service.Auth.PasswordAuth = existingService.Auth.PasswordAuth
|
||||
svc.Auth.PasswordAuth.Password == "" {
|
||||
svc.Auth.PasswordAuth = existingService.Auth.PasswordAuth
|
||||
}
|
||||
|
||||
if service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled &&
|
||||
if svc.Auth.PinAuth != nil && svc.Auth.PinAuth.Enabled &&
|
||||
existingService.Auth.PinAuth != nil && existingService.Auth.PinAuth.Enabled &&
|
||||
service.Auth.PinAuth.Pin == "" {
|
||||
service.Auth.PinAuth = existingService.Auth.PinAuth
|
||||
svc.Auth.PinAuth.Pin == "" {
|
||||
svc.Auth.PinAuth = existingService.Auth.PinAuth
|
||||
}
|
||||
|
||||
preserveHeaderAuthHashes(svc.Auth.HeaderAuths, existingService.Auth.HeaderAuths)
|
||||
}
|
||||
|
||||
// preserveHeaderAuthHashes fills in empty header auth values from the existing
|
||||
// service so that unchanged secrets are not lost on update.
|
||||
func preserveHeaderAuthHashes(headers, existing []*service.HeaderAuthConfig) {
|
||||
if len(headers) == 0 || len(existing) == 0 {
|
||||
return
|
||||
}
|
||||
existingByHeader := make(map[string]string, len(existing))
|
||||
for _, h := range existing {
|
||||
if h != nil && h.Value != "" {
|
||||
existingByHeader[http.CanonicalHeaderKey(h.Header)] = h.Value
|
||||
}
|
||||
}
|
||||
for _, h := range headers {
|
||||
if h != nil && h.Enabled && h.Value == "" {
|
||||
if hash, ok := existingByHeader[http.CanonicalHeaderKey(h.Header)]; ok {
|
||||
h.Value = hash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateHeaderAuthValues checks that all enabled header auths have a value
|
||||
// (either freshly provided or preserved from the existing service).
|
||||
func validateHeaderAuthValues(headers []*service.HeaderAuthConfig) error {
|
||||
for i, h := range headers {
|
||||
if h != nil && h.Enabled && h.Value == "" {
|
||||
return status.Errorf(status.InvalidArgument, "header_auths[%d]: value is required", i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) preserveServiceMetadata(service, existingService *service.Service) {
|
||||
@@ -592,24 +689,53 @@ func validateTargetReferences(ctx context.Context, transaction store.Store, acco
|
||||
for _, target := range targets {
|
||||
switch target.TargetType {
|
||||
case service.TargetTypePeer:
|
||||
if _, err := transaction.GetPeerByID(ctx, store.LockingStrengthShare, accountID, target.TargetId); err != nil {
|
||||
if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound {
|
||||
return status.Errorf(status.InvalidArgument, "peer target %q not found in account", target.TargetId)
|
||||
}
|
||||
return fmt.Errorf("look up peer target %q: %w", target.TargetId, err)
|
||||
if err := validatePeerTarget(ctx, transaction, accountID, target); err != nil {
|
||||
return err
|
||||
}
|
||||
case service.TargetTypeHost, service.TargetTypeSubnet, service.TargetTypeDomain:
|
||||
if _, err := transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, accountID, target.TargetId); err != nil {
|
||||
if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound {
|
||||
return status.Errorf(status.InvalidArgument, "resource target %q not found in account", target.TargetId)
|
||||
}
|
||||
return fmt.Errorf("look up resource target %q: %w", target.TargetId, err)
|
||||
if err := validateResourceTarget(ctx, transaction, accountID, target); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return status.Errorf(status.InvalidArgument, "unknown target type %q for target %q", target.TargetType, target.TargetId)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePeerTarget(ctx context.Context, transaction store.Store, accountID string, target *service.Target) error {
|
||||
if _, err := transaction.GetPeerByID(ctx, store.LockingStrengthShare, accountID, target.TargetId); err != nil {
|
||||
if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound {
|
||||
return status.Errorf(status.InvalidArgument, "peer target %q not found in account", target.TargetId)
|
||||
}
|
||||
return fmt.Errorf("look up peer target %q: %w", target.TargetId, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateResourceTarget(ctx context.Context, transaction store.Store, accountID string, target *service.Target) error {
|
||||
resource, err := transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, accountID, target.TargetId)
|
||||
if err != nil {
|
||||
if sErr, ok := status.FromError(err); ok && sErr.Type() == status.NotFound {
|
||||
return status.Errorf(status.InvalidArgument, "resource target %q not found in account", target.TargetId)
|
||||
}
|
||||
return fmt.Errorf("look up resource target %q: %w", target.TargetId, err)
|
||||
}
|
||||
return validateResourceTargetType(target, resource)
|
||||
}
|
||||
|
||||
// validateResourceTargetType checks that target_type matches the actual network resource type.
|
||||
func validateResourceTargetType(target *service.Target, resource *resourcetypes.NetworkResource) error {
|
||||
expected := resourcetypes.NetworkResourceType(target.TargetType)
|
||||
if resource.Type != expected {
|
||||
return status.Errorf(status.InvalidArgument,
|
||||
"target %q has target_type %q but resource is of type %q",
|
||||
target.TargetId, target.TargetType, resource.Type,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteService(ctx context.Context, accountID, userID, serviceID string) error {
|
||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||
resourcetypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
@@ -1214,3 +1215,126 @@ func TestValidateProtocolChange(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTargetReferences_ResourceTypeMismatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := store.NewMockStore(ctrl)
|
||||
accountID := "test-account"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
targetType rpservice.TargetType
|
||||
resourceType resourcetypes.NetworkResourceType
|
||||
wantErr bool
|
||||
}{
|
||||
{"host matches host", rpservice.TargetTypeHost, resourcetypes.Host, false},
|
||||
{"domain matches domain", rpservice.TargetTypeDomain, resourcetypes.Domain, false},
|
||||
{"subnet matches subnet", rpservice.TargetTypeSubnet, resourcetypes.Subnet, false},
|
||||
{"host but resource is domain", rpservice.TargetTypeHost, resourcetypes.Domain, true},
|
||||
{"domain but resource is host", rpservice.TargetTypeDomain, resourcetypes.Host, true},
|
||||
{"host but resource is subnet", rpservice.TargetTypeHost, resourcetypes.Subnet, true},
|
||||
{"subnet but resource is domain", rpservice.TargetTypeSubnet, resourcetypes.Domain, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockStore.EXPECT().
|
||||
GetNetworkResourceByID(gomock.Any(), store.LockingStrengthShare, accountID, "resource-1").
|
||||
Return(&resourcetypes.NetworkResource{Type: tt.resourceType}, nil)
|
||||
|
||||
targets := []*rpservice.Target{
|
||||
{TargetId: "resource-1", TargetType: tt.targetType, Host: "10.0.0.1"},
|
||||
}
|
||||
err := validateTargetReferences(ctx, mockStore, accountID, targets)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "target_type")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTargetReferences_PeerValid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := store.NewMockStore(ctrl)
|
||||
accountID := "test-account"
|
||||
|
||||
mockStore.EXPECT().
|
||||
GetPeerByID(gomock.Any(), store.LockingStrengthShare, accountID, "peer-1").
|
||||
Return(&nbpeer.Peer{}, nil)
|
||||
|
||||
targets := []*rpservice.Target{
|
||||
{TargetId: "peer-1", TargetType: rpservice.TargetTypePeer},
|
||||
}
|
||||
require.NoError(t, validateTargetReferences(ctx, mockStore, accountID, targets))
|
||||
}
|
||||
|
||||
func TestValidateSubdomainRequirement(t *testing.T) {
|
||||
ptrBool := func(b bool) *bool { return &b }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
cluster string
|
||||
requireSubdomain *bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "subdomain present, require_subdomain true",
|
||||
domain: "app.eu1.proxy.netbird.io",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: ptrBool(true),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "bare cluster domain, require_subdomain true",
|
||||
domain: "eu1.proxy.netbird.io",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: ptrBool(true),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bare cluster domain, require_subdomain false",
|
||||
domain: "eu1.proxy.netbird.io",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: ptrBool(false),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "bare cluster domain, require_subdomain nil (default)",
|
||||
domain: "eu1.proxy.netbird.io",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "custom domain apex is not the cluster",
|
||||
domain: "example.com",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: ptrBool(true),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
mockCaps := proxy.NewMockManager(ctrl)
|
||||
mockCaps.EXPECT().ClusterRequireSubdomain(gomock.Any(), tc.cluster).Return(tc.requireSubdomain).AnyTimes()
|
||||
|
||||
mgr := &Manager{capabilities: mockCaps}
|
||||
err := mgr.validateSubdomainRequirement(context.Background(), tc.domain, tc.cluster)
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "requires a subdomain label")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ import (
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
@@ -91,10 +92,37 @@ type BearerAuthConfig struct {
|
||||
DistributionGroups []string `json:"distribution_groups,omitempty" gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// HeaderAuthConfig defines a static header-value auth check.
|
||||
// The proxy compares the incoming header value against the stored hash.
|
||||
type HeaderAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Header string `json:"header"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
PasswordAuth *PasswordAuthConfig `json:"password_auth,omitempty" gorm:"serializer:json"`
|
||||
PinAuth *PINAuthConfig `json:"pin_auth,omitempty" gorm:"serializer:json"`
|
||||
BearerAuth *BearerAuthConfig `json:"bearer_auth,omitempty" gorm:"serializer:json"`
|
||||
HeaderAuths []*HeaderAuthConfig `json:"header_auths,omitempty" gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// AccessRestrictions controls who can connect to the service based on IP or geography.
|
||||
type AccessRestrictions struct {
|
||||
AllowedCIDRs []string `json:"allowed_cidrs,omitempty" gorm:"serializer:json"`
|
||||
BlockedCIDRs []string `json:"blocked_cidrs,omitempty" gorm:"serializer:json"`
|
||||
AllowedCountries []string `json:"allowed_countries,omitempty" gorm:"serializer:json"`
|
||||
BlockedCountries []string `json:"blocked_countries,omitempty" gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the AccessRestrictions.
|
||||
func (r AccessRestrictions) Copy() AccessRestrictions {
|
||||
return AccessRestrictions{
|
||||
AllowedCIDRs: slices.Clone(r.AllowedCIDRs),
|
||||
BlockedCIDRs: slices.Clone(r.BlockedCIDRs),
|
||||
AllowedCountries: slices.Clone(r.AllowedCountries),
|
||||
BlockedCountries: slices.Clone(r.BlockedCountries),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthConfig) HashSecrets() error {
|
||||
@@ -114,6 +142,16 @@ func (a *AuthConfig) HashSecrets() error {
|
||||
a.PinAuth.Pin = hashedPin
|
||||
}
|
||||
|
||||
for i, h := range a.HeaderAuths {
|
||||
if h != nil && h.Enabled && h.Value != "" {
|
||||
hashedValue, err := argon2id.Hash(h.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hash header auth[%d] value: %w", i, err)
|
||||
}
|
||||
h.Value = hashedValue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -124,6 +162,11 @@ func (a *AuthConfig) ClearSecrets() {
|
||||
if a.PinAuth != nil {
|
||||
a.PinAuth.Pin = ""
|
||||
}
|
||||
for _, h := range a.HeaderAuths {
|
||||
if h != nil {
|
||||
h.Value = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
@@ -141,14 +184,16 @@ type Service struct {
|
||||
ProxyCluster string `gorm:"index"`
|
||||
Targets []*Target `gorm:"foreignKey:ServiceID;constraint:OnDelete:CASCADE"`
|
||||
Enabled bool
|
||||
Terminated bool
|
||||
PassHostHeader bool
|
||||
RewriteRedirects bool
|
||||
Auth AuthConfig `gorm:"serializer:json"`
|
||||
Meta Meta `gorm:"embedded;embeddedPrefix:meta_"`
|
||||
SessionPrivateKey string `gorm:"column:session_private_key"`
|
||||
SessionPublicKey string `gorm:"column:session_public_key"`
|
||||
Source string `gorm:"default:'permanent';index:idx_service_source_peer"`
|
||||
SourcePeer string `gorm:"index:idx_service_source_peer"`
|
||||
Auth AuthConfig `gorm:"serializer:json"`
|
||||
Restrictions AccessRestrictions `gorm:"serializer:json"`
|
||||
Meta Meta `gorm:"embedded;embeddedPrefix:meta_"`
|
||||
SessionPrivateKey string `gorm:"column:session_private_key"`
|
||||
SessionPublicKey string `gorm:"column:session_public_key"`
|
||||
Source string `gorm:"default:'permanent';index:idx_service_source_peer"`
|
||||
SourcePeer string `gorm:"index:idx_service_source_peer"`
|
||||
// Mode determines the service type: "http", "tcp", "udp", or "tls".
|
||||
Mode string `gorm:"default:'http'"`
|
||||
ListenPort uint16
|
||||
@@ -188,6 +233,20 @@ func (s *Service) ToAPIResponse() *api.Service {
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.Auth.HeaderAuths) > 0 {
|
||||
apiHeaders := make([]api.HeaderAuthConfig, 0, len(s.Auth.HeaderAuths))
|
||||
for _, h := range s.Auth.HeaderAuths {
|
||||
if h == nil {
|
||||
continue
|
||||
}
|
||||
apiHeaders = append(apiHeaders, api.HeaderAuthConfig{
|
||||
Enabled: h.Enabled,
|
||||
Header: h.Header,
|
||||
})
|
||||
}
|
||||
authConfig.HeaderAuths = &apiHeaders
|
||||
}
|
||||
|
||||
// Convert internal targets to API targets
|
||||
apiTargets := make([]api.ServiceTarget, 0, len(s.Targets))
|
||||
for _, target := range s.Targets {
|
||||
@@ -198,13 +257,15 @@ func (s *Service) ToAPIResponse() *api.Service {
|
||||
Protocol: api.ServiceTargetProtocol(target.Protocol),
|
||||
TargetId: target.TargetId,
|
||||
TargetType: api.ServiceTargetTargetType(target.TargetType),
|
||||
Enabled: target.Enabled,
|
||||
Enabled: target.Enabled && !s.Terminated,
|
||||
}
|
||||
opts := targetOptionsToAPI(target.Options)
|
||||
if opts == nil {
|
||||
opts = &api.ServiceTargetOptions{}
|
||||
}
|
||||
opts.ProxyProtocol = &target.ProxyProtocol
|
||||
if target.ProxyProtocol {
|
||||
opts.ProxyProtocol = &target.ProxyProtocol
|
||||
}
|
||||
st.Options = opts
|
||||
apiTargets = append(apiTargets, st)
|
||||
}
|
||||
@@ -222,18 +283,20 @@ func (s *Service) ToAPIResponse() *api.Service {
|
||||
listenPort := int(s.ListenPort)
|
||||
|
||||
resp := &api.Service{
|
||||
Id: s.ID,
|
||||
Name: s.Name,
|
||||
Domain: s.Domain,
|
||||
Targets: apiTargets,
|
||||
Enabled: s.Enabled,
|
||||
PassHostHeader: &s.PassHostHeader,
|
||||
RewriteRedirects: &s.RewriteRedirects,
|
||||
Auth: authConfig,
|
||||
Meta: meta,
|
||||
Mode: &mode,
|
||||
ListenPort: &listenPort,
|
||||
PortAutoAssigned: &s.PortAutoAssigned,
|
||||
Id: s.ID,
|
||||
Name: s.Name,
|
||||
Domain: s.Domain,
|
||||
Targets: apiTargets,
|
||||
Enabled: s.Enabled && !s.Terminated,
|
||||
Terminated: &s.Terminated,
|
||||
PassHostHeader: &s.PassHostHeader,
|
||||
RewriteRedirects: &s.RewriteRedirects,
|
||||
Auth: authConfig,
|
||||
AccessRestrictions: restrictionsToAPI(s.Restrictions),
|
||||
Meta: meta,
|
||||
Mode: &mode,
|
||||
ListenPort: &listenPort,
|
||||
PortAutoAssigned: &s.PortAutoAssigned,
|
||||
}
|
||||
|
||||
if s.ProxyCluster != "" {
|
||||
@@ -263,7 +326,16 @@ func (s *Service) ToProtoMapping(operation Operation, authToken string, oidcConf
|
||||
auth.Oidc = true
|
||||
}
|
||||
|
||||
return &proto.ProxyMapping{
|
||||
for _, h := range s.Auth.HeaderAuths {
|
||||
if h != nil && h.Enabled {
|
||||
auth.HeaderAuths = append(auth.HeaderAuths, &proto.HeaderAuth{
|
||||
Header: h.Header,
|
||||
HashedValue: h.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mapping := &proto.ProxyMapping{
|
||||
Type: operationToProtoType(operation),
|
||||
Id: s.ID,
|
||||
Domain: s.Domain,
|
||||
@@ -276,6 +348,12 @@ func (s *Service) ToProtoMapping(operation Operation, authToken string, oidcConf
|
||||
Mode: s.Mode,
|
||||
ListenPort: int32(s.ListenPort), //nolint:gosec
|
||||
}
|
||||
|
||||
if r := restrictionsToProto(s.Restrictions); r != nil {
|
||||
mapping.AccessRestrictions = r
|
||||
}
|
||||
|
||||
return mapping
|
||||
}
|
||||
|
||||
// buildPathMappings constructs PathMapping entries from targets.
|
||||
@@ -334,8 +412,7 @@ func operationToProtoType(op Operation) proto.ProxyMappingUpdateType {
|
||||
case Delete:
|
||||
return proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED
|
||||
default:
|
||||
log.Fatalf("unknown operation type: %v", op)
|
||||
return proto.ProxyMappingUpdateType_UPDATE_TYPE_CREATED
|
||||
panic(fmt.Sprintf("unknown operation type: %v", op))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,6 +554,10 @@ func (s *Service) FromAPIRequest(req *api.ServiceRequest, accountID string) erro
|
||||
s.Auth = authFromAPI(req.Auth)
|
||||
}
|
||||
|
||||
if req.AccessRestrictions != nil {
|
||||
s.Restrictions = restrictionsFromAPI(req.AccessRestrictions)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -538,9 +619,70 @@ func authFromAPI(reqAuth *api.ServiceAuthConfig) AuthConfig {
|
||||
}
|
||||
auth.BearerAuth = bearerAuth
|
||||
}
|
||||
if reqAuth.HeaderAuths != nil {
|
||||
for _, h := range *reqAuth.HeaderAuths {
|
||||
auth.HeaderAuths = append(auth.HeaderAuths, &HeaderAuthConfig{
|
||||
Enabled: h.Enabled,
|
||||
Header: h.Header,
|
||||
Value: h.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
return auth
|
||||
}
|
||||
|
||||
func restrictionsFromAPI(r *api.AccessRestrictions) AccessRestrictions {
|
||||
if r == nil {
|
||||
return AccessRestrictions{}
|
||||
}
|
||||
var res AccessRestrictions
|
||||
if r.AllowedCidrs != nil {
|
||||
res.AllowedCIDRs = *r.AllowedCidrs
|
||||
}
|
||||
if r.BlockedCidrs != nil {
|
||||
res.BlockedCIDRs = *r.BlockedCidrs
|
||||
}
|
||||
if r.AllowedCountries != nil {
|
||||
res.AllowedCountries = *r.AllowedCountries
|
||||
}
|
||||
if r.BlockedCountries != nil {
|
||||
res.BlockedCountries = *r.BlockedCountries
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func restrictionsToAPI(r AccessRestrictions) *api.AccessRestrictions {
|
||||
if len(r.AllowedCIDRs) == 0 && len(r.BlockedCIDRs) == 0 && len(r.AllowedCountries) == 0 && len(r.BlockedCountries) == 0 {
|
||||
return nil
|
||||
}
|
||||
res := &api.AccessRestrictions{}
|
||||
if len(r.AllowedCIDRs) > 0 {
|
||||
res.AllowedCidrs = &r.AllowedCIDRs
|
||||
}
|
||||
if len(r.BlockedCIDRs) > 0 {
|
||||
res.BlockedCidrs = &r.BlockedCIDRs
|
||||
}
|
||||
if len(r.AllowedCountries) > 0 {
|
||||
res.AllowedCountries = &r.AllowedCountries
|
||||
}
|
||||
if len(r.BlockedCountries) > 0 {
|
||||
res.BlockedCountries = &r.BlockedCountries
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func restrictionsToProto(r AccessRestrictions) *proto.AccessRestrictions {
|
||||
if len(r.AllowedCIDRs) == 0 && len(r.BlockedCIDRs) == 0 && len(r.AllowedCountries) == 0 && len(r.BlockedCountries) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &proto.AccessRestrictions{
|
||||
AllowedCidrs: r.AllowedCIDRs,
|
||||
BlockedCidrs: r.BlockedCIDRs,
|
||||
AllowedCountries: r.AllowedCountries,
|
||||
BlockedCountries: r.BlockedCountries,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Validate() error {
|
||||
if s.Name == "" {
|
||||
return errors.New("service name is required")
|
||||
@@ -557,6 +699,13 @@ func (s *Service) Validate() error {
|
||||
s.Mode = ModeHTTP
|
||||
}
|
||||
|
||||
if err := validateHeaderAuths(s.Auth.HeaderAuths); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateAccessRestrictions(&s.Restrictions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch s.Mode {
|
||||
case ModeHTTP:
|
||||
return s.validateHTTPMode()
|
||||
@@ -645,7 +794,7 @@ func (s *Service) validateL4Target(target *Target) error {
|
||||
return errors.New("target_id is required for L4 services")
|
||||
}
|
||||
switch target.TargetType {
|
||||
case TargetTypePeer, TargetTypeHost:
|
||||
case TargetTypePeer, TargetTypeHost, TargetTypeDomain:
|
||||
// OK
|
||||
case TargetTypeSubnet:
|
||||
if target.Host == "" {
|
||||
@@ -657,6 +806,21 @@ func (s *Service) validateL4Target(target *Target) error {
|
||||
if target.Path != nil && *target.Path != "" && *target.Path != "/" {
|
||||
return errors.New("path is not supported for L4 services")
|
||||
}
|
||||
if target.Options.SessionIdleTimeout < 0 {
|
||||
return errors.New("session_idle_timeout must be positive for L4 services")
|
||||
}
|
||||
if target.Options.RequestTimeout < 0 {
|
||||
return errors.New("request_timeout must be positive for L4 services")
|
||||
}
|
||||
if target.Options.SkipTLSVerify {
|
||||
return errors.New("skip_tls_verify is not supported for L4 services")
|
||||
}
|
||||
if target.Options.PathRewrite != "" {
|
||||
return errors.New("path_rewrite is not supported for L4 services")
|
||||
}
|
||||
if len(target.Options.CustomHeaders) > 0 {
|
||||
return errors.New("custom_headers is not supported for L4 services")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -688,11 +852,9 @@ func IsPortBasedProtocol(mode string) bool {
|
||||
}
|
||||
|
||||
const (
|
||||
maxRequestTimeout = 5 * time.Minute
|
||||
maxSessionIdleTimeout = 10 * time.Minute
|
||||
maxCustomHeaders = 16
|
||||
maxHeaderKeyLen = 128
|
||||
maxHeaderValueLen = 4096
|
||||
maxCustomHeaders = 16
|
||||
maxHeaderKeyLen = 128
|
||||
maxHeaderValueLen = 4096
|
||||
)
|
||||
|
||||
// httpHeaderNameRe matches valid HTTP header field names per RFC 7230 token definition.
|
||||
@@ -731,22 +893,12 @@ func validateTargetOptions(idx int, opts *TargetOptions) error {
|
||||
return fmt.Errorf("target %d: unknown path_rewrite mode %q", idx, opts.PathRewrite)
|
||||
}
|
||||
|
||||
if opts.RequestTimeout != 0 {
|
||||
if opts.RequestTimeout <= 0 {
|
||||
return fmt.Errorf("target %d: request_timeout must be positive", idx)
|
||||
}
|
||||
if opts.RequestTimeout > maxRequestTimeout {
|
||||
return fmt.Errorf("target %d: request_timeout exceeds maximum of %s", idx, maxRequestTimeout)
|
||||
}
|
||||
if opts.RequestTimeout < 0 {
|
||||
return fmt.Errorf("target %d: request_timeout must be positive", idx)
|
||||
}
|
||||
|
||||
if opts.SessionIdleTimeout != 0 {
|
||||
if opts.SessionIdleTimeout <= 0 {
|
||||
return fmt.Errorf("target %d: session_idle_timeout must be positive", idx)
|
||||
}
|
||||
if opts.SessionIdleTimeout > maxSessionIdleTimeout {
|
||||
return fmt.Errorf("target %d: session_idle_timeout exceeds maximum of %s", idx, maxSessionIdleTimeout)
|
||||
}
|
||||
if opts.SessionIdleTimeout < 0 {
|
||||
return fmt.Errorf("target %d: session_idle_timeout must be positive", idx)
|
||||
}
|
||||
|
||||
if err := validateCustomHeaders(idx, opts.CustomHeaders); err != nil {
|
||||
@@ -796,6 +948,88 @@ func containsCRLF(s string) bool {
|
||||
return strings.ContainsAny(s, "\r\n")
|
||||
}
|
||||
|
||||
func validateHeaderAuths(headers []*HeaderAuthConfig) error {
|
||||
for i, h := range headers {
|
||||
if h == nil || !h.Enabled {
|
||||
continue
|
||||
}
|
||||
if h.Header == "" {
|
||||
return fmt.Errorf("header_auths[%d]: header name is required", i)
|
||||
}
|
||||
if !httpHeaderNameRe.MatchString(h.Header) {
|
||||
return fmt.Errorf("header_auths[%d]: header name %q is not a valid HTTP header name", i, h.Header)
|
||||
}
|
||||
canonical := http.CanonicalHeaderKey(h.Header)
|
||||
if _, ok := hopByHopHeaders[canonical]; ok {
|
||||
return fmt.Errorf("header_auths[%d]: header %q is a hop-by-hop header and cannot be used for auth", i, h.Header)
|
||||
}
|
||||
if _, ok := reservedHeaders[canonical]; ok {
|
||||
return fmt.Errorf("header_auths[%d]: header %q is managed by the proxy and cannot be used for auth", i, h.Header)
|
||||
}
|
||||
if canonical == "Host" {
|
||||
return fmt.Errorf("header_auths[%d]: Host header cannot be used for auth", i)
|
||||
}
|
||||
if len(h.Value) > maxHeaderValueLen {
|
||||
return fmt.Errorf("header_auths[%d]: value exceeds maximum length of %d", i, maxHeaderValueLen)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
maxCIDREntries = 200
|
||||
maxCountryEntries = 50
|
||||
)
|
||||
|
||||
// validateAccessRestrictions validates and normalizes access restriction
|
||||
// entries. Country codes are uppercased in place.
|
||||
func validateAccessRestrictions(r *AccessRestrictions) error {
|
||||
if len(r.AllowedCIDRs) > maxCIDREntries {
|
||||
return fmt.Errorf("allowed_cidrs: exceeds maximum of %d entries", maxCIDREntries)
|
||||
}
|
||||
if len(r.BlockedCIDRs) > maxCIDREntries {
|
||||
return fmt.Errorf("blocked_cidrs: exceeds maximum of %d entries", maxCIDREntries)
|
||||
}
|
||||
if len(r.AllowedCountries) > maxCountryEntries {
|
||||
return fmt.Errorf("allowed_countries: exceeds maximum of %d entries", maxCountryEntries)
|
||||
}
|
||||
if len(r.BlockedCountries) > maxCountryEntries {
|
||||
return fmt.Errorf("blocked_countries: exceeds maximum of %d entries", maxCountryEntries)
|
||||
}
|
||||
|
||||
for i, raw := range r.AllowedCIDRs {
|
||||
prefix, err := netip.ParsePrefix(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("allowed_cidrs[%d]: %w", i, err)
|
||||
}
|
||||
if prefix != prefix.Masked() {
|
||||
return fmt.Errorf("allowed_cidrs[%d]: %q has host bits set, use %s instead", i, raw, prefix.Masked())
|
||||
}
|
||||
}
|
||||
for i, raw := range r.BlockedCIDRs {
|
||||
prefix, err := netip.ParsePrefix(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("blocked_cidrs[%d]: %w", i, err)
|
||||
}
|
||||
if prefix != prefix.Masked() {
|
||||
return fmt.Errorf("blocked_cidrs[%d]: %q has host bits set, use %s instead", i, raw, prefix.Masked())
|
||||
}
|
||||
}
|
||||
for i, code := range r.AllowedCountries {
|
||||
if len(code) != 2 {
|
||||
return fmt.Errorf("allowed_countries[%d]: %q must be a 2-letter ISO 3166-1 alpha-2 code", i, code)
|
||||
}
|
||||
r.AllowedCountries[i] = strings.ToUpper(code)
|
||||
}
|
||||
for i, code := range r.BlockedCountries {
|
||||
if len(code) != 2 {
|
||||
return fmt.Errorf("blocked_countries[%d]: %q must be a 2-letter ISO 3166-1 alpha-2 code", i, code)
|
||||
}
|
||||
r.BlockedCountries[i] = strings.ToUpper(code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) EventMeta() map[string]any {
|
||||
meta := map[string]any{
|
||||
"name": s.Name,
|
||||
@@ -827,9 +1061,17 @@ func (s *Service) EventMeta() map[string]any {
|
||||
}
|
||||
|
||||
func (s *Service) isAuthEnabled() bool {
|
||||
return (s.Auth.PasswordAuth != nil && s.Auth.PasswordAuth.Enabled) ||
|
||||
if (s.Auth.PasswordAuth != nil && s.Auth.PasswordAuth.Enabled) ||
|
||||
(s.Auth.PinAuth != nil && s.Auth.PinAuth.Enabled) ||
|
||||
(s.Auth.BearerAuth != nil && s.Auth.BearerAuth.Enabled)
|
||||
(s.Auth.BearerAuth != nil && s.Auth.BearerAuth.Enabled) {
|
||||
return true
|
||||
}
|
||||
for _, h := range s.Auth.HeaderAuths {
|
||||
if h != nil && h.Enabled {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) Copy() *Service {
|
||||
@@ -866,6 +1108,16 @@ func (s *Service) Copy() *Service {
|
||||
}
|
||||
authCopy.BearerAuth = &ba
|
||||
}
|
||||
if len(s.Auth.HeaderAuths) > 0 {
|
||||
authCopy.HeaderAuths = make([]*HeaderAuthConfig, len(s.Auth.HeaderAuths))
|
||||
for i, h := range s.Auth.HeaderAuths {
|
||||
if h == nil {
|
||||
continue
|
||||
}
|
||||
hCopy := *h
|
||||
authCopy.HeaderAuths[i] = &hCopy
|
||||
}
|
||||
}
|
||||
|
||||
return &Service{
|
||||
ID: s.ID,
|
||||
@@ -875,9 +1127,11 @@ func (s *Service) Copy() *Service {
|
||||
ProxyCluster: s.ProxyCluster,
|
||||
Targets: targets,
|
||||
Enabled: s.Enabled,
|
||||
Terminated: s.Terminated,
|
||||
PassHostHeader: s.PassHostHeader,
|
||||
RewriteRedirects: s.RewriteRedirects,
|
||||
Auth: authCopy,
|
||||
Restrictions: s.Restrictions.Copy(),
|
||||
Meta: s.Meta,
|
||||
SessionPrivateKey: s.SessionPrivateKey,
|
||||
SessionPublicKey: s.SessionPublicKey,
|
||||
|
||||
@@ -120,9 +120,9 @@ func TestValidateTargetOptions_RequestTimeout(t *testing.T) {
|
||||
}{
|
||||
{"valid 30s", 30 * time.Second, ""},
|
||||
{"valid 2m", 2 * time.Minute, ""},
|
||||
{"valid 10m", 10 * time.Minute, ""},
|
||||
{"zero is fine", 0, ""},
|
||||
{"negative", -1 * time.Second, "must be positive"},
|
||||
{"exceeds max", 10 * time.Minute, "exceeds maximum"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -847,6 +847,32 @@ func TestValidate_TLSSubnetValid(t *testing.T) {
|
||||
require.NoError(t, rp.Validate())
|
||||
}
|
||||
|
||||
func TestValidate_L4DomainTargetValid(t *testing.T) {
|
||||
modes := []struct {
|
||||
mode string
|
||||
port uint16
|
||||
proto string
|
||||
}{
|
||||
{"tcp", 5432, "tcp"},
|
||||
{"tls", 443, "tcp"},
|
||||
{"udp", 5432, "udp"},
|
||||
}
|
||||
for _, m := range modes {
|
||||
t.Run(m.mode, func(t *testing.T) {
|
||||
rp := &Service{
|
||||
Name: m.mode + "-domain",
|
||||
Mode: m.mode,
|
||||
Domain: "cluster.test",
|
||||
ListenPort: m.port,
|
||||
Targets: []*Target{
|
||||
{TargetId: "resource-1", TargetType: TargetTypeDomain, Protocol: m.proto, Port: m.port, Enabled: true},
|
||||
},
|
||||
}
|
||||
require.NoError(t, rp.Validate())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_HTTPProxyProtocolRejected(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Targets[0].ProxyProtocol = true
|
||||
@@ -909,3 +935,107 @@ func TestExposeServiceRequest_Validate_HTTPAllowsAuth(t *testing.T) {
|
||||
req := ExposeServiceRequest{Port: 8080, Mode: "http", Pin: "123456"}
|
||||
require.NoError(t, req.Validate())
|
||||
}
|
||||
|
||||
func TestValidate_HeaderAuths(t *testing.T) {
|
||||
t.Run("single valid header", func(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Auth = AuthConfig{
|
||||
HeaderAuths: []*HeaderAuthConfig{
|
||||
{Enabled: true, Header: "X-API-Key", Value: "secret"},
|
||||
},
|
||||
}
|
||||
require.NoError(t, rp.Validate())
|
||||
})
|
||||
|
||||
t.Run("multiple headers same canonical name allowed", func(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Auth = AuthConfig{
|
||||
HeaderAuths: []*HeaderAuthConfig{
|
||||
{Enabled: true, Header: "Authorization", Value: "Bearer token-1"},
|
||||
{Enabled: true, Header: "Authorization", Value: "Bearer token-2"},
|
||||
},
|
||||
}
|
||||
require.NoError(t, rp.Validate())
|
||||
})
|
||||
|
||||
t.Run("multiple headers different case same canonical allowed", func(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Auth = AuthConfig{
|
||||
HeaderAuths: []*HeaderAuthConfig{
|
||||
{Enabled: true, Header: "x-api-key", Value: "key-1"},
|
||||
{Enabled: true, Header: "X-Api-Key", Value: "key-2"},
|
||||
},
|
||||
}
|
||||
require.NoError(t, rp.Validate())
|
||||
})
|
||||
|
||||
t.Run("multiple different headers allowed", func(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Auth = AuthConfig{
|
||||
HeaderAuths: []*HeaderAuthConfig{
|
||||
{Enabled: true, Header: "Authorization", Value: "Bearer tok"},
|
||||
{Enabled: true, Header: "X-API-Key", Value: "key"},
|
||||
},
|
||||
}
|
||||
require.NoError(t, rp.Validate())
|
||||
})
|
||||
|
||||
t.Run("empty header name rejected", func(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Auth = AuthConfig{
|
||||
HeaderAuths: []*HeaderAuthConfig{
|
||||
{Enabled: true, Header: "", Value: "val"},
|
||||
},
|
||||
}
|
||||
err := rp.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "header name is required")
|
||||
})
|
||||
|
||||
t.Run("hop-by-hop header rejected", func(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Auth = AuthConfig{
|
||||
HeaderAuths: []*HeaderAuthConfig{
|
||||
{Enabled: true, Header: "Connection", Value: "val"},
|
||||
},
|
||||
}
|
||||
err := rp.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "hop-by-hop")
|
||||
})
|
||||
|
||||
t.Run("host header rejected", func(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Auth = AuthConfig{
|
||||
HeaderAuths: []*HeaderAuthConfig{
|
||||
{Enabled: true, Header: "Host", Value: "val"},
|
||||
},
|
||||
}
|
||||
err := rp.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Host header cannot be used")
|
||||
})
|
||||
|
||||
t.Run("disabled entries skipped", func(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Auth = AuthConfig{
|
||||
HeaderAuths: []*HeaderAuthConfig{
|
||||
{Enabled: false, Header: "", Value: ""},
|
||||
{Enabled: true, Header: "X-Key", Value: "val"},
|
||||
},
|
||||
}
|
||||
require.NoError(t, rp.Validate())
|
||||
})
|
||||
|
||||
t.Run("value too long rejected", func(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Auth = AuthConfig{
|
||||
HeaderAuths: []*HeaderAuthConfig{
|
||||
{Enabled: true, Header: "X-Key", Value: strings.Repeat("a", maxHeaderValueLen+1)},
|
||||
},
|
||||
}
|
||||
err := rp.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "exceeds maximum length")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ func (s *BaseServer) RecordsManager() records.Manager {
|
||||
|
||||
func (s *BaseServer) ServiceManager() service.Manager {
|
||||
return Create(s, func() service.Manager {
|
||||
return nbreverseproxy.NewManager(s.Store(), s.AccountManager(), s.PermissionsManager(), s.ServiceProxyController(), s.ReverseProxyDomainManager())
|
||||
return nbreverseproxy.NewManager(s.Store(), s.AccountManager(), s.PermissionsManager(), s.ServiceProxyController(), s.ProxyManager(), s.ReverseProxyDomainManager())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -212,9 +212,6 @@ func (s *BaseServer) ProxyManager() proxy.Manager {
|
||||
func (s *BaseServer) ReverseProxyDomainManager() *manager.Manager {
|
||||
return Create(s, func() *manager.Manager {
|
||||
m := manager.NewManager(s.Store(), s.ProxyManager(), s.PermissionsManager(), s.AccountManager())
|
||||
s.AfterInit(func(s *BaseServer) {
|
||||
m.SetClusterCapabilities(s.ServiceProxyController())
|
||||
})
|
||||
return &m
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -122,7 +123,7 @@ func (s *ProxyServiceServer) cleanupStaleProxies(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := s.proxyManager.CleanupStale(ctx, 10*time.Minute); err != nil {
|
||||
if err := s.proxyManager.CleanupStale(ctx, 1*time.Hour); err != nil {
|
||||
log.WithContext(ctx).Debugf("Failed to cleanup stale proxies: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -181,9 +182,21 @@ func (s *ProxyServiceServer) GetMappingUpdate(req *proto.GetMappingUpdateRequest
|
||||
log.WithContext(ctx).Warnf("Failed to register proxy %s in cluster: %v", proxyID, err)
|
||||
}
|
||||
|
||||
// Register proxy in database
|
||||
if err := s.proxyManager.Connect(ctx, proxyID, proxyAddress, peerInfo); err != nil {
|
||||
log.WithContext(ctx).Warnf("Failed to register proxy %s in database: %v", proxyID, err)
|
||||
// Register proxy in database with capabilities
|
||||
var caps *proxy.Capabilities
|
||||
if c := req.GetCapabilities(); c != nil {
|
||||
caps = &proxy.Capabilities{
|
||||
SupportsCustomPorts: c.SupportsCustomPorts,
|
||||
RequireSubdomain: c.RequireSubdomain,
|
||||
}
|
||||
}
|
||||
if err := s.proxyManager.Connect(ctx, proxyID, proxyAddress, peerInfo, caps); err != nil {
|
||||
log.WithContext(ctx).Warnf("failed to register proxy %s in database: %v", proxyID, err)
|
||||
s.connectedProxies.Delete(proxyID)
|
||||
if unregErr := s.proxyController.UnregisterProxyFromCluster(ctx, conn.address, proxyID); unregErr != nil {
|
||||
log.WithContext(ctx).Debugf("cleanup after Connect failure for proxy %s: %v", proxyID, unregErr)
|
||||
}
|
||||
return status.Errorf(codes.Internal, "register proxy in database: %v", err)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
@@ -214,7 +227,7 @@ func (s *ProxyServiceServer) GetMappingUpdate(req *proto.GetMappingUpdateRequest
|
||||
go s.sender(conn, errChan)
|
||||
|
||||
// Start heartbeat goroutine
|
||||
go s.heartbeat(connCtx, proxyID)
|
||||
go s.heartbeat(connCtx, proxyID, proxyAddress, peerInfo)
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
@@ -225,14 +238,14 @@ func (s *ProxyServiceServer) GetMappingUpdate(req *proto.GetMappingUpdateRequest
|
||||
}
|
||||
|
||||
// heartbeat updates the proxy's last_seen timestamp every minute
|
||||
func (s *ProxyServiceServer) heartbeat(ctx context.Context, proxyID string) {
|
||||
func (s *ProxyServiceServer) heartbeat(ctx context.Context, proxyID, clusterAddress, ipAddress string) {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := s.proxyManager.Heartbeat(ctx, proxyID); err != nil {
|
||||
if err := s.proxyManager.Heartbeat(ctx, proxyID, clusterAddress, ipAddress); err != nil {
|
||||
log.WithContext(ctx).Debugf("Failed to update proxy %s heartbeat: %v", proxyID, err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
@@ -296,6 +309,9 @@ func (s *ProxyServiceServer) snapshotServiceMappings(ctx context.Context, conn *
|
||||
}
|
||||
|
||||
m := service.ToProtoMapping(rpservice.Create, token, s.GetOIDCValidationConfig())
|
||||
if !proxyAcceptsMapping(conn, m) {
|
||||
continue
|
||||
}
|
||||
mappings = append(mappings, m)
|
||||
}
|
||||
return mappings, nil
|
||||
@@ -444,22 +460,46 @@ func (s *ProxyServiceServer) SendServiceUpdateToCluster(ctx context.Context, upd
|
||||
|
||||
log.Debugf("Sending service update to cluster %s", clusterAddr)
|
||||
for _, proxyID := range proxyIDs {
|
||||
if connVal, ok := s.connectedProxies.Load(proxyID); ok {
|
||||
conn := connVal.(*proxyConnection)
|
||||
msg := s.perProxyMessage(updateResponse, proxyID)
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case conn.sendChan <- msg:
|
||||
log.WithContext(ctx).Debugf("Sent service update with id %s to proxy %s in cluster %s", update.Id, proxyID, clusterAddr)
|
||||
default:
|
||||
log.WithContext(ctx).Warnf("Failed to send service update to proxy %s in cluster %s (channel full)", proxyID, clusterAddr)
|
||||
}
|
||||
connVal, ok := s.connectedProxies.Load(proxyID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
conn := connVal.(*proxyConnection)
|
||||
if !proxyAcceptsMapping(conn, update) {
|
||||
log.WithContext(ctx).Debugf("Skipping proxy %s: does not support custom ports for mapping %s", proxyID, update.Id)
|
||||
continue
|
||||
}
|
||||
msg := s.perProxyMessage(updateResponse, proxyID)
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case conn.sendChan <- msg:
|
||||
log.WithContext(ctx).Debugf("Sent service update with id %s to proxy %s in cluster %s", update.Id, proxyID, clusterAddr)
|
||||
default:
|
||||
log.WithContext(ctx).Warnf("Failed to send service update to proxy %s in cluster %s (channel full)", proxyID, clusterAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// proxyAcceptsMapping returns whether the proxy should receive this mapping.
|
||||
// Old proxies that never reported capabilities are skipped for non-TLS L4
|
||||
// mappings with a custom listen port, since they don't understand the
|
||||
// protocol. Proxies that report capabilities (even SupportsCustomPorts=false)
|
||||
// are new enough to handle the mapping. TLS uses SNI routing and works on
|
||||
// any proxy. Delete operations are always sent so proxies can clean up.
|
||||
func proxyAcceptsMapping(conn *proxyConnection, mapping *proto.ProxyMapping) bool {
|
||||
if mapping.Type == proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED {
|
||||
return true
|
||||
}
|
||||
if mapping.ListenPort == 0 || mapping.Mode == "tls" {
|
||||
return true
|
||||
}
|
||||
// Old proxies that never reported capabilities don't understand
|
||||
// custom port mappings.
|
||||
return conn.capabilities != nil && conn.capabilities.SupportsCustomPorts != nil
|
||||
}
|
||||
|
||||
// perProxyMessage returns a copy of update with a fresh one-time token for
|
||||
// create/update operations. For delete operations the original mapping is
|
||||
// used unchanged because proxies do not need to authenticate for removal.
|
||||
@@ -493,48 +533,20 @@ func (s *ProxyServiceServer) perProxyMessage(update *proto.GetMappingUpdateRespo
|
||||
// should be set on the copy.
|
||||
func shallowCloneMapping(m *proto.ProxyMapping) *proto.ProxyMapping {
|
||||
return &proto.ProxyMapping{
|
||||
Type: m.Type,
|
||||
Id: m.Id,
|
||||
AccountId: m.AccountId,
|
||||
Domain: m.Domain,
|
||||
Path: m.Path,
|
||||
Auth: m.Auth,
|
||||
PassHostHeader: m.PassHostHeader,
|
||||
RewriteRedirects: m.RewriteRedirects,
|
||||
Mode: m.Mode,
|
||||
ListenPort: m.ListenPort,
|
||||
Type: m.Type,
|
||||
Id: m.Id,
|
||||
AccountId: m.AccountId,
|
||||
Domain: m.Domain,
|
||||
Path: m.Path,
|
||||
Auth: m.Auth,
|
||||
PassHostHeader: m.PassHostHeader,
|
||||
RewriteRedirects: m.RewriteRedirects,
|
||||
Mode: m.Mode,
|
||||
ListenPort: m.ListenPort,
|
||||
AccessRestrictions: m.AccessRestrictions,
|
||||
}
|
||||
}
|
||||
|
||||
// ClusterSupportsCustomPorts returns whether any connected proxy in the given
|
||||
// cluster reports custom port support. Returns nil if no proxy has reported
|
||||
// capabilities (old proxies that predate the field).
|
||||
func (s *ProxyServiceServer) ClusterSupportsCustomPorts(clusterAddr string) *bool {
|
||||
if s.proxyController == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hasCapabilities bool
|
||||
for _, pid := range s.proxyController.GetProxiesForCluster(clusterAddr) {
|
||||
connVal, ok := s.connectedProxies.Load(pid)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
conn := connVal.(*proxyConnection)
|
||||
if conn.capabilities == nil || conn.capabilities.SupportsCustomPorts == nil {
|
||||
continue
|
||||
}
|
||||
if *conn.capabilities.SupportsCustomPorts {
|
||||
return ptr(true)
|
||||
}
|
||||
hasCapabilities = true
|
||||
}
|
||||
if hasCapabilities {
|
||||
return ptr(false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ProxyServiceServer) Authenticate(ctx context.Context, req *proto.AuthenticateRequest) (*proto.AuthenticateResponse, error) {
|
||||
service, err := s.serviceManager.GetServiceByID(ctx, req.GetAccountId(), req.GetId())
|
||||
if err != nil {
|
||||
@@ -561,6 +573,8 @@ func (s *ProxyServiceServer) authenticateRequest(ctx context.Context, req *proto
|
||||
return s.authenticatePIN(ctx, req.GetId(), v, service.Auth.PinAuth)
|
||||
case *proto.AuthenticateRequest_Password:
|
||||
return s.authenticatePassword(ctx, req.GetId(), v, service.Auth.PasswordAuth)
|
||||
case *proto.AuthenticateRequest_HeaderAuth:
|
||||
return s.authenticateHeader(ctx, req.GetId(), v, service.Auth.HeaderAuths)
|
||||
default:
|
||||
return false, "", ""
|
||||
}
|
||||
@@ -594,6 +608,35 @@ func (s *ProxyServiceServer) authenticatePassword(ctx context.Context, serviceID
|
||||
return true, "password-user", proxyauth.MethodPassword
|
||||
}
|
||||
|
||||
func (s *ProxyServiceServer) authenticateHeader(ctx context.Context, serviceID string, req *proto.AuthenticateRequest_HeaderAuth, auths []*rpservice.HeaderAuthConfig) (bool, string, proxyauth.Method) {
|
||||
if len(auths) == 0 {
|
||||
log.WithContext(ctx).Debugf("header authentication attempted but no header auths configured for service %s", serviceID)
|
||||
return false, "", ""
|
||||
}
|
||||
|
||||
headerName := http.CanonicalHeaderKey(req.HeaderAuth.GetHeaderName())
|
||||
|
||||
var lastErr error
|
||||
for _, auth := range auths {
|
||||
if auth == nil || !auth.Enabled {
|
||||
continue
|
||||
}
|
||||
if headerName != "" && http.CanonicalHeaderKey(auth.Header) != headerName {
|
||||
continue
|
||||
}
|
||||
if err := argon2id.Verify(req.HeaderAuth.GetHeaderValue(), auth.Value); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return true, "header-user", proxyauth.MethodHeader
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
s.logAuthenticationError(ctx, lastErr, "Header")
|
||||
}
|
||||
return false, "", ""
|
||||
}
|
||||
|
||||
func (s *ProxyServiceServer) logAuthenticationError(ctx context.Context, err error, authType string) {
|
||||
if errors.Is(err, argon2id.ErrMismatchedHashAndPassword) {
|
||||
log.WithContext(ctx).Tracef("%s authentication failed: invalid credentials", authType)
|
||||
@@ -752,6 +795,9 @@ func (s *ProxyServiceServer) GetOIDCURL(ctx context.Context, req *proto.GetOIDCU
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "parse redirect url: %v", err)
|
||||
}
|
||||
if redirectURL.Scheme != "https" && redirectURL.Scheme != "http" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "redirect URL must use http or https scheme")
|
||||
}
|
||||
// Validate redirectURL against known service endpoints to avoid abuse of OIDC redirection.
|
||||
services, err := s.serviceManager.GetAccountServices(ctx, req.GetAccountId())
|
||||
if err != nil {
|
||||
@@ -836,12 +882,9 @@ func (s *ProxyServiceServer) generateHMAC(input string) string {
|
||||
|
||||
// ValidateState validates the state parameter from an OAuth callback.
|
||||
// Returns the original redirect URL if valid, or an error if invalid.
|
||||
// The HMAC is verified before consuming the PKCE verifier to prevent
|
||||
// an attacker from invalidating a legitimate user's auth flow.
|
||||
func (s *ProxyServiceServer) ValidateState(state string) (verifier, redirectURL string, err error) {
|
||||
verifier, ok := s.pkceVerifierStore.LoadAndDelete(state)
|
||||
if !ok {
|
||||
return "", "", errors.New("no verifier for state")
|
||||
}
|
||||
|
||||
// State format: base64(redirectURL)|nonce|hmac(redirectURL|nonce)
|
||||
parts := strings.Split(state, "|")
|
||||
if len(parts) != 3 {
|
||||
@@ -865,6 +908,12 @@ func (s *ProxyServiceServer) ValidateState(state string) (verifier, redirectURL
|
||||
return "", "", errors.New("invalid state signature")
|
||||
}
|
||||
|
||||
// Consume the PKCE verifier only after HMAC validation passes.
|
||||
verifier, ok := s.pkceVerifierStore.LoadAndDelete(state)
|
||||
if !ok {
|
||||
return "", "", errors.New("no verifier for state")
|
||||
}
|
||||
|
||||
return verifier, redirectURL, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
@@ -90,6 +91,10 @@ func (m *mockReverseProxyManager) StopServiceFromPeer(_ context.Context, _, _, _
|
||||
|
||||
func (m *mockReverseProxyManager) StartExposeReaper(_ context.Context) {}
|
||||
|
||||
func (m *mockReverseProxyManager) GetActiveClusters(_ context.Context, _, _ string) ([]proxy.Cluster, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type mockUsersManager struct {
|
||||
users map[string]*types.User
|
||||
err error
|
||||
|
||||
@@ -53,10 +53,6 @@ func (c *testProxyController) UnregisterProxyFromCluster(_ context.Context, clus
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *testProxyController) ClusterSupportsCustomPorts(_ string) *bool {
|
||||
return ptr(true)
|
||||
}
|
||||
|
||||
func (c *testProxyController) GetProxiesForCluster(clusterAddr string) []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -351,14 +347,14 @@ func TestSendServiceUpdateToCluster_FiltersOnCapability(t *testing.T) {
|
||||
|
||||
const cluster = "proxy.example.com"
|
||||
|
||||
// Proxy A supports custom ports.
|
||||
chA := registerFakeProxyWithCaps(s, "proxy-a", cluster, &proto.ProxyCapabilities{SupportsCustomPorts: ptr(true)})
|
||||
// Proxy B does NOT support custom ports (shared cloud proxy).
|
||||
chB := registerFakeProxyWithCaps(s, "proxy-b", cluster, &proto.ProxyCapabilities{SupportsCustomPorts: ptr(false)})
|
||||
// Modern proxy reports capabilities.
|
||||
chModern := registerFakeProxyWithCaps(s, "proxy-modern", cluster, &proto.ProxyCapabilities{SupportsCustomPorts: ptr(true)})
|
||||
// Legacy proxy never reported capabilities (nil).
|
||||
chLegacy := registerFakeProxy(s, "proxy-legacy", cluster)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// TLS passthrough works on all proxies regardless of custom port support.
|
||||
// TLS passthrough with custom port: all proxies receive it (SNI routing).
|
||||
tlsMapping := &proto.ProxyMapping{
|
||||
Type: proto.ProxyMappingUpdateType_UPDATE_TYPE_CREATED,
|
||||
Id: "service-tls",
|
||||
@@ -371,12 +367,26 @@ func TestSendServiceUpdateToCluster_FiltersOnCapability(t *testing.T) {
|
||||
|
||||
s.SendServiceUpdateToCluster(ctx, tlsMapping, cluster)
|
||||
|
||||
msgA := drainMapping(chA)
|
||||
msgB := drainMapping(chB)
|
||||
assert.NotNil(t, msgA, "proxy-a should receive TLS mapping")
|
||||
assert.NotNil(t, msgB, "proxy-b should receive TLS mapping (passthrough works on all proxies)")
|
||||
assert.NotNil(t, drainMapping(chModern), "modern proxy should receive TLS mapping")
|
||||
assert.NotNil(t, drainMapping(chLegacy), "legacy proxy should receive TLS mapping (SNI works on all)")
|
||||
|
||||
// Send an HTTP mapping: both should receive it.
|
||||
// TCP mapping with custom port: only modern proxy receives it.
|
||||
tcpMapping := &proto.ProxyMapping{
|
||||
Type: proto.ProxyMappingUpdateType_UPDATE_TYPE_CREATED,
|
||||
Id: "service-tcp",
|
||||
AccountId: "account-1",
|
||||
Domain: "db.example.com",
|
||||
Mode: "tcp",
|
||||
ListenPort: 5432,
|
||||
Path: []*proto.PathMapping{{Target: "10.0.0.5:5432"}},
|
||||
}
|
||||
|
||||
s.SendServiceUpdateToCluster(ctx, tcpMapping, cluster)
|
||||
|
||||
assert.NotNil(t, drainMapping(chModern), "modern proxy should receive TCP custom-port mapping")
|
||||
assert.Nil(t, drainMapping(chLegacy), "legacy proxy should NOT receive TCP custom-port mapping")
|
||||
|
||||
// HTTP mapping (no listen port): both receive it.
|
||||
httpMapping := &proto.ProxyMapping{
|
||||
Type: proto.ProxyMappingUpdateType_UPDATE_TYPE_CREATED,
|
||||
Id: "service-http",
|
||||
@@ -387,10 +397,16 @@ func TestSendServiceUpdateToCluster_FiltersOnCapability(t *testing.T) {
|
||||
|
||||
s.SendServiceUpdateToCluster(ctx, httpMapping, cluster)
|
||||
|
||||
msgA = drainMapping(chA)
|
||||
msgB = drainMapping(chB)
|
||||
assert.NotNil(t, msgA, "proxy-a should receive HTTP mapping")
|
||||
assert.NotNil(t, msgB, "proxy-b should receive HTTP mapping")
|
||||
assert.NotNil(t, drainMapping(chModern), "modern proxy should receive HTTP mapping")
|
||||
assert.NotNil(t, drainMapping(chLegacy), "legacy proxy should receive HTTP mapping")
|
||||
|
||||
// Proxy that reports SupportsCustomPorts=false still receives custom-port
|
||||
// mappings because it understands the protocol (it's new enough).
|
||||
chNewNoCustom := registerFakeProxyWithCaps(s, "proxy-new-no-custom", cluster, &proto.ProxyCapabilities{SupportsCustomPorts: ptr(false)})
|
||||
|
||||
s.SendServiceUpdateToCluster(ctx, tcpMapping, cluster)
|
||||
|
||||
assert.NotNil(t, drainMapping(chNewNoCustom), "new proxy with SupportsCustomPorts=false should still receive mapping")
|
||||
}
|
||||
|
||||
func TestSendServiceUpdateToCluster_TLSNotFiltered(t *testing.T) {
|
||||
@@ -404,7 +420,8 @@ func TestSendServiceUpdateToCluster_TLSNotFiltered(t *testing.T) {
|
||||
|
||||
const cluster = "proxy.example.com"
|
||||
|
||||
chShared := registerFakeProxyWithCaps(s, "proxy-shared", cluster, &proto.ProxyCapabilities{SupportsCustomPorts: ptr(false)})
|
||||
// Legacy proxy (no capabilities) still receives TLS since it uses SNI.
|
||||
chLegacy := registerFakeProxy(s, "proxy-legacy", cluster)
|
||||
|
||||
tlsMapping := &proto.ProxyMapping{
|
||||
Type: proto.ProxyMappingUpdateType_UPDATE_TYPE_CREATED,
|
||||
@@ -417,8 +434,8 @@ func TestSendServiceUpdateToCluster_TLSNotFiltered(t *testing.T) {
|
||||
|
||||
s.SendServiceUpdateToCluster(context.Background(), tlsMapping, cluster)
|
||||
|
||||
msg := drainMapping(chShared)
|
||||
assert.NotNil(t, msg, "shared proxy should receive TLS mapping even without custom port support")
|
||||
msg := drainMapping(chLegacy)
|
||||
assert.NotNil(t, msg, "legacy proxy should receive TLS mapping (SNI works without custom port support)")
|
||||
}
|
||||
|
||||
// TestServiceModifyNotifications exercises every possible modification
|
||||
@@ -585,7 +602,7 @@ func TestServiceModifyNotifications(t *testing.T) {
|
||||
s.SetProxyController(newTestProxyController())
|
||||
const cluster = "proxy.example.com"
|
||||
chModern := registerFakeProxyWithCaps(s, "modern", cluster, &proto.ProxyCapabilities{SupportsCustomPorts: ptr(true)})
|
||||
chLegacy := registerFakeProxyWithCaps(s, "legacy", cluster, &proto.ProxyCapabilities{SupportsCustomPorts: ptr(false)})
|
||||
chLegacy := registerFakeProxy(s, "legacy", cluster)
|
||||
|
||||
// TLS passthrough works on all proxies regardless of custom port support
|
||||
s.SendServiceUpdateToCluster(ctx, tlsOnlyMapping(proto.ProxyMappingUpdateType_UPDATE_TYPE_MODIFIED), cluster)
|
||||
@@ -604,7 +621,7 @@ func TestServiceModifyNotifications(t *testing.T) {
|
||||
}
|
||||
s.SetProxyController(newTestProxyController())
|
||||
const cluster = "proxy.example.com"
|
||||
chLegacy := registerFakeProxyWithCaps(s, "legacy", cluster, &proto.ProxyCapabilities{SupportsCustomPorts: ptr(false)})
|
||||
chLegacy := registerFakeProxy(s, "legacy", cluster)
|
||||
|
||||
mapping := tlsOnlyMapping(proto.ProxyMappingUpdateType_UPDATE_TYPE_MODIFIED)
|
||||
mapping.ListenPort = 0 // default port
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/sessionkey"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
@@ -320,6 +321,10 @@ func (m *testValidateSessionServiceManager) StopServiceFromPeer(_ context.Contex
|
||||
|
||||
func (m *testValidateSessionServiceManager) StartExposeReaper(_ context.Context) {}
|
||||
|
||||
func (m *testValidateSessionServiceManager) GetActiveClusters(_ context.Context, _, _ string) ([]proxy.Cluster, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type testValidateSessionProxyManager struct{}
|
||||
|
||||
func (m *testValidateSessionProxyManager) Connect(_ context.Context, _, _, _ string) error {
|
||||
@@ -338,6 +343,10 @@ func (m *testValidateSessionProxyManager) GetActiveClusterAddresses(_ context.Co
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionProxyManager) GetActiveClusters(_ context.Context) ([]proxy.Cluster, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionProxyManager) CleanupStale(_ context.Context, _ time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3138,7 +3138,7 @@ func createManager(t testing.TB) (*DefaultAccountManager, *update_channel.PeersU
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
manager.SetServiceManager(reverseproxymanager.NewManager(store, manager, permissionsManager, proxyController, nil))
|
||||
manager.SetServiceManager(reverseproxymanager.NewManager(store, manager, permissionsManager, proxyController, proxyManager, nil))
|
||||
|
||||
return manager, updateManager, nil
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ type Record struct {
|
||||
GeonameID uint `maxminddb:"geoname_id"`
|
||||
ISOCode string `maxminddb:"iso_code"`
|
||||
} `maxminddb:"country"`
|
||||
Subdivisions []struct {
|
||||
ISOCode string `maxminddb:"iso_code"`
|
||||
Names struct {
|
||||
En string `maxminddb:"en"`
|
||||
} `maxminddb:"names"`
|
||||
} `maxminddb:"subdivisions"`
|
||||
}
|
||||
|
||||
type City struct {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
|
||||
nbproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
@@ -433,6 +434,10 @@ func (m *testServiceManager) StopServiceFromPeer(_ context.Context, _, _, _ stri
|
||||
|
||||
func (m *testServiceManager) StartExposeReaper(_ context.Context) {}
|
||||
|
||||
func (m *testServiceManager) GetActiveClusters(_ context.Context, _, _ string) ([]nbproxy.Cluster, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func createTestState(t *testing.T, ps *nbgrpc.ProxyServiceServer, redirectURL string) string {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
func Test_Accounts_GetAll(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, true},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all accounts", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/accounts.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/accounts", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.Account{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(got))
|
||||
account := got[0]
|
||||
assert.Equal(t, "test.com", account.Domain)
|
||||
assert.Equal(t, "private", account.DomainCategory)
|
||||
assert.Equal(t, true, account.Settings.PeerLoginExpirationEnabled)
|
||||
assert.Equal(t, 86400, account.Settings.PeerLoginExpiration)
|
||||
assert.Equal(t, false, account.Settings.RegularUsersViewBlocked)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Accounts_Update(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestBody *api.AccountRequest
|
||||
verifyResponse func(t *testing.T, account *api.Account)
|
||||
verifyDB func(t *testing.T, account *types.Account)
|
||||
}{
|
||||
{
|
||||
name: "Disable peer login expiration",
|
||||
requestBody: &api.AccountRequest{
|
||||
Settings: api.AccountSettings{
|
||||
PeerLoginExpirationEnabled: false,
|
||||
PeerLoginExpiration: 86400,
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, account *api.Account) {
|
||||
t.Helper()
|
||||
assert.Equal(t, false, account.Settings.PeerLoginExpirationEnabled)
|
||||
},
|
||||
verifyDB: func(t *testing.T, dbAccount *types.Account) {
|
||||
t.Helper()
|
||||
assert.Equal(t, false, dbAccount.Settings.PeerLoginExpirationEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update peer login expiration to 48h",
|
||||
requestBody: &api.AccountRequest{
|
||||
Settings: api.AccountSettings{
|
||||
PeerLoginExpirationEnabled: true,
|
||||
PeerLoginExpiration: 172800,
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, account *api.Account) {
|
||||
t.Helper()
|
||||
assert.Equal(t, 172800, account.Settings.PeerLoginExpiration)
|
||||
},
|
||||
verifyDB: func(t *testing.T, dbAccount *types.Account) {
|
||||
t.Helper()
|
||||
assert.Equal(t, 172800*time.Second, dbAccount.Settings.PeerLoginExpiration)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Enable regular users view blocked",
|
||||
requestBody: &api.AccountRequest{
|
||||
Settings: api.AccountSettings{
|
||||
PeerLoginExpirationEnabled: true,
|
||||
PeerLoginExpiration: 86400,
|
||||
RegularUsersViewBlocked: true,
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, account *api.Account) {
|
||||
t.Helper()
|
||||
assert.Equal(t, true, account.Settings.RegularUsersViewBlocked)
|
||||
},
|
||||
verifyDB: func(t *testing.T, dbAccount *types.Account) {
|
||||
t.Helper()
|
||||
assert.Equal(t, true, dbAccount.Settings.RegularUsersViewBlocked)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Enable groups propagation",
|
||||
requestBody: &api.AccountRequest{
|
||||
Settings: api.AccountSettings{
|
||||
PeerLoginExpirationEnabled: true,
|
||||
PeerLoginExpiration: 86400,
|
||||
GroupsPropagationEnabled: &trueVal,
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, account *api.Account) {
|
||||
t.Helper()
|
||||
assert.NotNil(t, account.Settings.GroupsPropagationEnabled)
|
||||
assert.Equal(t, true, *account.Settings.GroupsPropagationEnabled)
|
||||
},
|
||||
verifyDB: func(t *testing.T, dbAccount *types.Account) {
|
||||
t.Helper()
|
||||
assert.Equal(t, true, dbAccount.Settings.GroupsPropagationEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Enable JWT groups",
|
||||
requestBody: &api.AccountRequest{
|
||||
Settings: api.AccountSettings{
|
||||
PeerLoginExpirationEnabled: true,
|
||||
PeerLoginExpiration: 86400,
|
||||
GroupsPropagationEnabled: &falseVal,
|
||||
JwtGroupsEnabled: &trueVal,
|
||||
JwtGroupsClaimName: stringPointer("groups"),
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, account *api.Account) {
|
||||
t.Helper()
|
||||
assert.NotNil(t, account.Settings.JwtGroupsEnabled)
|
||||
assert.Equal(t, true, *account.Settings.JwtGroupsEnabled)
|
||||
assert.NotNil(t, account.Settings.JwtGroupsClaimName)
|
||||
assert.Equal(t, "groups", *account.Settings.JwtGroupsClaimName)
|
||||
},
|
||||
verifyDB: func(t *testing.T, dbAccount *types.Account) {
|
||||
t.Helper()
|
||||
assert.Equal(t, true, dbAccount.Settings.JWTGroupsEnabled)
|
||||
assert.Equal(t, "groups", dbAccount.Settings.JWTGroupsClaimName)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/accounts.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/accounts/{accountId}", "{accountId}", testing_tools.TestAccountId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := &api.Account{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, testing_tools.TestAccountId, got.Id)
|
||||
assert.Equal(t, "test.com", got.Domain)
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbAccount := testing_tools.VerifyAccountSettings(t, db)
|
||||
tc.verifyDB(t, dbAccount)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stringPointer(s string) *string {
|
||||
return &s
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
func Test_Nameservers_GetAll(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all nameservers", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/dns/nameservers", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.NameserverGroup{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(got))
|
||||
assert.Equal(t, "testNSGroup", got[0].Name)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Nameservers_GetById(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
nsGroupId string
|
||||
expectedStatus int
|
||||
expectGroup bool
|
||||
}{
|
||||
{
|
||||
name: "Get existing nameserver group",
|
||||
nsGroupId: "testNSGroupId",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectGroup: true,
|
||||
},
|
||||
{
|
||||
name: "Get non-existing nameserver group",
|
||||
nsGroupId: "nonExistingNSGroupId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectGroup: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/dns/nameservers/{nsgroupId}", "{nsgroupId}", tc.nsGroupId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.expectGroup {
|
||||
got := &api.NameserverGroup{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
assert.Equal(t, "testNSGroupId", got.Id)
|
||||
assert.Equal(t, "testNSGroup", got.Name)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Nameservers_Create(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
requestBody *api.PostApiDnsNameserversJSONRequestBody
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, nsGroup *api.NameserverGroup)
|
||||
}{
|
||||
{
|
||||
name: "Create nameserver group with single NS",
|
||||
requestBody: &api.PostApiDnsNameserversJSONRequestBody{
|
||||
Name: "newNSGroup",
|
||||
Description: "a new nameserver group",
|
||||
Nameservers: []api.Nameserver{
|
||||
{Ip: "8.8.8.8", NsType: "udp", Port: 53},
|
||||
},
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
Primary: false,
|
||||
Domains: []string{"test.com"},
|
||||
Enabled: true,
|
||||
SearchDomainsEnabled: false,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, nsGroup *api.NameserverGroup) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, nsGroup.Id)
|
||||
assert.Equal(t, "newNSGroup", nsGroup.Name)
|
||||
assert.Equal(t, 1, len(nsGroup.Nameservers))
|
||||
assert.Equal(t, false, nsGroup.Primary)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create primary nameserver group",
|
||||
requestBody: &api.PostApiDnsNameserversJSONRequestBody{
|
||||
Name: "primaryNS",
|
||||
Description: "primary nameserver",
|
||||
Nameservers: []api.Nameserver{
|
||||
{Ip: "1.1.1.1", NsType: "udp", Port: 53},
|
||||
},
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
Primary: true,
|
||||
Domains: []string{},
|
||||
Enabled: true,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, nsGroup *api.NameserverGroup) {
|
||||
t.Helper()
|
||||
assert.Equal(t, true, nsGroup.Primary)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create nameserver group with empty groups",
|
||||
requestBody: &api.PostApiDnsNameserversJSONRequestBody{
|
||||
Name: "emptyGroupsNS",
|
||||
Description: "no groups",
|
||||
Nameservers: []api.Nameserver{
|
||||
{Ip: "8.8.8.8", NsType: "udp", Port: 53},
|
||||
},
|
||||
Groups: []string{},
|
||||
Primary: true,
|
||||
Domains: []string{},
|
||||
Enabled: true,
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/dns/nameservers", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.NameserverGroup{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify the created NS group directly in the DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbNS := testing_tools.VerifyNSGroupInDB(t, db, got.Id)
|
||||
assert.Equal(t, got.Name, dbNS.Name)
|
||||
assert.Equal(t, got.Primary, dbNS.Primary)
|
||||
assert.Equal(t, len(got.Nameservers), len(dbNS.NameServers))
|
||||
assert.Equal(t, got.Enabled, dbNS.Enabled)
|
||||
assert.Equal(t, got.SearchDomainsEnabled, dbNS.SearchDomainsEnabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Nameservers_Update(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
nsGroupId string
|
||||
requestBody *api.PutApiDnsNameserversNsgroupIdJSONRequestBody
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, nsGroup *api.NameserverGroup)
|
||||
}{
|
||||
{
|
||||
name: "Update nameserver group name",
|
||||
nsGroupId: "testNSGroupId",
|
||||
requestBody: &api.PutApiDnsNameserversNsgroupIdJSONRequestBody{
|
||||
Name: "updatedNSGroup",
|
||||
Description: "updated description",
|
||||
Nameservers: []api.Nameserver{
|
||||
{Ip: "1.1.1.1", NsType: "udp", Port: 53},
|
||||
},
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
Primary: false,
|
||||
Domains: []string{"example.com"},
|
||||
Enabled: true,
|
||||
SearchDomainsEnabled: false,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, nsGroup *api.NameserverGroup) {
|
||||
t.Helper()
|
||||
assert.Equal(t, "updatedNSGroup", nsGroup.Name)
|
||||
assert.Equal(t, "updated description", nsGroup.Description)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update non-existing nameserver group",
|
||||
nsGroupId: "nonExistingNSGroupId",
|
||||
requestBody: &api.PutApiDnsNameserversNsgroupIdJSONRequestBody{
|
||||
Name: "whatever",
|
||||
Nameservers: []api.Nameserver{
|
||||
{Ip: "1.1.1.1", NsType: "udp", Port: 53},
|
||||
},
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
Primary: true,
|
||||
Domains: []string{},
|
||||
Enabled: true,
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/dns/nameservers/{nsgroupId}", "{nsgroupId}", tc.nsGroupId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.NameserverGroup{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify the updated NS group directly in the DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbNS := testing_tools.VerifyNSGroupInDB(t, db, tc.nsGroupId)
|
||||
assert.Equal(t, "updatedNSGroup", dbNS.Name)
|
||||
assert.Equal(t, "updated description", dbNS.Description)
|
||||
assert.Equal(t, false, dbNS.Primary)
|
||||
assert.Equal(t, true, dbNS.Enabled)
|
||||
assert.Equal(t, 1, len(dbNS.NameServers))
|
||||
assert.Equal(t, false, dbNS.SearchDomainsEnabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Nameservers_Delete(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
nsGroupId string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Delete existing nameserver group",
|
||||
nsGroupId: "testNSGroupId",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Delete non-existing nameserver group",
|
||||
nsGroupId: "nonExistingNSGroupId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, false)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/dns/nameservers/{nsgroupId}", "{nsgroupId}", tc.nsGroupId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
|
||||
// Verify deletion in DB for successful deletes by privileged users
|
||||
if tc.expectedStatus == http.StatusOK && user.expectResponse {
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
testing_tools.VerifyNSGroupNotInDB(t, db, tc.nsGroupId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DnsSettings_Get(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get DNS settings", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/dns/settings", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := &api.DNSSettings{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.NotNil(t, got.DisabledManagementGroups)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DnsSettings_Update(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
requestBody *api.PutApiDnsSettingsJSONRequestBody
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, settings *api.DNSSettings)
|
||||
expectedDBDisabledMgmtLen int
|
||||
expectedDBDisabledMgmtItem string
|
||||
}{
|
||||
{
|
||||
name: "Update disabled management groups",
|
||||
requestBody: &api.PutApiDnsSettingsJSONRequestBody{
|
||||
DisabledManagementGroups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, settings *api.DNSSettings) {
|
||||
t.Helper()
|
||||
assert.Equal(t, 1, len(settings.DisabledManagementGroups))
|
||||
assert.Equal(t, testing_tools.TestGroupId, settings.DisabledManagementGroups[0])
|
||||
},
|
||||
expectedDBDisabledMgmtLen: 1,
|
||||
expectedDBDisabledMgmtItem: testing_tools.TestGroupId,
|
||||
},
|
||||
{
|
||||
name: "Update with empty disabled management groups",
|
||||
requestBody: &api.PutApiDnsSettingsJSONRequestBody{
|
||||
DisabledManagementGroups: []string{},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, settings *api.DNSSettings) {
|
||||
t.Helper()
|
||||
assert.Equal(t, 0, len(settings.DisabledManagementGroups))
|
||||
},
|
||||
expectedDBDisabledMgmtLen: 0,
|
||||
},
|
||||
{
|
||||
name: "Update with non-existing group",
|
||||
requestBody: &api.PutApiDnsSettingsJSONRequestBody{
|
||||
DisabledManagementGroups: []string{"nonExistingGroupId"},
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/dns.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPut, "/api/dns/settings", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.DNSSettings{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify DNS settings directly in the DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbAccount := testing_tools.VerifyAccountSettings(t, db)
|
||||
assert.Equal(t, tc.expectedDBDisabledMgmtLen, len(dbAccount.DNSSettings.DisabledManagementGroups))
|
||||
if tc.expectedDBDisabledMgmtItem != "" {
|
||||
assert.Contains(t, dbAccount.DNSSettings.DisabledManagementGroups, tc.expectedDBDisabledMgmtItem)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
func Test_Events_GetAll(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all events", func(t *testing.T) {
|
||||
apiHandler, _, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/events.sql", nil, false)
|
||||
|
||||
// First, perform a mutation to generate an event (create a group as admin)
|
||||
groupBody, err := json.Marshal(&api.GroupRequest{Name: "eventTestGroup"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal group request: %v", err)
|
||||
}
|
||||
createReq := testing_tools.BuildRequest(t, groupBody, http.MethodPost, "/api/groups", testing_tools.TestAdminId)
|
||||
createRecorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(createRecorder, createReq)
|
||||
assert.Equal(t, http.StatusOK, createRecorder.Code, "Failed to create group to generate event")
|
||||
|
||||
// Now query events
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/events", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.Event{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, len(got), 1, "Expected at least one event after creating a group")
|
||||
|
||||
// Verify the group creation event exists
|
||||
found := false
|
||||
for _, event := range got {
|
||||
if event.ActivityCode == "group.add" {
|
||||
found = true
|
||||
assert.Equal(t, testing_tools.TestAdminId, event.InitiatorId)
|
||||
assert.Equal(t, "Group created", event.Activity)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "Expected to find a group.add event")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Events_GetAll_Empty(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/events.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/events", testing_tools.TestAdminId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, true)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.Event{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(got), "Expected empty events list when no mutations have been performed")
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
func Test_Groups_GetAll(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, true},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all groups", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/groups", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.Group{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, len(got), 2)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Groups_GetById(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, true},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
groupId string
|
||||
expectedStatus int
|
||||
expectGroup bool
|
||||
}{
|
||||
{
|
||||
name: "Get existing group",
|
||||
groupId: testing_tools.TestGroupId,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectGroup: true,
|
||||
},
|
||||
{
|
||||
name: "Get non-existing group",
|
||||
groupId: "nonExistingGroupId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectGroup: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/groups/{groupId}", "{groupId}", tc.groupId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.expectGroup {
|
||||
got := &api.Group{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
assert.Equal(t, tc.groupId, got.Id)
|
||||
assert.Equal(t, "testGroupName", got.Name)
|
||||
assert.Equal(t, 1, got.PeersCount)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Groups_Create(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
requestBody *api.GroupRequest
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, group *api.Group)
|
||||
}{
|
||||
{
|
||||
name: "Create group with valid name",
|
||||
requestBody: &api.GroupRequest{
|
||||
Name: "brandNewGroup",
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, group *api.Group) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, group.Id)
|
||||
assert.Equal(t, "brandNewGroup", group.Name)
|
||||
assert.Equal(t, 0, group.PeersCount)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create group with peers",
|
||||
requestBody: &api.GroupRequest{
|
||||
Name: "groupWithPeers",
|
||||
Peers: &[]string{testing_tools.TestPeerId},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, group *api.Group) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, group.Id)
|
||||
assert.Equal(t, "groupWithPeers", group.Name)
|
||||
assert.Equal(t, 1, group.PeersCount)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create group with empty name",
|
||||
requestBody: &api.GroupRequest{
|
||||
Name: "",
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/groups", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.Group{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify group exists in DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbGroup := testing_tools.VerifyGroupInDB(t, db, got.Id)
|
||||
assert.Equal(t, tc.requestBody.Name, dbGroup.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Groups_Update(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
groupId string
|
||||
requestBody *api.GroupRequest
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, group *api.Group)
|
||||
}{
|
||||
{
|
||||
name: "Update group name",
|
||||
groupId: testing_tools.TestGroupId,
|
||||
requestBody: &api.GroupRequest{
|
||||
Name: "updatedGroupName",
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, group *api.Group) {
|
||||
t.Helper()
|
||||
assert.Equal(t, testing_tools.TestGroupId, group.Id)
|
||||
assert.Equal(t, "updatedGroupName", group.Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update group peers",
|
||||
groupId: testing_tools.TestGroupId,
|
||||
requestBody: &api.GroupRequest{
|
||||
Name: "testGroupName",
|
||||
Peers: &[]string{},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, group *api.Group) {
|
||||
t.Helper()
|
||||
assert.Equal(t, 0, group.PeersCount)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update with empty name",
|
||||
groupId: testing_tools.TestGroupId,
|
||||
requestBody: &api.GroupRequest{
|
||||
Name: "",
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Update non-existing group",
|
||||
groupId: "nonExistingGroupId",
|
||||
requestBody: &api.GroupRequest{
|
||||
Name: "someName",
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/groups/{groupId}", "{groupId}", tc.groupId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.Group{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify updated group in DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbGroup := testing_tools.VerifyGroupInDB(t, db, tc.groupId)
|
||||
assert.Equal(t, tc.requestBody.Name, dbGroup.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Groups_Delete(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
groupId string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Delete existing group not in use",
|
||||
groupId: testing_tools.NewGroupId,
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Delete non-existing group",
|
||||
groupId: "nonExistingGroupId",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/groups.sql", nil, false)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/groups/{groupId}", "{groupId}", tc.groupId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if expectResponse && tc.expectedStatus == http.StatusOK {
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
testing_tools.VerifyGroupNotInDB(t, db, tc.groupId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,605 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
const (
|
||||
testPeerId2 = "testPeerId2"
|
||||
)
|
||||
|
||||
func Test_Peers_GetAll(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{
|
||||
name: "Regular user",
|
||||
userId: testing_tools.TestUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Admin user",
|
||||
userId: testing_tools.TestAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Owner user",
|
||||
userId: testing_tools.TestOwnerId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Regular service user",
|
||||
userId: testing_tools.TestServiceUserId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Admin service user",
|
||||
userId: testing_tools.TestServiceAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Blocked user",
|
||||
userId: testing_tools.BlockedUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Other user",
|
||||
userId: testing_tools.OtherUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid token",
|
||||
userId: testing_tools.InvalidToken,
|
||||
expectResponse: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all peers", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/peers", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
var got []api.PeerBatch
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, len(got), 2, "Expected at least 2 peers")
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Peers_GetById(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{
|
||||
name: "Regular user",
|
||||
userId: testing_tools.TestUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Admin user",
|
||||
userId: testing_tools.TestAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Owner user",
|
||||
userId: testing_tools.TestOwnerId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Regular service user",
|
||||
userId: testing_tools.TestServiceUserId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Admin service user",
|
||||
userId: testing_tools.TestServiceAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Blocked user",
|
||||
userId: testing_tools.BlockedUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Other user",
|
||||
userId: testing_tools.OtherUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid token",
|
||||
userId: testing_tools.InvalidToken,
|
||||
expectResponse: false,
|
||||
},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestType string
|
||||
requestPath string
|
||||
requestId string
|
||||
verifyResponse func(t *testing.T, peer *api.Peer)
|
||||
}{
|
||||
{
|
||||
name: "Get existing peer",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/peers/{peerId}",
|
||||
requestId: testing_tools.TestPeerId,
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, peer *api.Peer) {
|
||||
t.Helper()
|
||||
assert.Equal(t, testing_tools.TestPeerId, peer.Id)
|
||||
assert.Equal(t, "test-peer-1", peer.Name)
|
||||
assert.Equal(t, "test-host-1", peer.Hostname)
|
||||
assert.Equal(t, "Debian GNU/Linux ", peer.Os)
|
||||
assert.Equal(t, "0.12.0", peer.Version)
|
||||
assert.Equal(t, false, peer.SshEnabled)
|
||||
assert.Equal(t, true, peer.LoginExpirationEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get second existing peer",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/peers/{peerId}",
|
||||
requestId: testPeerId2,
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, peer *api.Peer) {
|
||||
t.Helper()
|
||||
assert.Equal(t, testPeerId2, peer.Id)
|
||||
assert.Equal(t, "test-peer-2", peer.Name)
|
||||
assert.Equal(t, "test-host-2", peer.Hostname)
|
||||
assert.Equal(t, "Ubuntu ", peer.Os)
|
||||
assert.Equal(t, true, peer.SshEnabled)
|
||||
assert.Equal(t, false, peer.LoginExpirationEnabled)
|
||||
assert.Equal(t, true, peer.Connected)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get non-existing peer",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/peers/{peerId}",
|
||||
requestId: "nonExistingPeerId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
verifyResponse: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, tc.requestType, strings.Replace(tc.requestPath, "{peerId}", tc.requestId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.Peer{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Peers_Update(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{
|
||||
name: "Regular user",
|
||||
userId: testing_tools.TestUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Admin user",
|
||||
userId: testing_tools.TestAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Owner user",
|
||||
userId: testing_tools.TestOwnerId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Regular service user",
|
||||
userId: testing_tools.TestServiceUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Admin service user",
|
||||
userId: testing_tools.TestServiceAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Blocked user",
|
||||
userId: testing_tools.BlockedUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Other user",
|
||||
userId: testing_tools.OtherUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid token",
|
||||
userId: testing_tools.InvalidToken,
|
||||
expectResponse: false,
|
||||
},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestBody *api.PeerRequest
|
||||
requestType string
|
||||
requestPath string
|
||||
requestId string
|
||||
verifyResponse func(t *testing.T, peer *api.Peer)
|
||||
}{
|
||||
{
|
||||
name: "Update peer name",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/peers/{peerId}",
|
||||
requestId: testing_tools.TestPeerId,
|
||||
requestBody: &api.PeerRequest{
|
||||
Name: "updated-peer-name",
|
||||
SshEnabled: false,
|
||||
LoginExpirationEnabled: true,
|
||||
InactivityExpirationEnabled: false,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, peer *api.Peer) {
|
||||
t.Helper()
|
||||
assert.Equal(t, testing_tools.TestPeerId, peer.Id)
|
||||
assert.Equal(t, "updated-peer-name", peer.Name)
|
||||
assert.Equal(t, false, peer.SshEnabled)
|
||||
assert.Equal(t, true, peer.LoginExpirationEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Enable SSH on peer",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/peers/{peerId}",
|
||||
requestId: testing_tools.TestPeerId,
|
||||
requestBody: &api.PeerRequest{
|
||||
Name: "test-peer-1",
|
||||
SshEnabled: true,
|
||||
LoginExpirationEnabled: true,
|
||||
InactivityExpirationEnabled: false,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, peer *api.Peer) {
|
||||
t.Helper()
|
||||
assert.Equal(t, testing_tools.TestPeerId, peer.Id)
|
||||
assert.Equal(t, "test-peer-1", peer.Name)
|
||||
assert.Equal(t, true, peer.SshEnabled)
|
||||
assert.Equal(t, true, peer.LoginExpirationEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Disable login expiration on peer",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/peers/{peerId}",
|
||||
requestId: testing_tools.TestPeerId,
|
||||
requestBody: &api.PeerRequest{
|
||||
Name: "test-peer-1",
|
||||
SshEnabled: false,
|
||||
LoginExpirationEnabled: false,
|
||||
InactivityExpirationEnabled: false,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, peer *api.Peer) {
|
||||
t.Helper()
|
||||
assert.Equal(t, testing_tools.TestPeerId, peer.Id)
|
||||
assert.Equal(t, false, peer.LoginExpirationEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update non-existing peer",
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/peers/{peerId}",
|
||||
requestId: "nonExistingPeerId",
|
||||
requestBody: &api.PeerRequest{
|
||||
Name: "updated-name",
|
||||
SshEnabled: false,
|
||||
LoginExpirationEnabled: false,
|
||||
InactivityExpirationEnabled: false,
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
verifyResponse: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, tc.requestType, strings.Replace(tc.requestPath, "{peerId}", tc.requestId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.Peer{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify updated peer in DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbPeer := testing_tools.VerifyPeerInDB(t, db, tc.requestId)
|
||||
assert.Equal(t, tc.requestBody.Name, dbPeer.Name)
|
||||
assert.Equal(t, tc.requestBody.SshEnabled, dbPeer.SSHEnabled)
|
||||
assert.Equal(t, tc.requestBody.LoginExpirationEnabled, dbPeer.LoginExpirationEnabled)
|
||||
assert.Equal(t, tc.requestBody.InactivityExpirationEnabled, dbPeer.InactivityExpirationEnabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Peers_Delete(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{
|
||||
name: "Regular user",
|
||||
userId: testing_tools.TestUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Admin user",
|
||||
userId: testing_tools.TestAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Owner user",
|
||||
userId: testing_tools.TestOwnerId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Regular service user",
|
||||
userId: testing_tools.TestServiceUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Admin service user",
|
||||
userId: testing_tools.TestServiceAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Blocked user",
|
||||
userId: testing_tools.BlockedUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Other user",
|
||||
userId: testing_tools.OtherUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid token",
|
||||
userId: testing_tools.InvalidToken,
|
||||
expectResponse: false,
|
||||
},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestType string
|
||||
requestPath string
|
||||
requestId string
|
||||
}{
|
||||
{
|
||||
name: "Delete existing peer",
|
||||
requestType: http.MethodDelete,
|
||||
requestPath: "/api/peers/{peerId}",
|
||||
requestId: testPeerId2,
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Delete non-existing peer",
|
||||
requestType: http.MethodDelete,
|
||||
requestPath: "/api/peers/{peerId}",
|
||||
requestId: "nonExistingPeerId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, false)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, tc.requestType, strings.Replace(tc.requestPath, "{peerId}", tc.requestId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify peer is actually deleted in DB
|
||||
if tc.expectedStatus == http.StatusOK {
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
testing_tools.VerifyPeerNotInDB(t, db, tc.requestId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Peers_GetAccessiblePeers(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{
|
||||
name: "Regular user",
|
||||
userId: testing_tools.TestUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Admin user",
|
||||
userId: testing_tools.TestAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Owner user",
|
||||
userId: testing_tools.TestOwnerId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Regular service user",
|
||||
userId: testing_tools.TestServiceUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Admin service user",
|
||||
userId: testing_tools.TestServiceAdminId,
|
||||
expectResponse: true,
|
||||
},
|
||||
{
|
||||
name: "Blocked user",
|
||||
userId: testing_tools.BlockedUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Other user",
|
||||
userId: testing_tools.OtherUserId,
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid token",
|
||||
userId: testing_tools.InvalidToken,
|
||||
expectResponse: false,
|
||||
},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestType string
|
||||
requestPath string
|
||||
requestId string
|
||||
}{
|
||||
{
|
||||
name: "Get accessible peers for existing peer",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/peers/{peerId}/accessible-peers",
|
||||
requestId: testing_tools.TestPeerId,
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Get accessible peers for non-existing peer",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/api/peers/{peerId}/accessible-peers",
|
||||
requestId: "nonExistingPeerId",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/peers_integration.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, tc.requestType, strings.Replace(tc.requestPath, "{peerId}", tc.requestId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.expectedStatus == http.StatusOK {
|
||||
var got []api.AccessiblePeer
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
// The accessible peers list should be a valid array (may be empty if no policies connect peers)
|
||||
assert.NotNil(t, got, "Expected accessible peers to be a valid array")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
func Test_Policies_GetAll(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all policies", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/policies", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.Policy{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(got))
|
||||
assert.Equal(t, "testPolicy", got[0].Name)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Policies_GetById(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
policyId string
|
||||
expectedStatus int
|
||||
expectPolicy bool
|
||||
}{
|
||||
{
|
||||
name: "Get existing policy",
|
||||
policyId: "testPolicyId",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectPolicy: true,
|
||||
},
|
||||
{
|
||||
name: "Get non-existing policy",
|
||||
policyId: "nonExistingPolicyId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectPolicy: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/policies/{policyId}", "{policyId}", tc.policyId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.expectPolicy {
|
||||
got := &api.Policy{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
assert.NotNil(t, got.Id)
|
||||
assert.Equal(t, tc.policyId, *got.Id)
|
||||
assert.Equal(t, "testPolicy", got.Name)
|
||||
assert.Equal(t, true, got.Enabled)
|
||||
assert.GreaterOrEqual(t, len(got.Rules), 1)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Policies_Create(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
srcGroups := []string{testing_tools.TestGroupId}
|
||||
dstGroups := []string{testing_tools.TestGroupId}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
requestBody *api.PolicyCreate
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, policy *api.Policy)
|
||||
}{
|
||||
{
|
||||
name: "Create policy with accept rule",
|
||||
requestBody: &api.PolicyCreate{
|
||||
Name: "newPolicy",
|
||||
Enabled: true,
|
||||
Rules: []api.PolicyRuleUpdate{
|
||||
{
|
||||
Name: "allowAll",
|
||||
Enabled: true,
|
||||
Action: "accept",
|
||||
Protocol: "all",
|
||||
Bidirectional: true,
|
||||
Sources: &srcGroups,
|
||||
Destinations: &dstGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, policy *api.Policy) {
|
||||
t.Helper()
|
||||
assert.NotNil(t, policy.Id)
|
||||
assert.Equal(t, "newPolicy", policy.Name)
|
||||
assert.Equal(t, true, policy.Enabled)
|
||||
assert.Equal(t, 1, len(policy.Rules))
|
||||
assert.Equal(t, "allowAll", policy.Rules[0].Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create policy with drop rule",
|
||||
requestBody: &api.PolicyCreate{
|
||||
Name: "dropPolicy",
|
||||
Enabled: true,
|
||||
Rules: []api.PolicyRuleUpdate{
|
||||
{
|
||||
Name: "dropAll",
|
||||
Enabled: true,
|
||||
Action: "drop",
|
||||
Protocol: "all",
|
||||
Bidirectional: true,
|
||||
Sources: &srcGroups,
|
||||
Destinations: &dstGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, policy *api.Policy) {
|
||||
t.Helper()
|
||||
assert.Equal(t, "dropPolicy", policy.Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create policy with TCP rule and ports",
|
||||
requestBody: &api.PolicyCreate{
|
||||
Name: "tcpPolicy",
|
||||
Enabled: true,
|
||||
Rules: []api.PolicyRuleUpdate{
|
||||
{
|
||||
Name: "tcpRule",
|
||||
Enabled: true,
|
||||
Action: "accept",
|
||||
Protocol: "tcp",
|
||||
Bidirectional: true,
|
||||
Sources: &srcGroups,
|
||||
Destinations: &dstGroups,
|
||||
Ports: &[]string{"80", "443"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, policy *api.Policy) {
|
||||
t.Helper()
|
||||
assert.Equal(t, "tcpPolicy", policy.Name)
|
||||
assert.NotNil(t, policy.Rules[0].Ports)
|
||||
assert.Equal(t, 2, len(*policy.Rules[0].Ports))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create policy with empty name",
|
||||
requestBody: &api.PolicyCreate{
|
||||
Name: "",
|
||||
Enabled: true,
|
||||
Rules: []api.PolicyRuleUpdate{
|
||||
{
|
||||
Name: "rule",
|
||||
Enabled: true,
|
||||
Action: "accept",
|
||||
Protocol: "all",
|
||||
Sources: &srcGroups,
|
||||
Destinations: &dstGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Create policy with no rules",
|
||||
requestBody: &api.PolicyCreate{
|
||||
Name: "noRulesPolicy",
|
||||
Enabled: true,
|
||||
Rules: []api.PolicyRuleUpdate{},
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/policies", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.Policy{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify policy exists in DB with correct fields
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbPolicy := testing_tools.VerifyPolicyInDB(t, db, *got.Id)
|
||||
assert.Equal(t, tc.requestBody.Name, dbPolicy.Name)
|
||||
assert.Equal(t, tc.requestBody.Enabled, dbPolicy.Enabled)
|
||||
assert.Equal(t, len(tc.requestBody.Rules), len(dbPolicy.Rules))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Policies_Update(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
srcGroups := []string{testing_tools.TestGroupId}
|
||||
dstGroups := []string{testing_tools.TestGroupId}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
policyId string
|
||||
requestBody *api.PolicyCreate
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, policy *api.Policy)
|
||||
}{
|
||||
{
|
||||
name: "Update policy name",
|
||||
policyId: "testPolicyId",
|
||||
requestBody: &api.PolicyCreate{
|
||||
Name: "updatedPolicy",
|
||||
Enabled: true,
|
||||
Rules: []api.PolicyRuleUpdate{
|
||||
{
|
||||
Name: "testRule",
|
||||
Enabled: true,
|
||||
Action: "accept",
|
||||
Protocol: "all",
|
||||
Bidirectional: true,
|
||||
Sources: &srcGroups,
|
||||
Destinations: &dstGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, policy *api.Policy) {
|
||||
t.Helper()
|
||||
assert.Equal(t, "updatedPolicy", policy.Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update policy enabled state",
|
||||
policyId: "testPolicyId",
|
||||
requestBody: &api.PolicyCreate{
|
||||
Name: "testPolicy",
|
||||
Enabled: false,
|
||||
Rules: []api.PolicyRuleUpdate{
|
||||
{
|
||||
Name: "testRule",
|
||||
Enabled: true,
|
||||
Action: "accept",
|
||||
Protocol: "all",
|
||||
Bidirectional: true,
|
||||
Sources: &srcGroups,
|
||||
Destinations: &dstGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, policy *api.Policy) {
|
||||
t.Helper()
|
||||
assert.Equal(t, false, policy.Enabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update non-existing policy",
|
||||
policyId: "nonExistingPolicyId",
|
||||
requestBody: &api.PolicyCreate{
|
||||
Name: "whatever",
|
||||
Enabled: true,
|
||||
Rules: []api.PolicyRuleUpdate{
|
||||
{
|
||||
Name: "rule",
|
||||
Enabled: true,
|
||||
Action: "accept",
|
||||
Protocol: "all",
|
||||
Sources: &srcGroups,
|
||||
Destinations: &dstGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/policies/{policyId}", "{policyId}", tc.policyId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.Policy{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify updated policy in DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbPolicy := testing_tools.VerifyPolicyInDB(t, db, tc.policyId)
|
||||
assert.Equal(t, tc.requestBody.Name, dbPolicy.Name)
|
||||
assert.Equal(t, tc.requestBody.Enabled, dbPolicy.Enabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Policies_Delete(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
policyId string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Delete existing policy",
|
||||
policyId: "testPolicyId",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Delete non-existing policy",
|
||||
policyId: "nonExistingPolicyId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/policies.sql", nil, false)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/policies/{policyId}", "{policyId}", tc.policyId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if expectResponse && tc.expectedStatus == http.StatusOK {
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
testing_tools.VerifyPolicyNotInDB(t, db, tc.policyId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
func Test_Routes_GetAll(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all routes", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/routes", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.Route{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, len(got))
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Routes_GetById(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
routeId string
|
||||
expectedStatus int
|
||||
expectRoute bool
|
||||
}{
|
||||
{
|
||||
name: "Get existing route",
|
||||
routeId: "testRouteId",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectRoute: true,
|
||||
},
|
||||
{
|
||||
name: "Get non-existing route",
|
||||
routeId: "nonExistingRouteId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectRoute: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/routes/{routeId}", "{routeId}", tc.routeId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.expectRoute {
|
||||
got := &api.Route{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
assert.Equal(t, tc.routeId, got.Id)
|
||||
assert.Equal(t, "Test Network Route", got.Description)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Routes_Create(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
networkCIDR := "10.10.0.0/24"
|
||||
peerID := testing_tools.TestPeerId
|
||||
peerGroups := []string{"peerGroupId"}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
requestBody *api.RouteRequest
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, route *api.Route)
|
||||
}{
|
||||
{
|
||||
name: "Create network route with peer",
|
||||
requestBody: &api.RouteRequest{
|
||||
Description: "New network route",
|
||||
Network: &networkCIDR,
|
||||
Peer: &peerID,
|
||||
NetworkId: "newNet",
|
||||
Metric: 100,
|
||||
Masquerade: true,
|
||||
Enabled: true,
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, route *api.Route) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, route.Id)
|
||||
assert.Equal(t, "New network route", route.Description)
|
||||
assert.Equal(t, 100, route.Metric)
|
||||
assert.Equal(t, true, route.Masquerade)
|
||||
assert.Equal(t, true, route.Enabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create network route with peer groups",
|
||||
requestBody: &api.RouteRequest{
|
||||
Description: "Route with peer groups",
|
||||
Network: &networkCIDR,
|
||||
PeerGroups: &peerGroups,
|
||||
NetworkId: "peerGroupNet",
|
||||
Metric: 150,
|
||||
Masquerade: false,
|
||||
Enabled: true,
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, route *api.Route) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, route.Id)
|
||||
assert.Equal(t, "Route with peer groups", route.Description)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create route with empty network_id",
|
||||
requestBody: &api.RouteRequest{
|
||||
Description: "Empty net id",
|
||||
Network: &networkCIDR,
|
||||
Peer: &peerID,
|
||||
NetworkId: "",
|
||||
Metric: 100,
|
||||
Enabled: true,
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Create route with metric 0",
|
||||
requestBody: &api.RouteRequest{
|
||||
Description: "Zero metric",
|
||||
Network: &networkCIDR,
|
||||
Peer: &peerID,
|
||||
NetworkId: "zeroMetric",
|
||||
Metric: 0,
|
||||
Enabled: true,
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Create route with metric 10000",
|
||||
requestBody: &api.RouteRequest{
|
||||
Description: "High metric",
|
||||
Network: &networkCIDR,
|
||||
Peer: &peerID,
|
||||
NetworkId: "highMetric",
|
||||
Metric: 10000,
|
||||
Enabled: true,
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/routes", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.Route{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify route exists in DB with correct fields
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbRoute := testing_tools.VerifyRouteInDB(t, db, route.ID(got.Id))
|
||||
assert.Equal(t, tc.requestBody.Description, dbRoute.Description)
|
||||
assert.Equal(t, tc.requestBody.Metric, dbRoute.Metric)
|
||||
assert.Equal(t, tc.requestBody.Masquerade, dbRoute.Masquerade)
|
||||
assert.Equal(t, tc.requestBody.Enabled, dbRoute.Enabled)
|
||||
assert.Equal(t, route.NetID(tc.requestBody.NetworkId), dbRoute.NetID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Routes_Update(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
networkCIDR := "10.0.0.0/24"
|
||||
peerID := testing_tools.TestPeerId
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
routeId string
|
||||
requestBody *api.RouteRequest
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, route *api.Route)
|
||||
}{
|
||||
{
|
||||
name: "Update route description",
|
||||
routeId: "testRouteId",
|
||||
requestBody: &api.RouteRequest{
|
||||
Description: "Updated description",
|
||||
Network: &networkCIDR,
|
||||
Peer: &peerID,
|
||||
NetworkId: "testNet",
|
||||
Metric: 100,
|
||||
Masquerade: true,
|
||||
Enabled: true,
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, route *api.Route) {
|
||||
t.Helper()
|
||||
assert.Equal(t, "testRouteId", route.Id)
|
||||
assert.Equal(t, "Updated description", route.Description)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update route metric",
|
||||
routeId: "testRouteId",
|
||||
requestBody: &api.RouteRequest{
|
||||
Description: "Test Network Route",
|
||||
Network: &networkCIDR,
|
||||
Peer: &peerID,
|
||||
NetworkId: "testNet",
|
||||
Metric: 500,
|
||||
Masquerade: true,
|
||||
Enabled: true,
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, route *api.Route) {
|
||||
t.Helper()
|
||||
assert.Equal(t, 500, route.Metric)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update non-existing route",
|
||||
routeId: "nonExistingRouteId",
|
||||
requestBody: &api.RouteRequest{
|
||||
Description: "whatever",
|
||||
Network: &networkCIDR,
|
||||
Peer: &peerID,
|
||||
NetworkId: "testNet",
|
||||
Metric: 100,
|
||||
Enabled: true,
|
||||
Groups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/routes/{routeId}", "{routeId}", tc.routeId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.Route{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify updated route in DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbRoute := testing_tools.VerifyRouteInDB(t, db, route.ID(got.Id))
|
||||
assert.Equal(t, tc.requestBody.Description, dbRoute.Description)
|
||||
assert.Equal(t, tc.requestBody.Metric, dbRoute.Metric)
|
||||
assert.Equal(t, tc.requestBody.Masquerade, dbRoute.Masquerade)
|
||||
assert.Equal(t, tc.requestBody.Enabled, dbRoute.Enabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Routes_Delete(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
routeId string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Delete existing route",
|
||||
routeId: "testRouteId",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Delete non-existing route",
|
||||
routeId: "nonExistingRouteId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/routes.sql", nil, false)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/routes/{routeId}", "{routeId}", tc.routeId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
|
||||
// Verify route was deleted from DB for successful deletes
|
||||
if tc.expectedStatus == http.StatusOK && user.expectResponse {
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
testing_tools.VerifyRouteNotInDB(t, db, route.ID(tc.routeId))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/setup_keys"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
@@ -254,7 +252,7 @@ func Test_SetupKeys_Create(t *testing.T) {
|
||||
expectedResponse: nil,
|
||||
},
|
||||
{
|
||||
name: "Create Setup Key",
|
||||
name: "Create Setup Key with nil AutoGroups",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/setup-keys",
|
||||
requestBody: &api.CreateSetupKeyRequest{
|
||||
@@ -308,14 +306,15 @@ func Test_SetupKeys_Create(t *testing.T) {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
gotID := got.Id
|
||||
validateCreatedKey(t, tc.expectedResponse, got)
|
||||
|
||||
key, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got.Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
validateCreatedKey(t, tc.expectedResponse, setup_keys.ToResponseBody(key))
|
||||
// Verify setup key exists in DB via gorm
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbKey := testing_tools.VerifySetupKeyInDB(t, db, gotID)
|
||||
assert.Equal(t, tc.expectedResponse.Name, dbKey.Name)
|
||||
assert.Equal(t, tc.expectedResponse.Revoked, dbKey.Revoked)
|
||||
assert.Equal(t, tc.expectedResponse.UsageLimit, dbKey.UsageLimit)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
@@ -571,7 +570,7 @@ func Test_SetupKeys_Update(t *testing.T) {
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/setup_keys.sql", nil, true)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
@@ -594,14 +593,16 @@ func Test_SetupKeys_Update(t *testing.T) {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
gotID := got.Id
|
||||
gotRevoked := got.Revoked
|
||||
gotUsageLimit := got.UsageLimit
|
||||
validateCreatedKey(t, tc.expectedResponse, got)
|
||||
|
||||
key, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got.Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
validateCreatedKey(t, tc.expectedResponse, setup_keys.ToResponseBody(key))
|
||||
// Verify updated setup key in DB via gorm
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbKey := testing_tools.VerifySetupKeyInDB(t, db, gotID)
|
||||
assert.Equal(t, gotRevoked, dbKey.Revoked)
|
||||
assert.Equal(t, gotUsageLimit, dbKey.UsageLimit)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
@@ -759,8 +760,8 @@ func Test_SetupKeys_Get(t *testing.T) {
|
||||
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectRespnose := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectRespnose {
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
got := &api.SetupKey{}
|
||||
@@ -768,14 +769,16 @@ func Test_SetupKeys_Get(t *testing.T) {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
gotID := got.Id
|
||||
gotName := got.Name
|
||||
gotRevoked := got.Revoked
|
||||
validateCreatedKey(t, tc.expectedResponse, got)
|
||||
|
||||
key, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got.Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
validateCreatedKey(t, tc.expectedResponse, setup_keys.ToResponseBody(key))
|
||||
// Verify setup key in DB via gorm
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbKey := testing_tools.VerifySetupKeyInDB(t, db, gotID)
|
||||
assert.Equal(t, gotName, dbKey.Name)
|
||||
assert.Equal(t, gotRevoked, dbKey.Revoked)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
@@ -928,15 +931,17 @@ func Test_SetupKeys_GetAll(t *testing.T) {
|
||||
return tc.expectedResponse[i].UsageLimit < tc.expectedResponse[j].UsageLimit
|
||||
})
|
||||
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
for i := range tc.expectedResponse {
|
||||
gotID := got[i].Id
|
||||
gotName := got[i].Name
|
||||
gotRevoked := got[i].Revoked
|
||||
validateCreatedKey(t, tc.expectedResponse[i], &got[i])
|
||||
|
||||
key, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got[i].Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
validateCreatedKey(t, tc.expectedResponse[i], setup_keys.ToResponseBody(key))
|
||||
// Verify each setup key in DB via gorm
|
||||
dbKey := testing_tools.VerifySetupKeyInDB(t, db, gotID)
|
||||
assert.Equal(t, gotName, dbKey.Name)
|
||||
assert.Equal(t, gotRevoked, dbKey.Revoked)
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -1104,8 +1109,9 @@ func Test_SetupKeys_Delete(t *testing.T) {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
_, err := am.GetSetupKey(context.Background(), testing_tools.TestAccountId, testing_tools.TestUserId, got.Id)
|
||||
assert.Errorf(t, err, "Expected error when trying to get deleted key")
|
||||
// Verify setup key deleted from DB via gorm
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
testing_tools.VerifySetupKeyNotInDB(t, db, got.Id)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
@@ -1120,7 +1126,7 @@ func Test_SetupKeys_Delete(t *testing.T) {
|
||||
func validateCreatedKey(t *testing.T, expectedKey *api.SetupKey, got *api.SetupKey) {
|
||||
t.Helper()
|
||||
|
||||
if got.Expires.After(time.Now().Add(-1*time.Minute)) && got.Expires.Before(time.Now().Add(testing_tools.ExpiresIn*time.Second)) ||
|
||||
if (got.Expires.After(time.Now().Add(-1*time.Minute)) && got.Expires.Before(time.Now().Add(testing_tools.ExpiresIn*time.Second))) ||
|
||||
got.Expires.After(time.Date(2300, 01, 01, 0, 0, 0, 0, time.Local)) ||
|
||||
got.Expires.Before(time.Date(1950, 01, 01, 0, 0, 0, 0, time.Local)) {
|
||||
got.Expires = time.Time{}
|
||||
|
||||
@@ -0,0 +1,701 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
|
||||
"github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
func Test_Users_GetAll(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, true},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, true},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all users", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/users", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.User{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, len(got), 1)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Users_GetAll_ServiceUsers(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all service users", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, "/api/users?service_user=true", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.User{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
for _, u := range got {
|
||||
assert.NotNil(t, u.IsServiceUser)
|
||||
assert.Equal(t, true, *u.IsServiceUser)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Users_Create_ServiceUser(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
requestBody *api.UserCreateRequest
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, user *api.User)
|
||||
}{
|
||||
{
|
||||
name: "Create service user with admin role",
|
||||
requestBody: &api.UserCreateRequest{
|
||||
Role: "admin",
|
||||
IsServiceUser: true,
|
||||
AutoGroups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, user *api.User) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, user.Id)
|
||||
assert.Equal(t, "admin", user.Role)
|
||||
assert.NotNil(t, user.IsServiceUser)
|
||||
assert.Equal(t, true, *user.IsServiceUser)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create service user with user role",
|
||||
requestBody: &api.UserCreateRequest{
|
||||
Role: "user",
|
||||
IsServiceUser: true,
|
||||
AutoGroups: []string{testing_tools.TestGroupId},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, user *api.User) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, user.Id)
|
||||
assert.Equal(t, "user", user.Role)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create service user with empty auto_groups",
|
||||
requestBody: &api.UserCreateRequest{
|
||||
Role: "admin",
|
||||
IsServiceUser: true,
|
||||
AutoGroups: []string{},
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, user *api.User) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, user.Id)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPost, "/api/users", user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.User{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify user in DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbUser := testing_tools.VerifyUserInDB(t, db, got.Id)
|
||||
assert.True(t, dbUser.IsServiceUser)
|
||||
assert.Equal(t, string(dbUser.Role), string(tc.requestBody.Role))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Users_Update(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
targetUserId string
|
||||
requestBody *api.UserRequest
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, user *api.User)
|
||||
}{
|
||||
{
|
||||
name: "Update user role to admin",
|
||||
targetUserId: testing_tools.TestUserId,
|
||||
requestBody: &api.UserRequest{
|
||||
Role: "admin",
|
||||
AutoGroups: []string{},
|
||||
IsBlocked: false,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, user *api.User) {
|
||||
t.Helper()
|
||||
assert.Equal(t, "admin", user.Role)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update user auto_groups",
|
||||
targetUserId: testing_tools.TestUserId,
|
||||
requestBody: &api.UserRequest{
|
||||
Role: "user",
|
||||
AutoGroups: []string{testing_tools.TestGroupId},
|
||||
IsBlocked: false,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, user *api.User) {
|
||||
t.Helper()
|
||||
assert.Equal(t, 1, len(user.AutoGroups))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Block user",
|
||||
targetUserId: testing_tools.TestUserId,
|
||||
requestBody: &api.UserRequest{
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
IsBlocked: true,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, user *api.User) {
|
||||
t.Helper()
|
||||
assert.Equal(t, true, user.IsBlocked)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update non-existing user",
|
||||
targetUserId: "nonExistingUserId",
|
||||
requestBody: &api.UserRequest{
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
IsBlocked: false,
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, _ := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, false)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPut, strings.Replace("/api/users/{userId}", "{userId}", tc.targetUserId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.User{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify updated fields in DB
|
||||
if tc.expectedStatus == http.StatusOK {
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbUser := testing_tools.VerifyUserInDB(t, db, tc.targetUserId)
|
||||
assert.Equal(t, string(dbUser.Role), string(tc.requestBody.Role))
|
||||
assert.Equal(t, dbUser.Blocked, tc.requestBody.IsBlocked)
|
||||
assert.ElementsMatch(t, dbUser.AutoGroups, tc.requestBody.AutoGroups)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Users_Delete(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
targetUserId string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Delete existing service user",
|
||||
targetUserId: "deletableServiceUserId",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Delete non-existing user",
|
||||
targetUserId: "nonExistingUserId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, strings.Replace("/api/users/{userId}", "{userId}", tc.targetUserId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
|
||||
// Verify user deleted from DB for successful deletes
|
||||
if expectResponse && tc.expectedStatus == http.StatusOK {
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
testing_tools.VerifyUserNotInDB(t, db, tc.targetUserId)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PATs_GetAll(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - Get all PATs for service user", func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, strings.Replace("/api/users/{userId}/tokens", "{userId}", testing_tools.TestServiceUserId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, http.StatusOK, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
got := []api.PersonalAccessToken{}
|
||||
if err := json.Unmarshal(content, &got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(got))
|
||||
assert.Equal(t, "serviceToken", got[0].Name)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PATs_GetById(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
tokenId string
|
||||
expectedStatus int
|
||||
expectToken bool
|
||||
}{
|
||||
{
|
||||
name: "Get existing PAT",
|
||||
tokenId: "serviceTokenId",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectToken: true,
|
||||
},
|
||||
{
|
||||
name: "Get non-existing PAT",
|
||||
tokenId: "nonExistingTokenId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectToken: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, _, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
|
||||
|
||||
path := strings.Replace("/api/users/{userId}/tokens/{tokenId}", "{userId}", testing_tools.TestServiceUserId, 1)
|
||||
path = strings.Replace(path, "{tokenId}", tc.tokenId, 1)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodGet, path, user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.expectToken {
|
||||
got := &api.PersonalAccessToken{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
assert.Equal(t, "serviceTokenId", got.Id)
|
||||
assert.Equal(t, "serviceToken", got.Name)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PATs_Create(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
targetUserId string
|
||||
requestBody *api.PersonalAccessTokenRequest
|
||||
expectedStatus int
|
||||
verifyResponse func(t *testing.T, pat *api.PersonalAccessTokenGenerated)
|
||||
}{
|
||||
{
|
||||
name: "Create PAT with 30 day expiry",
|
||||
targetUserId: testing_tools.TestServiceUserId,
|
||||
requestBody: &api.PersonalAccessTokenRequest{
|
||||
Name: "newPAT",
|
||||
ExpiresIn: 30,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, pat *api.PersonalAccessTokenGenerated) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, pat.PlainToken)
|
||||
assert.Equal(t, "newPAT", pat.PersonalAccessToken.Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create PAT with 365 day expiry",
|
||||
targetUserId: testing_tools.TestServiceUserId,
|
||||
requestBody: &api.PersonalAccessTokenRequest{
|
||||
Name: "longPAT",
|
||||
ExpiresIn: 365,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
verifyResponse: func(t *testing.T, pat *api.PersonalAccessTokenGenerated) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, pat.PlainToken)
|
||||
assert.Equal(t, "longPAT", pat.PersonalAccessToken.Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create PAT with empty name",
|
||||
targetUserId: testing_tools.TestServiceUserId,
|
||||
requestBody: &api.PersonalAccessTokenRequest{
|
||||
Name: "",
|
||||
ExpiresIn: 30,
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Create PAT with 0 day expiry",
|
||||
targetUserId: testing_tools.TestServiceUserId,
|
||||
requestBody: &api.PersonalAccessTokenRequest{
|
||||
Name: "zeroPAT",
|
||||
ExpiresIn: 0,
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Create PAT with expiry over 365 days",
|
||||
targetUserId: testing_tools.TestServiceUserId,
|
||||
requestBody: &api.PersonalAccessTokenRequest{
|
||||
Name: "tooLongPAT",
|
||||
ExpiresIn: 400,
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
|
||||
|
||||
body, err := json.Marshal(tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req := testing_tools.BuildRequest(t, body, http.MethodPost, strings.Replace("/api/users/{userId}/tokens", "{userId}", tc.targetUserId, 1), user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
content, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
if !expectResponse {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.verifyResponse != nil {
|
||||
got := &api.PersonalAccessTokenGenerated{}
|
||||
if err := json.Unmarshal(content, got); err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
tc.verifyResponse(t, got)
|
||||
|
||||
// Verify PAT in DB
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
dbPAT := testing_tools.VerifyPATInDB(t, db, got.PersonalAccessToken.Id)
|
||||
assert.Equal(t, tc.requestBody.Name, dbPAT.Name)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PATs_Delete(t *testing.T) {
|
||||
users := []struct {
|
||||
name string
|
||||
userId string
|
||||
expectResponse bool
|
||||
}{
|
||||
{"Regular user", testing_tools.TestUserId, false},
|
||||
{"Admin user", testing_tools.TestAdminId, true},
|
||||
{"Owner user", testing_tools.TestOwnerId, true},
|
||||
{"Regular service user", testing_tools.TestServiceUserId, false},
|
||||
{"Admin service user", testing_tools.TestServiceAdminId, true},
|
||||
{"Blocked user", testing_tools.BlockedUserId, false},
|
||||
{"Other user", testing_tools.OtherUserId, false},
|
||||
{"Invalid token", testing_tools.InvalidToken, false},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
tokenId string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Delete existing PAT",
|
||||
tokenId: "serviceTokenId",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Delete non-existing PAT",
|
||||
tokenId: "nonExistingTokenId",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
for _, user := range users {
|
||||
t.Run(user.name+" - "+tc.name, func(t *testing.T) {
|
||||
apiHandler, am, done := channel.BuildApiBlackBoxWithDBState(t, "../testdata/users_integration.sql", nil, true)
|
||||
|
||||
path := strings.Replace("/api/users/{userId}/tokens/{tokenId}", "{userId}", testing_tools.TestServiceUserId, 1)
|
||||
path = strings.Replace(path, "{tokenId}", tc.tokenId, 1)
|
||||
|
||||
req := testing_tools.BuildRequest(t, []byte{}, http.MethodDelete, path, user.userId)
|
||||
recorder := httptest.NewRecorder()
|
||||
apiHandler.ServeHTTP(recorder, req)
|
||||
|
||||
_, expectResponse := testing_tools.ReadResponse(t, recorder, tc.expectedStatus, user.expectResponse)
|
||||
|
||||
// Verify PAT deleted from DB for successful deletes
|
||||
if expectResponse && tc.expectedStatus == http.StatusOK {
|
||||
db := testing_tools.GetDB(t, am.GetStore())
|
||||
testing_tools.VerifyPATNotInDB(t, db, tc.tokenId)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for peerShouldNotReceiveUpdate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
|
||||
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
|
||||
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
|
||||
INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','[]',0,'');
|
||||
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
|
||||
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
|
||||
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `name_server_groups` (`id` text,`account_id` text,`name` text,`description` text,`name_servers` text,`groups` text,`primary` numeric,`domains` text,`enabled` numeric,`search_domains_enabled` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_name_server_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
|
||||
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
|
||||
INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','["testPeerId"]',0,'');
|
||||
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
|
||||
INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
|
||||
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
|
||||
INSERT INTO name_server_groups VALUES('testNSGroupId','testAccountId','testNSGroup','test nameserver group','[{"IP":"1.1.1.1","NSType":1,"Port":53}]','["testGroupId"]',0,'["example.com"]',1,0);
|
||||
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
|
||||
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
|
||||
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
|
||||
INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','[]',0,'');
|
||||
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
|
||||
INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
|
||||
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
|
||||
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
|
||||
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
|
||||
INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','["testPeerId"]',0,'');
|
||||
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
|
||||
INSERT INTO "groups" VALUES('allGroupId','testAccountId','All','api','[]',0,'');
|
||||
INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
|
||||
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
@@ -0,0 +1,25 @@
|
||||
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
|
||||
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `networks` (`id` text,`account_id` text,`name` text,`description` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_networks` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `network_routers` (`id` text,`network_id` text,`account_id` text,`peer` text,`peer_groups` text,`masquerade` numeric,`metric` integer,`enabled` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_network_routers` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `network_resources` (`id` text,`network_id` text,`account_id` text,`name` text,`description` text,`type` text,`domain` text,`prefix` text,`enabled` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_network_resources` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
|
||||
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
|
||||
INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'testServiceUser','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'testServiceAdmin','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','["testPeerId"]',0,'');
|
||||
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
|
||||
INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:00',0,0,NULL,'["testGroupId"]',1,0);
|
||||
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
|
||||
INSERT INTO networks VALUES('testNetworkId','testAccountId','testNetwork','test network description');
|
||||
INSERT INTO network_routers VALUES('testRouterId','testNetworkId','testAccountId','testPeerId','[]',1,100,1);
|
||||
INSERT INTO network_resources VALUES('testResourceId','testNetworkId','testAccountId','testResource','test resource description','host','','"3.3.3.3/32"',1);
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
|
||||
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
|
||||
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
|
||||
INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','["testPeerId","testPeerId2"]',0,'');
|
||||
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
|
||||
INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
|
||||
|
||||
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','test-host-1','linux','Linux','','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'test-peer-1','test-peer-1','2023-03-02 09:21:02.189035775+01:00',0,0,0,'testUserId','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
INSERT INTO peers VALUES('testPeerId2','testAccountId','6rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYBg=','82546A29-6BC8-4311-BCFC-9CDBF33F1A49','"100.64.114.32"','test-host-2','linux','Linux','','unknown','Ubuntu','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'test-peer-2','test-peer-2','2023-03-02 09:21:02.189035775+01:00',1,0,0,'testAdminId','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',1,0,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
|
||||
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `policies` (`id` text,`account_id` text,`name` text,`description` text,`enabled` numeric,`source_posture_checks` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_policies_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `policy_rules` (`id` text,`policy_id` text,`name` text,`description` text,`enabled` numeric,`action` text,`protocol` text,`bidirectional` numeric,`sources` text,`destinations` text,`source_resource` text,`destination_resource` text,`ports` text,`port_ranges` text,`authorized_groups` text,`authorized_user` text,PRIMARY KEY (`id`),CONSTRAINT `fk_policies_rules_g` FOREIGN KEY (`policy_id`) REFERENCES `policies`(`id`));
|
||||
|
||||
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
|
||||
INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','["testPeerId"]',0,'');
|
||||
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
|
||||
INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
|
||||
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
|
||||
INSERT INTO policies VALUES('testPolicyId','testAccountId','testPolicy','test policy description',1,NULL);
|
||||
INSERT INTO policy_rules VALUES('testRuleId','testPolicyId','testRule','test rule',1,'accept','all',1,'["testGroupId"]','["testGroupId"]',NULL,NULL,NULL,NULL,NULL,'');
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
|
||||
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `routes` (`id` text,`account_id` text,`network` text,`domains` text,`keep_route` numeric,`net_id` text,`description` text,`peer` text,`peer_groups` text,`network_type` integer,`masquerade` numeric,`metric` integer,`enabled` numeric,`groups` text,`access_control_groups` text,`skip_auto_apply` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_routes_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
|
||||
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
|
||||
INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','["testPeerId"]',0,'');
|
||||
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
|
||||
INSERT INTO "groups" VALUES('peerGroupId','testAccountId','peerGroupName','api','["testPeerId"]',0,'');
|
||||
INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
|
||||
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
|
||||
INSERT INTO routes VALUES('testRouteId','testAccountId','"10.0.0.0/24"',NULL,0,'testNet','Test Network Route','testPeerId',NULL,1,1,100,1,'["testGroupId"]',NULL,0);
|
||||
INSERT INTO routes VALUES('testDomainRouteId','testAccountId','"0.0.0.0/0"','["example.com"]',0,'testDomainNet','Test Domain Route','','["peerGroupId"]',3,1,200,1,'["testGroupId"]',NULL,0);
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
|
||||
CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `groups` (`id` text,`account_id` text,`name` text,`issued` text,`peers` text,`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_groups_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`key_secret` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
|
||||
CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime DEFAULT NULL,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`));
|
||||
CREATE INDEX `idx_personal_access_tokens_user_id` ON `personal_access_tokens`(`user_id`);
|
||||
|
||||
INSERT INTO accounts VALUES('testAccountId','','2024-10-02 16:01:38.000000000+00:00','test.com','private',1,'testNetworkIdentifier','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',1,86400000000000,0,0,0,'',NULL,NULL,NULL);
|
||||
INSERT INTO users VALUES('testUserId','testAccountId','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testAdminId','testAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testOwnerId','testAccountId','owner',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceUserId','testAccountId','user',1,0,'testServiceUser','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('testServiceAdminId','testAccountId','admin',1,0,'testServiceAdmin','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('blockedUserId','testAccountId','admin',0,0,'','[]',1,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('otherUserId','otherAccountId','admin',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO users VALUES('deletableServiceUserId','testAccountId','user',1,0,'deletableServiceUser','[]',0,NULL,'2024-10-02 16:01:38.000000000+00:00','api',0,'');
|
||||
INSERT INTO "groups" VALUES('testGroupId','testAccountId','testGroupName','api','["testPeerId"]',0,'');
|
||||
INSERT INTO "groups" VALUES('newGroupId','testAccountId','newGroupName','api','[]',0,'');
|
||||
INSERT INTO setup_keys VALUES('testKeyId','testAccountId','testKey','testK****','existingKey','one-off','2021-08-19 20:46:20.000000000+00:00','2321-09-18 20:46:20.000000000+00:00','2021-08-19 20:46:20.000000000+00:000',0,0,NULL,'["testGroupId"]',1,0);
|
||||
INSERT INTO peers VALUES('testPeerId','testAccountId','5rvhvriKJZ3S9oxYToVj5TzDM9u9y8cxg7htIMWlYAg=','72546A29-6BC8-4311-BCFC-9CDBF33F1A48','"100.64.114.31"','f2a34f6a4731','linux','Linux','11','unknown','Debian GNU/Linux','','0.12.0','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'f2a34f6a4731','f2a34f6a4731','2023-03-02 09:21:02.189035775+01:00',0,0,0,'','ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILzUUSYG/LGnV8zarb2SGN+tib/PZ+M7cL4WtTzUrTpk',0,1,'2023-03-01 19:48:19.817799698+01:00','2024-10-02 17:00:32.527947+02:00',0,'""','','',0);
|
||||
|
||||
INSERT INTO personal_access_tokens VALUES('testTokenId','testUserId','testToken','hashedTokenValue123','2325-10-02 16:01:38.000000000+00:00','testUserId','2024-10-02 16:01:38.000000000+00:00',NULL);
|
||||
INSERT INTO personal_access_tokens VALUES('serviceTokenId','testServiceUserId','serviceToken','hashedServiceTokenValue123','2325-10-02 16:01:38.000000000+00:00','testAdminId','2024-10-02 16:01:38.000000000+00:00',NULL);
|
||||
@@ -114,8 +114,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create proxy controller: %v", err)
|
||||
}
|
||||
domainManager.SetClusterCapabilities(serviceProxyController)
|
||||
serviceManager := reverseproxymanager.NewManager(store, am, permissionsManager, serviceProxyController, domainManager)
|
||||
serviceManager := reverseproxymanager.NewManager(store, am, permissionsManager, serviceProxyController, proxyMgr, domainManager)
|
||||
proxyServiceServer.SetServiceManager(serviceManager)
|
||||
am.SetServiceManager(serviceManager)
|
||||
|
||||
@@ -128,14 +127,14 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
||||
GetPATInfoFunc: authManager.GetPATInfo,
|
||||
}
|
||||
|
||||
networksManagerMock := networks.NewManagerMock()
|
||||
resourcesManagerMock := resources.NewManagerMock()
|
||||
routersManagerMock := routers.NewManagerMock()
|
||||
groupsManagerMock := groups.NewManagerMock()
|
||||
groupsManager := groups.NewManager(store, permissionsManager, am)
|
||||
routersManager := routers.NewManager(store, permissionsManager, am)
|
||||
resourcesManager := resources.NewManager(store, permissionsManager, groupsManager, am, serviceManager)
|
||||
networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, am)
|
||||
customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "")
|
||||
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
|
||||
|
||||
apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil)
|
||||
apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create API handler: %v", err)
|
||||
}
|
||||
@@ -167,6 +166,111 @@ func peerShouldReceiveUpdate(t testing_tools.TB, updateMessage <-chan *network_m
|
||||
}
|
||||
}
|
||||
|
||||
// PeerShouldReceiveAnyUpdate waits for a peer update message and returns it.
|
||||
// Fails the test if no update is received within timeout.
|
||||
func PeerShouldReceiveAnyUpdate(t testing_tools.TB, updateMessage <-chan *network_map.UpdateMessage) *network_map.UpdateMessage {
|
||||
t.Helper()
|
||||
select {
|
||||
case msg := <-updateMessage:
|
||||
if msg == nil {
|
||||
t.Errorf("Received nil update message, expected valid message")
|
||||
}
|
||||
return msg
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Errorf("Timed out waiting for update message")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PeerShouldNotReceiveAnyUpdate verifies no peer update message is received.
|
||||
func PeerShouldNotReceiveAnyUpdate(t testing_tools.TB, updateMessage <-chan *network_map.UpdateMessage) {
|
||||
t.Helper()
|
||||
peerShouldNotReceiveUpdate(t, updateMessage)
|
||||
}
|
||||
|
||||
// BuildApiBlackBoxWithDBStateAndPeerChannel creates the API handler and returns
|
||||
// the peer update channel directly so tests can verify updates inline.
|
||||
func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile string) (http.Handler, account.Manager, <-chan *network_map.UpdateMessage) {
|
||||
store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), sqlFile, t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test store: %v", err)
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create metrics: %v", err)
|
||||
}
|
||||
|
||||
peersUpdateManager := update_channel.NewPeersUpdateManager(nil)
|
||||
updMsg := peersUpdateManager.CreateChannel(context.Background(), testing_tools.TestPeerId)
|
||||
|
||||
geoMock := &geolocation.Mock{}
|
||||
validatorMock := server.MockIntegratedValidator{}
|
||||
proxyController := integrations.NewController(store)
|
||||
userManager := users.NewManager(store)
|
||||
permissionsManager := permissions.NewManager(store)
|
||||
settingsManager := settings.NewManager(store, userManager, integrations.NewManager(&activity.InMemoryEventStore{}), permissionsManager, settings.IdpConfig{})
|
||||
peersManager := peers.NewManager(store, permissionsManager)
|
||||
|
||||
jobManager := job.NewJobManager(nil, store, peersManager)
|
||||
|
||||
ctx := context.Background()
|
||||
requestBuffer := server.NewAccountRequestBuffer(ctx, store)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{})
|
||||
am, err := server.BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create manager: %v", err)
|
||||
}
|
||||
|
||||
accessLogsManager := accesslogsmanager.NewManager(store, permissionsManager, nil)
|
||||
proxyTokenStore, err := nbgrpc.NewOneTimeTokenStore(ctx, 5*time.Minute, 10*time.Minute, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create proxy token store: %v", err)
|
||||
}
|
||||
pkceverifierStore, err := nbgrpc.NewPKCEVerifierStore(ctx, 10*time.Minute, 10*time.Minute, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PKCE verifier store: %v", err)
|
||||
}
|
||||
noopMeter := noop.NewMeterProvider().Meter("")
|
||||
proxyMgr, err := proxymanager.NewManager(store, noopMeter)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create proxy manager: %v", err)
|
||||
}
|
||||
proxyServiceServer := nbgrpc.NewProxyServiceServer(accessLogsManager, proxyTokenStore, pkceverifierStore, nbgrpc.ProxyOIDCConfig{}, peersManager, userManager, proxyMgr)
|
||||
domainManager := manager.NewManager(store, proxyMgr, permissionsManager, am)
|
||||
serviceProxyController, err := proxymanager.NewGRPCController(proxyServiceServer, noopMeter)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create proxy controller: %v", err)
|
||||
}
|
||||
serviceManager := reverseproxymanager.NewManager(store, am, permissionsManager, serviceProxyController, proxyMgr, domainManager)
|
||||
proxyServiceServer.SetServiceManager(serviceManager)
|
||||
am.SetServiceManager(serviceManager)
|
||||
|
||||
// @note this is required so that PAT's validate from store, but JWT's are mocked
|
||||
authManager := serverauth.NewManager(store, "", "", "", "", []string{}, false)
|
||||
authManagerMock := &serverauth.MockManager{
|
||||
ValidateAndParseTokenFunc: mockValidateAndParseToken,
|
||||
EnsureUserAccessByJWTGroupsFunc: authManager.EnsureUserAccessByJWTGroups,
|
||||
MarkPATUsedFunc: authManager.MarkPATUsed,
|
||||
GetPATInfoFunc: authManager.GetPATInfo,
|
||||
}
|
||||
|
||||
groupsManager := groups.NewManager(store, permissionsManager, am)
|
||||
routersManager := routers.NewManager(store, permissionsManager, am)
|
||||
resourcesManager := resources.NewManager(store, permissionsManager, groupsManager, am, serviceManager)
|
||||
networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, am)
|
||||
customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "")
|
||||
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
|
||||
|
||||
apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create API handler: %v", err)
|
||||
}
|
||||
|
||||
return apiHandler, am, updMsg
|
||||
}
|
||||
|
||||
func mockValidateAndParseToken(_ context.Context, token string) (auth.UserAuth, *jwt.Token, error) {
|
||||
userAuth := auth.UserAuth{}
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package testing_tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
)
|
||||
|
||||
// GetDB extracts the *gorm.DB from a store.Store (must be *SqlStore).
|
||||
func GetDB(t *testing.T, s store.Store) *gorm.DB {
|
||||
t.Helper()
|
||||
sqlStore, ok := s.(*store.SqlStore)
|
||||
require.True(t, ok, "Store is not a *SqlStore, cannot get gorm.DB")
|
||||
return sqlStore.GetDB()
|
||||
}
|
||||
|
||||
// VerifyGroupInDB reads a group directly from the DB and returns it.
|
||||
func VerifyGroupInDB(t *testing.T, db *gorm.DB, groupID string) *types.Group {
|
||||
t.Helper()
|
||||
var group types.Group
|
||||
err := db.Where("id = ? AND account_id = ?", groupID, TestAccountId).First(&group).Error
|
||||
require.NoError(t, err, "Expected group %s to exist in DB", groupID)
|
||||
return &group
|
||||
}
|
||||
|
||||
// VerifyGroupNotInDB verifies that a group does not exist in the DB.
|
||||
func VerifyGroupNotInDB(t *testing.T, db *gorm.DB, groupID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&types.Group{}).Where("id = ? AND account_id = ?", groupID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected group %s to NOT exist in DB", groupID)
|
||||
}
|
||||
|
||||
// VerifyPolicyInDB reads a policy directly from the DB and returns it.
|
||||
func VerifyPolicyInDB(t *testing.T, db *gorm.DB, policyID string) *types.Policy {
|
||||
t.Helper()
|
||||
var policy types.Policy
|
||||
err := db.Preload("Rules").Where("id = ? AND account_id = ?", policyID, TestAccountId).First(&policy).Error
|
||||
require.NoError(t, err, "Expected policy %s to exist in DB", policyID)
|
||||
return &policy
|
||||
}
|
||||
|
||||
// VerifyPolicyNotInDB verifies that a policy does not exist in the DB.
|
||||
func VerifyPolicyNotInDB(t *testing.T, db *gorm.DB, policyID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&types.Policy{}).Where("id = ? AND account_id = ?", policyID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected policy %s to NOT exist in DB", policyID)
|
||||
}
|
||||
|
||||
// VerifyRouteInDB reads a route directly from the DB and returns it.
|
||||
func VerifyRouteInDB(t *testing.T, db *gorm.DB, routeID route.ID) *route.Route {
|
||||
t.Helper()
|
||||
var r route.Route
|
||||
err := db.Where("id = ? AND account_id = ?", routeID, TestAccountId).First(&r).Error
|
||||
require.NoError(t, err, "Expected route %s to exist in DB", routeID)
|
||||
return &r
|
||||
}
|
||||
|
||||
// VerifyRouteNotInDB verifies that a route does not exist in the DB.
|
||||
func VerifyRouteNotInDB(t *testing.T, db *gorm.DB, routeID route.ID) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&route.Route{}).Where("id = ? AND account_id = ?", routeID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected route %s to NOT exist in DB", routeID)
|
||||
}
|
||||
|
||||
// VerifyNSGroupInDB reads a nameserver group directly from the DB and returns it.
|
||||
func VerifyNSGroupInDB(t *testing.T, db *gorm.DB, nsGroupID string) *nbdns.NameServerGroup {
|
||||
t.Helper()
|
||||
var nsGroup nbdns.NameServerGroup
|
||||
err := db.Where("id = ? AND account_id = ?", nsGroupID, TestAccountId).First(&nsGroup).Error
|
||||
require.NoError(t, err, "Expected NS group %s to exist in DB", nsGroupID)
|
||||
return &nsGroup
|
||||
}
|
||||
|
||||
// VerifyNSGroupNotInDB verifies that a nameserver group does not exist in the DB.
|
||||
func VerifyNSGroupNotInDB(t *testing.T, db *gorm.DB, nsGroupID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&nbdns.NameServerGroup{}).Where("id = ? AND account_id = ?", nsGroupID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected NS group %s to NOT exist in DB", nsGroupID)
|
||||
}
|
||||
|
||||
// VerifyPeerInDB reads a peer directly from the DB and returns it.
|
||||
func VerifyPeerInDB(t *testing.T, db *gorm.DB, peerID string) *nbpeer.Peer {
|
||||
t.Helper()
|
||||
var peer nbpeer.Peer
|
||||
err := db.Where("id = ? AND account_id = ?", peerID, TestAccountId).First(&peer).Error
|
||||
require.NoError(t, err, "Expected peer %s to exist in DB", peerID)
|
||||
return &peer
|
||||
}
|
||||
|
||||
// VerifyPeerNotInDB verifies that a peer does not exist in the DB.
|
||||
func VerifyPeerNotInDB(t *testing.T, db *gorm.DB, peerID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&nbpeer.Peer{}).Where("id = ? AND account_id = ?", peerID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected peer %s to NOT exist in DB", peerID)
|
||||
}
|
||||
|
||||
// VerifySetupKeyInDB reads a setup key directly from the DB and returns it.
|
||||
func VerifySetupKeyInDB(t *testing.T, db *gorm.DB, keyID string) *types.SetupKey {
|
||||
t.Helper()
|
||||
var key types.SetupKey
|
||||
err := db.Where("id = ? AND account_id = ?", keyID, TestAccountId).First(&key).Error
|
||||
require.NoError(t, err, "Expected setup key %s to exist in DB", keyID)
|
||||
return &key
|
||||
}
|
||||
|
||||
// VerifySetupKeyNotInDB verifies that a setup key does not exist in the DB.
|
||||
func VerifySetupKeyNotInDB(t *testing.T, db *gorm.DB, keyID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&types.SetupKey{}).Where("id = ? AND account_id = ?", keyID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected setup key %s to NOT exist in DB", keyID)
|
||||
}
|
||||
|
||||
// VerifyUserInDB reads a user directly from the DB and returns it.
|
||||
func VerifyUserInDB(t *testing.T, db *gorm.DB, userID string) *types.User {
|
||||
t.Helper()
|
||||
var user types.User
|
||||
err := db.Where("id = ? AND account_id = ?", userID, TestAccountId).First(&user).Error
|
||||
require.NoError(t, err, "Expected user %s to exist in DB", userID)
|
||||
return &user
|
||||
}
|
||||
|
||||
// VerifyUserNotInDB verifies that a user does not exist in the DB.
|
||||
func VerifyUserNotInDB(t *testing.T, db *gorm.DB, userID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&types.User{}).Where("id = ? AND account_id = ?", userID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected user %s to NOT exist in DB", userID)
|
||||
}
|
||||
|
||||
// VerifyPATInDB reads a PAT directly from the DB and returns it.
|
||||
func VerifyPATInDB(t *testing.T, db *gorm.DB, tokenID string) *types.PersonalAccessToken {
|
||||
t.Helper()
|
||||
var pat types.PersonalAccessToken
|
||||
err := db.Where("id = ?", tokenID).First(&pat).Error
|
||||
require.NoError(t, err, "Expected PAT %s to exist in DB", tokenID)
|
||||
return &pat
|
||||
}
|
||||
|
||||
// VerifyPATNotInDB verifies that a PAT does not exist in the DB.
|
||||
func VerifyPATNotInDB(t *testing.T, db *gorm.DB, tokenID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&types.PersonalAccessToken{}).Where("id = ?", tokenID).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected PAT %s to NOT exist in DB", tokenID)
|
||||
}
|
||||
|
||||
// VerifyAccountSettings reads the account and returns its settings from the DB.
|
||||
func VerifyAccountSettings(t *testing.T, db *gorm.DB) *types.Account {
|
||||
t.Helper()
|
||||
var account types.Account
|
||||
err := db.Where("id = ?", TestAccountId).First(&account).Error
|
||||
require.NoError(t, err, "Expected account %s to exist in DB", TestAccountId)
|
||||
return &account
|
||||
}
|
||||
|
||||
// VerifyNetworkInDB reads a network directly from the store and returns it.
|
||||
func VerifyNetworkInDB(t *testing.T, db *gorm.DB, networkID string) *networkTypes.Network {
|
||||
t.Helper()
|
||||
var network networkTypes.Network
|
||||
err := db.Where("id = ? AND account_id = ?", networkID, TestAccountId).First(&network).Error
|
||||
require.NoError(t, err, "Expected network %s to exist in DB", networkID)
|
||||
return &network
|
||||
}
|
||||
|
||||
// VerifyNetworkNotInDB verifies that a network does not exist in the DB.
|
||||
func VerifyNetworkNotInDB(t *testing.T, db *gorm.DB, networkID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&networkTypes.Network{}).Where("id = ? AND account_id = ?", networkID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected network %s to NOT exist in DB", networkID)
|
||||
}
|
||||
|
||||
// VerifyNetworkResourceInDB reads a network resource directly from the DB and returns it.
|
||||
func VerifyNetworkResourceInDB(t *testing.T, db *gorm.DB, resourceID string) *resourceTypes.NetworkResource {
|
||||
t.Helper()
|
||||
var resource resourceTypes.NetworkResource
|
||||
err := db.Where("id = ? AND account_id = ?", resourceID, TestAccountId).First(&resource).Error
|
||||
require.NoError(t, err, "Expected network resource %s to exist in DB", resourceID)
|
||||
return &resource
|
||||
}
|
||||
|
||||
// VerifyNetworkResourceNotInDB verifies that a network resource does not exist in the DB.
|
||||
func VerifyNetworkResourceNotInDB(t *testing.T, db *gorm.DB, resourceID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&resourceTypes.NetworkResource{}).Where("id = ? AND account_id = ?", resourceID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected network resource %s to NOT exist in DB", resourceID)
|
||||
}
|
||||
|
||||
// VerifyNetworkRouterInDB reads a network router directly from the DB and returns it.
|
||||
func VerifyNetworkRouterInDB(t *testing.T, db *gorm.DB, routerID string) *routerTypes.NetworkRouter {
|
||||
t.Helper()
|
||||
var router routerTypes.NetworkRouter
|
||||
err := db.Where("id = ? AND account_id = ?", routerID, TestAccountId).First(&router).Error
|
||||
require.NoError(t, err, "Expected network router %s to exist in DB", routerID)
|
||||
return &router
|
||||
}
|
||||
|
||||
// VerifyNetworkRouterNotInDB verifies that a network router does not exist in the DB.
|
||||
func VerifyNetworkRouterNotInDB(t *testing.T, db *gorm.DB, routerID string) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
db.Model(&routerTypes.NetworkRouter{}).Where("id = ? AND account_id = ?", routerID, TestAccountId).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "Expected network router %s to NOT exist in DB", routerID)
|
||||
}
|
||||
@@ -197,6 +197,7 @@ func NewManager(ctx context.Context, config Config, appMetrics telemetry.AppMetr
|
||||
case "jumpcloud":
|
||||
return NewJumpCloudManager(JumpCloudClientConfig{
|
||||
APIToken: config.ExtraConfig["ApiToken"],
|
||||
ApiUrl: config.ExtraConfig["ApiUrl"],
|
||||
}, appMetrics)
|
||||
case "pocketid":
|
||||
return NewPocketIdManager(PocketIdClientConfig{
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/TheJumpCloud/jcapi-go/v1"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
accept = "application/json"
|
||||
jumpCloudDefaultApiUrl = "https://console.jumpcloud.com"
|
||||
jumpCloudSearchPageSize = 100
|
||||
)
|
||||
|
||||
// jumpCloudUser represents a JumpCloud V1 API system user.
|
||||
type jumpCloudUser struct {
|
||||
ID string `json:"_id"`
|
||||
Email string `json:"email"`
|
||||
Firstname string `json:"firstname"`
|
||||
Middlename string `json:"middlename"`
|
||||
Lastname string `json:"lastname"`
|
||||
}
|
||||
|
||||
// jumpCloudUserList represents the response from the JumpCloud search endpoint.
|
||||
type jumpCloudUserList struct {
|
||||
Results []jumpCloudUser `json:"results"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
}
|
||||
|
||||
// JumpCloudManager JumpCloud manager client instance.
|
||||
type JumpCloudManager struct {
|
||||
client *v1.APIClient
|
||||
apiBase string
|
||||
apiToken string
|
||||
httpClient ManagerHTTPClient
|
||||
credentials ManagerCredentials
|
||||
@@ -29,6 +45,7 @@ type JumpCloudManager struct {
|
||||
// JumpCloudClientConfig JumpCloud manager client configurations.
|
||||
type JumpCloudClientConfig struct {
|
||||
APIToken string
|
||||
ApiUrl string
|
||||
}
|
||||
|
||||
// JumpCloudCredentials JumpCloud authentication information.
|
||||
@@ -55,7 +72,15 @@ func NewJumpCloudManager(config JumpCloudClientConfig, appMetrics telemetry.AppM
|
||||
return nil, fmt.Errorf("jumpCloud IdP configuration is incomplete, ApiToken is missing")
|
||||
}
|
||||
|
||||
client := v1.NewAPIClient(v1.NewConfiguration())
|
||||
apiBase := config.ApiUrl
|
||||
if apiBase == "" {
|
||||
apiBase = jumpCloudDefaultApiUrl
|
||||
}
|
||||
apiBase = strings.TrimSuffix(apiBase, "/")
|
||||
if !strings.HasSuffix(apiBase, "/api") {
|
||||
apiBase += "/api"
|
||||
}
|
||||
|
||||
credentials := &JumpCloudCredentials{
|
||||
clientConfig: config,
|
||||
httpClient: httpClient,
|
||||
@@ -64,7 +89,7 @@ func NewJumpCloudManager(config JumpCloudClientConfig, appMetrics telemetry.AppM
|
||||
}
|
||||
|
||||
return &JumpCloudManager{
|
||||
client: client,
|
||||
apiBase: apiBase,
|
||||
apiToken: config.APIToken,
|
||||
httpClient: httpClient,
|
||||
credentials: credentials,
|
||||
@@ -78,37 +103,58 @@ func (jc *JumpCloudCredentials) Authenticate(_ context.Context) (JWTToken, error
|
||||
return JWTToken{}, nil
|
||||
}
|
||||
|
||||
func (jm *JumpCloudManager) authenticationContext() context.Context {
|
||||
return context.WithValue(context.Background(), v1.ContextAPIKey, v1.APIKey{
|
||||
Key: jm.apiToken,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
func (jm *JumpCloudManager) UpdateUserAppMetadata(_ context.Context, _ string, _ AppMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserDataByID requests user data from JumpCloud via ID.
|
||||
func (jm *JumpCloudManager) GetUserDataByID(_ context.Context, userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||
authCtx := jm.authenticationContext()
|
||||
user, resp, err := jm.client.SystemusersApi.SystemusersGet(authCtx, userID, contentType, accept, nil)
|
||||
// doRequest executes an HTTP request against the JumpCloud V1 API.
|
||||
func (jm *JumpCloudManager) doRequest(ctx context.Context, method, path string, body io.Reader) ([]byte, error) {
|
||||
reqURL := jm.apiBase + path
|
||||
req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("x-api-key", jm.apiToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := jm.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
||||
return nil, fmt.Errorf("JumpCloud API request %s %s failed with status %d", method, path, resp.StatusCode)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
func (jm *JumpCloudManager) UpdateUserAppMetadata(_ context.Context, _ string, _ AppMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserDataByID requests user data from JumpCloud via ID.
|
||||
func (jm *JumpCloudManager) GetUserDataByID(ctx context.Context, userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||
body, err := jm.doRequest(ctx, http.MethodGet, "/systemusers/"+userID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
||||
}
|
||||
|
||||
var user jumpCloudUser
|
||||
if err = jm.helper.Unmarshal(body, &user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userData := parseJumpCloudUser(user)
|
||||
userData.AppMetadata = appMetadata
|
||||
|
||||
@@ -116,30 +162,20 @@ func (jm *JumpCloudManager) GetUserDataByID(_ context.Context, userID string, ap
|
||||
}
|
||||
|
||||
// GetAccount returns all the users for a given profile.
|
||||
func (jm *JumpCloudManager) GetAccount(_ context.Context, accountID string) ([]*UserData, error) {
|
||||
authCtx := jm.authenticationContext()
|
||||
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
|
||||
func (jm *JumpCloudManager) GetAccount(ctx context.Context, accountID string) ([]*UserData, error) {
|
||||
allUsers, err := jm.searchAllUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get account %s users, statusCode %d", accountID, resp.StatusCode)
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountGetAccount()
|
||||
}
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
for _, user := range userList.Results {
|
||||
users := make([]*UserData, 0, len(allUsers))
|
||||
for _, user := range allUsers {
|
||||
userData := parseJumpCloudUser(user)
|
||||
userData.AppMetadata.WTAccountID = accountID
|
||||
|
||||
users = append(users, userData)
|
||||
}
|
||||
|
||||
@@ -148,27 +184,18 @@ func (jm *JumpCloudManager) GetAccount(_ context.Context, accountID string) ([]*
|
||||
|
||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||
// It returns a list of users indexed by accountID.
|
||||
func (jm *JumpCloudManager) GetAllAccounts(_ context.Context) (map[string][]*UserData, error) {
|
||||
authCtx := jm.authenticationContext()
|
||||
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
|
||||
func (jm *JumpCloudManager) GetAllAccounts(ctx context.Context) (map[string][]*UserData, error) {
|
||||
allUsers, err := jm.searchAllUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||
}
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
for _, user := range userList.Results {
|
||||
for _, user := range allUsers {
|
||||
userData := parseJumpCloudUser(user)
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||
}
|
||||
@@ -176,6 +203,41 @@ func (jm *JumpCloudManager) GetAllAccounts(_ context.Context) (map[string][]*Use
|
||||
return indexedUsers, nil
|
||||
}
|
||||
|
||||
// searchAllUsers paginates through all system users using limit/skip.
|
||||
func (jm *JumpCloudManager) searchAllUsers(ctx context.Context) ([]jumpCloudUser, error) {
|
||||
var allUsers []jumpCloudUser
|
||||
|
||||
for skip := 0; ; skip += jumpCloudSearchPageSize {
|
||||
searchReq := map[string]int{
|
||||
"limit": jumpCloudSearchPageSize,
|
||||
"skip": skip,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(searchReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := jm.doRequest(ctx, http.MethodPost, "/search/systemusers", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var userList jumpCloudUserList
|
||||
if err = jm.helper.Unmarshal(body, &userList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allUsers = append(allUsers, userList.Results...)
|
||||
|
||||
if skip+len(userList.Results) >= userList.TotalCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return allUsers, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in JumpCloud Idp and sends an invitation.
|
||||
func (jm *JumpCloudManager) CreateUser(_ context.Context, _, _, _, _ string) (*UserData, error) {
|
||||
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||
@@ -183,7 +245,7 @@ func (jm *JumpCloudManager) CreateUser(_ context.Context, _, _, _, _ string) (*U
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
// If no users have been found, this function returns an empty list.
|
||||
func (jm *JumpCloudManager) GetUserByEmail(_ context.Context, email string) ([]*UserData, error) {
|
||||
func (jm *JumpCloudManager) GetUserByEmail(ctx context.Context, email string) ([]*UserData, error) {
|
||||
searchFilter := map[string]interface{}{
|
||||
"searchFilter": map[string]interface{}{
|
||||
"filter": []string{email},
|
||||
@@ -191,25 +253,26 @@ func (jm *JumpCloudManager) GetUserByEmail(_ context.Context, email string) ([]*
|
||||
},
|
||||
}
|
||||
|
||||
authCtx := jm.authenticationContext()
|
||||
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, searchFilter)
|
||||
payload, err := json.Marshal(searchFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", email, resp.StatusCode)
|
||||
body, err := jm.doRequest(ctx, http.MethodPost, "/search/systemusers", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
||||
}
|
||||
|
||||
usersData := make([]*UserData, 0)
|
||||
var userList jumpCloudUserList
|
||||
if err = jm.helper.Unmarshal(body, &userList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usersData := make([]*UserData, 0, len(userList.Results))
|
||||
for _, user := range userList.Results {
|
||||
usersData = append(usersData, parseJumpCloudUser(user))
|
||||
}
|
||||
@@ -224,20 +287,11 @@ func (jm *JumpCloudManager) InviteUserByID(_ context.Context, _ string) error {
|
||||
}
|
||||
|
||||
// DeleteUser from jumpCloud directory
|
||||
func (jm *JumpCloudManager) DeleteUser(_ context.Context, userID string) error {
|
||||
authCtx := jm.authenticationContext()
|
||||
_, resp, err := jm.client.SystemusersApi.SystemusersDelete(authCtx, userID, contentType, accept, nil)
|
||||
func (jm *JumpCloudManager) DeleteUser(ctx context.Context, userID string) error {
|
||||
_, err := jm.doRequest(ctx, http.MethodDelete, "/systemusers/"+userID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||
}
|
||||
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if jm.appMetrics != nil {
|
||||
jm.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
@@ -247,11 +301,11 @@ func (jm *JumpCloudManager) DeleteUser(_ context.Context, userID string) error {
|
||||
}
|
||||
|
||||
// parseJumpCloudUser parse JumpCloud system user returned from API V1 to UserData.
|
||||
func parseJumpCloudUser(user v1.Systemuserreturn) *UserData {
|
||||
func parseJumpCloudUser(user jumpCloudUser) *UserData {
|
||||
names := []string{user.Firstname, user.Middlename, user.Lastname}
|
||||
return &UserData{
|
||||
Email: user.Email,
|
||||
Name: strings.Join(names, " "),
|
||||
ID: user.Id,
|
||||
ID: user.ID,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
@@ -44,3 +51,212 @@ func TestNewJumpCloudManager(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJumpCloudGetUserDataByID(t *testing.T) {
|
||||
userResponse := jumpCloudUser{
|
||||
ID: "user123",
|
||||
Email: "test@example.com",
|
||||
Firstname: "John",
|
||||
Middlename: "",
|
||||
Lastname: "Doe",
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/systemusers/user123", r.URL.Path)
|
||||
assert.Equal(t, http.MethodGet, r.Method)
|
||||
assert.Equal(t, "test-api-key", r.Header.Get("x-api-key"))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(userResponse)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
manager := newTestJumpCloudManager(t, server.URL)
|
||||
|
||||
userData, err := manager.GetUserDataByID(context.Background(), "user123", AppMetadata{WTAccountID: "acc1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "user123", userData.ID)
|
||||
assert.Equal(t, "test@example.com", userData.Email)
|
||||
assert.Equal(t, "John Doe", userData.Name)
|
||||
assert.Equal(t, "acc1", userData.AppMetadata.WTAccountID)
|
||||
}
|
||||
|
||||
func TestJumpCloudGetAccount(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/search/systemusers", r.URL.Path)
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
var reqBody map[string]any
|
||||
assert.NoError(t, json.NewDecoder(r.Body).Decode(&reqBody))
|
||||
assert.Contains(t, reqBody, "limit")
|
||||
assert.Contains(t, reqBody, "skip")
|
||||
|
||||
resp := jumpCloudUserList{
|
||||
Results: []jumpCloudUser{
|
||||
{ID: "u1", Email: "a@test.com", Firstname: "Alice", Lastname: "Smith"},
|
||||
{ID: "u2", Email: "b@test.com", Firstname: "Bob", Lastname: "Jones"},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
manager := newTestJumpCloudManager(t, server.URL)
|
||||
|
||||
users, err := manager.GetAccount(context.Background(), "testAccount")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, users, 2)
|
||||
assert.Equal(t, "testAccount", users[0].AppMetadata.WTAccountID)
|
||||
assert.Equal(t, "testAccount", users[1].AppMetadata.WTAccountID)
|
||||
}
|
||||
|
||||
func TestJumpCloudGetAllAccounts(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := jumpCloudUserList{
|
||||
Results: []jumpCloudUser{
|
||||
{ID: "u1", Email: "a@test.com", Firstname: "Alice"},
|
||||
{ID: "u2", Email: "b@test.com", Firstname: "Bob"},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
manager := newTestJumpCloudManager(t, server.URL)
|
||||
|
||||
indexedUsers, err := manager.GetAllAccounts(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, indexedUsers[UnsetAccountID], 2)
|
||||
}
|
||||
|
||||
func TestJumpCloudGetAllAccountsPagination(t *testing.T) {
|
||||
totalUsers := 250
|
||||
allUsers := make([]jumpCloudUser, totalUsers)
|
||||
for i := range allUsers {
|
||||
allUsers[i] = jumpCloudUser{
|
||||
ID: fmt.Sprintf("u%d", i),
|
||||
Email: fmt.Sprintf("user%d@test.com", i),
|
||||
Firstname: fmt.Sprintf("User%d", i),
|
||||
}
|
||||
}
|
||||
|
||||
requestCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var reqBody map[string]int
|
||||
assert.NoError(t, json.NewDecoder(r.Body).Decode(&reqBody))
|
||||
|
||||
limit := reqBody["limit"]
|
||||
skip := reqBody["skip"]
|
||||
requestCount++
|
||||
|
||||
end := skip + limit
|
||||
if end > totalUsers {
|
||||
end = totalUsers
|
||||
}
|
||||
|
||||
resp := jumpCloudUserList{
|
||||
Results: allUsers[skip:end],
|
||||
TotalCount: totalUsers,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
manager := newTestJumpCloudManager(t, server.URL)
|
||||
|
||||
indexedUsers, err := manager.GetAllAccounts(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, indexedUsers[UnsetAccountID], totalUsers)
|
||||
assert.Equal(t, 3, requestCount, "should require 3 pages for 250 users at page size 100")
|
||||
}
|
||||
|
||||
func TestJumpCloudGetUserByEmail(t *testing.T) {
|
||||
searchResponse := jumpCloudUserList{
|
||||
Results: []jumpCloudUser{
|
||||
{ID: "u1", Email: "alice@test.com", Firstname: "Alice", Lastname: "Smith"},
|
||||
},
|
||||
TotalCount: 1,
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/search/systemusers", r.URL.Path)
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(body), "alice@test.com")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(searchResponse)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
manager := newTestJumpCloudManager(t, server.URL)
|
||||
|
||||
users, err := manager.GetUserByEmail(context.Background(), "alice@test.com")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, users, 1)
|
||||
assert.Equal(t, "alice@test.com", users[0].Email)
|
||||
}
|
||||
|
||||
func TestJumpCloudDeleteUser(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/systemusers/user123", r.URL.Path)
|
||||
assert.Equal(t, http.MethodDelete, r.Method)
|
||||
assert.Equal(t, "test-api-key", r.Header.Get("x-api-key"))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"_id": "user123"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
manager := newTestJumpCloudManager(t, server.URL)
|
||||
|
||||
err := manager.DeleteUser(context.Background(), "user123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestJumpCloudAPIError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
manager := newTestJumpCloudManager(t, server.URL)
|
||||
|
||||
_, err := manager.GetUserDataByID(context.Background(), "user123", AppMetadata{})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "401")
|
||||
}
|
||||
|
||||
func TestParseJumpCloudUser(t *testing.T) {
|
||||
user := jumpCloudUser{
|
||||
ID: "abc123",
|
||||
Email: "test@example.com",
|
||||
Firstname: "John",
|
||||
Middlename: "M",
|
||||
Lastname: "Doe",
|
||||
}
|
||||
|
||||
userData := parseJumpCloudUser(user)
|
||||
assert.Equal(t, "abc123", userData.ID)
|
||||
assert.Equal(t, "test@example.com", userData.Email)
|
||||
assert.Equal(t, "John M Doe", userData.Name)
|
||||
}
|
||||
|
||||
func newTestJumpCloudManager(t *testing.T, apiBase string) *JumpCloudManager {
|
||||
t.Helper()
|
||||
return &JumpCloudManager{
|
||||
apiBase: apiBase,
|
||||
apiToken: "test-api-key",
|
||||
httpClient: http.DefaultClient,
|
||||
helper: JsonParser{},
|
||||
appMetrics: nil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
|
||||
if err != nil {
|
||||
newLabel = ""
|
||||
} else {
|
||||
_, err := transaction.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, update.Name)
|
||||
_, err := transaction.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, newLabel)
|
||||
if err == nil {
|
||||
newLabel = ""
|
||||
}
|
||||
@@ -859,7 +859,9 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
|
||||
opEvent.Meta["setup_key_name"] = peerAddConfig.SetupKeyName
|
||||
}
|
||||
|
||||
am.StoreEvent(ctx, opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta)
|
||||
if !temporary {
|
||||
am.StoreEvent(ctx, opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta)
|
||||
}
|
||||
|
||||
if err := am.networkMapController.OnPeersAdded(ctx, accountID, []string{newPeer.ID}); err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to update network map cache for peer %s: %v", newPeer.ID, err)
|
||||
@@ -1480,9 +1482,11 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto
|
||||
if err = transaction.DeletePeer(ctx, accountID, peer.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peerDeletedEvents = append(peerDeletedEvents, func() {
|
||||
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(dnsDomain))
|
||||
})
|
||||
if !(peer.ProxyMeta.Embedded || peer.Meta.KernelVersion == "wasm") {
|
||||
peerDeletedEvents = append(peerDeletedEvents, func() {
|
||||
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerRemovedByUser, peer.EventMeta(dnsDomain))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return peerDeletedEvents, nil
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/job"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/management/server/settings"
|
||||
"github.com/netbirdio/netbird/shared/auth"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/util"
|
||||
@@ -2738,3 +2739,70 @@ func TestProcessPeerAddAuth(t *testing.T) {
|
||||
assert.Empty(t, config.GroupsToAdd)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdatePeer_DnsLabelCollisionWithFQDN(t *testing.T) {
|
||||
manager, _, err := createManager(t)
|
||||
require.NoError(t, err, "unable to create account manager")
|
||||
|
||||
accountID, err := manager.GetAccountIDByUserID(context.Background(), auth.UserAuth{UserId: userID})
|
||||
require.NoError(t, err, "unable to create an account")
|
||||
|
||||
// Add first peer with hostname that produces DNS label "netbird1"
|
||||
key1, err := wgtypes.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
peer1, _, _, err := manager.AddPeer(context.Background(), "", "", userID, &nbpeer.Peer{
|
||||
Key: key1.PublicKey().String(),
|
||||
Meta: nbpeer.PeerSystemMeta{Hostname: "netbird1.netbird.cloud"},
|
||||
}, false)
|
||||
require.NoError(t, err, "unable to add first peer")
|
||||
assert.Equal(t, "netbird1", peer1.DNSLabel)
|
||||
|
||||
// Add second peer with a different hostname
|
||||
key2, err := wgtypes.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
peer2, _, _, err := manager.AddPeer(context.Background(), "", "", userID, &nbpeer.Peer{
|
||||
Key: key2.PublicKey().String(),
|
||||
Meta: nbpeer.PeerSystemMeta{Hostname: "ip-10-29-5-130"},
|
||||
}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
update := peer2.Copy()
|
||||
update.Name = "netbird1.demo.netbird.cloud"
|
||||
updated, err := manager.UpdatePeer(context.Background(), accountID, userID, update)
|
||||
require.NoError(t, err, "renaming peer should not fail with duplicate DNS label error")
|
||||
assert.Equal(t, "netbird1.demo.netbird.cloud", updated.Name)
|
||||
assert.NotEqual(t, "netbird1", updated.DNSLabel, "DNS label should not collide with existing peer")
|
||||
assert.Contains(t, updated.DNSLabel, "netbird1-", "DNS label should be IP-based fallback")
|
||||
}
|
||||
|
||||
func TestUpdatePeer_DnsLabelUniqueName(t *testing.T) {
|
||||
manager, _, err := createManager(t)
|
||||
require.NoError(t, err, "unable to create account manager")
|
||||
|
||||
accountID, err := manager.GetAccountIDByUserID(context.Background(), auth.UserAuth{UserId: userID})
|
||||
require.NoError(t, err, "unable to create an account")
|
||||
|
||||
key1, err := wgtypes.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
peer1, _, _, err := manager.AddPeer(context.Background(), "", "", userID, &nbpeer.Peer{
|
||||
Key: key1.PublicKey().String(),
|
||||
Meta: nbpeer.PeerSystemMeta{Hostname: "web-server"},
|
||||
}, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "web-server", peer1.DNSLabel)
|
||||
|
||||
// Add second peer and rename it to a unique FQDN whose first label doesn't collide
|
||||
key2, err := wgtypes.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
peer2, _, _, err := manager.AddPeer(context.Background(), "", "", userID, &nbpeer.Peer{
|
||||
Key: key2.PublicKey().String(),
|
||||
Meta: nbpeer.PeerSystemMeta{Hostname: "old-name"},
|
||||
}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
update := peer2.Copy()
|
||||
update.Name = "api-server.example.com"
|
||||
updated, err := manager.UpdatePeer(context.Background(), accountID, userID, update)
|
||||
require.NoError(t, err, "renaming to unique FQDN should succeed")
|
||||
assert.Equal(t, "api-server", updated.DNSLabel, "DNS label should be first label of FQDN")
|
||||
}
|
||||
|
||||
@@ -4997,7 +4997,6 @@ func (s *SqlStore) GetServiceByDomain(ctx context.Context, domain string) (*rpse
|
||||
return service, nil
|
||||
}
|
||||
|
||||
|
||||
func (s *SqlStore) GetServices(ctx context.Context, lockStrength LockingStrength) ([]*rpservice.Service, error) {
|
||||
tx := s.db.Preload("Targets")
|
||||
if lockStrength != LockingStrengthNone {
|
||||
@@ -5408,17 +5407,35 @@ func (s *SqlStore) SaveProxy(ctx context.Context, p *proxy.Proxy) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProxyHeartbeat updates the last_seen timestamp for a proxy
|
||||
func (s *SqlStore) UpdateProxyHeartbeat(ctx context.Context, proxyID string) error {
|
||||
// UpdateProxyHeartbeat updates the last_seen timestamp for a proxy or creates a new entry if it doesn't exist
|
||||
func (s *SqlStore) UpdateProxyHeartbeat(ctx context.Context, proxyID, clusterAddress, ipAddress string) error {
|
||||
now := time.Now()
|
||||
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&proxy.Proxy{}).
|
||||
Where("id = ? AND status = ?", proxyID, "connected").
|
||||
Update("last_seen", time.Now())
|
||||
Update("last_seen", now)
|
||||
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to update proxy heartbeat: %v", result.Error)
|
||||
return status.Errorf(status.Internal, "failed to update proxy heartbeat")
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
p := &proxy.Proxy{
|
||||
ID: proxyID,
|
||||
ClusterAddress: clusterAddress,
|
||||
IPAddress: ipAddress,
|
||||
LastSeen: now,
|
||||
ConnectedAt: &now,
|
||||
Status: "connected",
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Save(p).Error; err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to create proxy on heartbeat: %v", err)
|
||||
return status.Errorf(status.Internal, "failed to create proxy on heartbeat")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5428,7 +5445,7 @@ func (s *SqlStore) GetActiveProxyClusterAddresses(ctx context.Context) ([]string
|
||||
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&proxy.Proxy{}).
|
||||
Where("status = ? AND last_seen > ?", "connected", time.Now().Add(-2*time.Minute)).
|
||||
Where("status = ? AND last_seen > ?", "connected", time.Now().Add(-proxyActiveThreshold)).
|
||||
Distinct("cluster_address").
|
||||
Pluck("cluster_address", &addresses)
|
||||
|
||||
@@ -5440,6 +5457,81 @@ func (s *SqlStore) GetActiveProxyClusterAddresses(ctx context.Context) ([]string
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
// GetActiveProxyClusters returns all active proxy clusters with their connected proxy count.
|
||||
func (s *SqlStore) GetActiveProxyClusters(ctx context.Context) ([]proxy.Cluster, error) {
|
||||
var clusters []proxy.Cluster
|
||||
|
||||
result := s.db.Model(&proxy.Proxy{}).
|
||||
Select("cluster_address as address, COUNT(*) as connected_proxies").
|
||||
Where("status = ? AND last_seen > ?", "connected", time.Now().Add(-proxyActiveThreshold)).
|
||||
Group("cluster_address").
|
||||
Scan(&clusters)
|
||||
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get active proxy clusters: %v", result.Error)
|
||||
return nil, status.Errorf(status.Internal, "get active proxy clusters")
|
||||
}
|
||||
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
// proxyActiveThreshold is the maximum age of a heartbeat for a proxy to be
|
||||
// considered active. Must be at least 2x the heartbeat interval (1 min).
|
||||
const proxyActiveThreshold = 2 * time.Minute
|
||||
|
||||
var validCapabilityColumns = map[string]struct{}{
|
||||
"supports_custom_ports": {},
|
||||
"require_subdomain": {},
|
||||
}
|
||||
|
||||
// GetClusterSupportsCustomPorts returns whether any active proxy in the cluster
|
||||
// supports custom ports. Returns nil when no proxy reported the capability.
|
||||
func (s *SqlStore) GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool {
|
||||
return s.getClusterCapability(ctx, clusterAddr, "supports_custom_ports")
|
||||
}
|
||||
|
||||
// GetClusterRequireSubdomain returns whether any active proxy in the cluster
|
||||
// requires a subdomain. Returns nil when no proxy reported the capability.
|
||||
func (s *SqlStore) GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool {
|
||||
return s.getClusterCapability(ctx, clusterAddr, "require_subdomain")
|
||||
}
|
||||
|
||||
// getClusterCapability returns an aggregated boolean capability for the given
|
||||
// cluster. It checks active (connected, recently seen) proxies and returns:
|
||||
// - *true if any proxy in the cluster has the capability set to true,
|
||||
// - *false if at least one proxy reported but none set it to true,
|
||||
// - nil if no proxy reported the capability at all.
|
||||
func (s *SqlStore) getClusterCapability(ctx context.Context, clusterAddr, column string) *bool {
|
||||
if _, ok := validCapabilityColumns[column]; !ok {
|
||||
log.WithContext(ctx).Errorf("invalid capability column: %s", column)
|
||||
return nil
|
||||
}
|
||||
|
||||
var result struct {
|
||||
HasCapability bool
|
||||
AnyTrue bool
|
||||
}
|
||||
|
||||
err := s.db.WithContext(ctx).
|
||||
Model(&proxy.Proxy{}).
|
||||
Select("COUNT(CASE WHEN "+column+" IS NOT NULL THEN 1 END) > 0 AS has_capability, "+
|
||||
"COALESCE(MAX(CASE WHEN "+column+" = true THEN 1 ELSE 0 END), 0) = 1 AS any_true").
|
||||
Where("cluster_address = ? AND status = ? AND last_seen > ?",
|
||||
clusterAddr, "connected", time.Now().Add(-proxyActiveThreshold)).
|
||||
Scan(&result).Error
|
||||
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("query cluster capability %s for %s: %v", column, clusterAddr, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !result.HasCapability {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &result.AnyTrue
|
||||
}
|
||||
|
||||
// CleanupStaleProxies deletes proxies that haven't sent heartbeat in the specified duration
|
||||
func (s *SqlStore) CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error {
|
||||
cutoffTime := time.Now().Add(-inactivityDuration)
|
||||
@@ -5459,3 +5551,61 @@ func (s *SqlStore) CleanupStaleProxies(ctx context.Context, inactivityDuration t
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRoutingPeerNetworks returns the distinct network names where the peer is assigned as a routing peer
|
||||
// in an enabled network router, either directly or via peer groups.
|
||||
func (s *SqlStore) GetRoutingPeerNetworks(_ context.Context, accountID, peerID string) ([]string, error) {
|
||||
var routers []*routerTypes.NetworkRouter
|
||||
if err := s.db.Select("peer, peer_groups, network_id").Where("account_id = ? AND enabled = true", accountID).Find(&routers).Error; err != nil {
|
||||
return nil, status.Errorf(status.Internal, "failed to get enabled routers: %v", err)
|
||||
}
|
||||
|
||||
if len(routers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var groupPeers []types.GroupPeer
|
||||
if err := s.db.Select("group_id").Where("account_id = ? AND peer_id = ?", accountID, peerID).Find(&groupPeers).Error; err != nil {
|
||||
return nil, status.Errorf(status.Internal, "failed to get peer group memberships: %v", err)
|
||||
}
|
||||
|
||||
groupSet := make(map[string]struct{}, len(groupPeers))
|
||||
for _, gp := range groupPeers {
|
||||
groupSet[gp.GroupID] = struct{}{}
|
||||
}
|
||||
|
||||
networkIDs := make(map[string]struct{})
|
||||
for _, r := range routers {
|
||||
if r.Peer == peerID {
|
||||
networkIDs[r.NetworkID] = struct{}{}
|
||||
} else if r.Peer == "" {
|
||||
for _, pg := range r.PeerGroups {
|
||||
if _, ok := groupSet[pg]; ok {
|
||||
networkIDs[r.NetworkID] = struct{}{}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(networkIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(networkIDs))
|
||||
for id := range networkIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
var networks []*networkTypes.Network
|
||||
if err := s.db.Select("name").Where("account_id = ? AND id IN ?", accountID, ids).Find(&networks).Error; err != nil {
|
||||
return nil, status.Errorf(status.Internal, "failed to get networks: %v", err)
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(networks))
|
||||
for _, n := range networks {
|
||||
names = append(names, n.Name)
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
@@ -284,11 +284,16 @@ type Store interface {
|
||||
DeleteServiceTargets(ctx context.Context, accountID string, serviceID string) error
|
||||
|
||||
SaveProxy(ctx context.Context, proxy *proxy.Proxy) error
|
||||
UpdateProxyHeartbeat(ctx context.Context, proxyID string) error
|
||||
UpdateProxyHeartbeat(ctx context.Context, proxyID, clusterAddress, ipAddress string) error
|
||||
GetActiveProxyClusterAddresses(ctx context.Context) ([]string, error)
|
||||
GetActiveProxyClusters(ctx context.Context) ([]proxy.Cluster, error)
|
||||
GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||
GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||
CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error
|
||||
|
||||
GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)
|
||||
|
||||
GetRoutingPeerNetworks(ctx context.Context, accountID, peerID string) ([]string, error)
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -165,6 +165,34 @@ func (mr *MockStoreMockRecorder) CleanupStaleProxies(ctx, inactivityDuration int
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanupStaleProxies", reflect.TypeOf((*MockStore)(nil).CleanupStaleProxies), ctx, inactivityDuration)
|
||||
}
|
||||
|
||||
// GetClusterSupportsCustomPorts mocks base method.
|
||||
func (m *MockStore) GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetClusterSupportsCustomPorts", ctx, clusterAddr)
|
||||
ret0, _ := ret[0].(*bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetClusterSupportsCustomPorts indicates an expected call of GetClusterSupportsCustomPorts.
|
||||
func (mr *MockStoreMockRecorder) GetClusterSupportsCustomPorts(ctx, clusterAddr interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterSupportsCustomPorts", reflect.TypeOf((*MockStore)(nil).GetClusterSupportsCustomPorts), ctx, clusterAddr)
|
||||
}
|
||||
|
||||
// GetClusterRequireSubdomain mocks base method.
|
||||
func (m *MockStore) GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetClusterRequireSubdomain", ctx, clusterAddr)
|
||||
ret0, _ := ret[0].(*bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetClusterRequireSubdomain indicates an expected call of GetClusterRequireSubdomain.
|
||||
func (mr *MockStoreMockRecorder) GetClusterRequireSubdomain(ctx, clusterAddr interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterRequireSubdomain", reflect.TypeOf((*MockStore)(nil).GetClusterRequireSubdomain), ctx, clusterAddr)
|
||||
}
|
||||
|
||||
// Close mocks base method.
|
||||
func (m *MockStore) Close(ctx context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1287,6 +1315,21 @@ func (mr *MockStoreMockRecorder) GetActiveProxyClusterAddresses(ctx interface{})
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveProxyClusterAddresses", reflect.TypeOf((*MockStore)(nil).GetActiveProxyClusterAddresses), ctx)
|
||||
}
|
||||
|
||||
// GetActiveProxyClusters mocks base method.
|
||||
func (m *MockStore) GetActiveProxyClusters(ctx context.Context) ([]proxy.Cluster, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetActiveProxyClusters", ctx)
|
||||
ret0, _ := ret[0].([]proxy.Cluster)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetActiveProxyClusters indicates an expected call of GetActiveProxyClusters.
|
||||
func (mr *MockStoreMockRecorder) GetActiveProxyClusters(ctx interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveProxyClusters", reflect.TypeOf((*MockStore)(nil).GetActiveProxyClusters), ctx)
|
||||
}
|
||||
|
||||
// GetAllAccounts mocks base method.
|
||||
func (m *MockStore) GetAllAccounts(ctx context.Context) []*types2.Account {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2318,6 +2361,21 @@ func (mr *MockStoreMockRecorder) IncrementSetupKeyUsage(ctx, setupKeyID interfac
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementSetupKeyUsage", reflect.TypeOf((*MockStore)(nil).IncrementSetupKeyUsage), ctx, setupKeyID)
|
||||
}
|
||||
|
||||
// GetRoutingPeerNetworks mocks base method.
|
||||
func (m *MockStore) GetRoutingPeerNetworks(ctx context.Context, accountID, peerID string) ([]string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetRoutingPeerNetworks", ctx, accountID, peerID)
|
||||
ret0, _ := ret[0].([]string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetRoutingPeerNetworks indicates an expected call of GetRoutingPeerNetworks.
|
||||
func (mr *MockStoreMockRecorder) GetRoutingPeerNetworks(ctx, accountID, peerID interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoutingPeerNetworks", reflect.TypeOf((*MockStore)(nil).GetRoutingPeerNetworks), ctx, accountID, peerID)
|
||||
}
|
||||
|
||||
// IsPrimaryAccount mocks base method.
|
||||
func (m *MockStore) IsPrimaryAccount(ctx context.Context, accountID string) (bool, string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2924,17 +2982,17 @@ func (mr *MockStoreMockRecorder) UpdateGroups(ctx, accountID, groups interface{}
|
||||
}
|
||||
|
||||
// UpdateProxyHeartbeat mocks base method.
|
||||
func (m *MockStore) UpdateProxyHeartbeat(ctx context.Context, proxyID string) error {
|
||||
func (m *MockStore) UpdateProxyHeartbeat(ctx context.Context, proxyID, clusterAddress, ipAddress string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateProxyHeartbeat", ctx, proxyID)
|
||||
ret := m.ctrl.Call(m, "UpdateProxyHeartbeat", ctx, proxyID, clusterAddress, ipAddress)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateProxyHeartbeat indicates an expected call of UpdateProxyHeartbeat.
|
||||
func (mr *MockStoreMockRecorder) UpdateProxyHeartbeat(ctx, proxyID interface{}) *gomock.Call {
|
||||
func (mr *MockStoreMockRecorder) UpdateProxyHeartbeat(ctx, proxyID, clusterAddress, ipAddress interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProxyHeartbeat", reflect.TypeOf((*MockStore)(nil).UpdateProxyHeartbeat), ctx, proxyID)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProxyHeartbeat", reflect.TypeOf((*MockStore)(nil).UpdateProxyHeartbeat), ctx, proxyID, clusterAddress, ipAddress)
|
||||
}
|
||||
|
||||
// UpdateService mocks base method.
|
||||
|
||||
@@ -84,6 +84,12 @@ func setupTestAccount() *Account {
|
||||
},
|
||||
},
|
||||
Groups: map[string]*Group{
|
||||
"groupAll": {
|
||||
ID: "groupAll",
|
||||
Name: "All",
|
||||
Peers: []string{"peer1", "peer2", "peer3", "peer11", "peer12", "peer21", "peer31", "peer32", "peer41", "peer51", "peer61"},
|
||||
Issued: GroupIssuedAPI,
|
||||
},
|
||||
"group1": {
|
||||
ID: "group1",
|
||||
Peers: []string{"peer11", "peer12"},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+11093
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user