mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-20 21:59:07 +02:00
Merge branch 'main' into feat/auto-upgrade
This commit is contained in:
+1
-1
@@ -227,7 +227,7 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update host's static platform and system information
|
// update host's static platform and system information
|
||||||
system.UpdateStaticInfo()
|
system.UpdateStaticInfoAsync()
|
||||||
|
|
||||||
configFilePath, err := activeProf.FilePath()
|
configFilePath, err := activeProf.FilePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
log.Info("starting NetBird service") //nolint
|
log.Info("starting NetBird service") //nolint
|
||||||
|
|
||||||
// Collect static system and platform information
|
// Collect static system and platform information
|
||||||
system.UpdateStaticInfo()
|
system.UpdateStaticInfoAsync()
|
||||||
|
|
||||||
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
||||||
p.serv = grpc.NewServer()
|
p.serv = grpc.NewServer()
|
||||||
|
|||||||
+16
-14
@@ -9,29 +9,26 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
clientProto "github.com/netbirdio/netbird/client/proto"
|
||||||
|
client "github.com/netbirdio/netbird/client/server"
|
||||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
mgmt "github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
"github.com/netbirdio/netbird/management/server/groups"
|
"github.com/netbirdio/netbird/management/server/groups"
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||||
|
"github.com/netbirdio/netbird/management/server/peers"
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
|
||||||
|
|
||||||
clientProto "github.com/netbirdio/netbird/client/proto"
|
|
||||||
client "github.com/netbirdio/netbird/client/server"
|
|
||||||
mgmt "github.com/netbirdio/netbird/management/server"
|
|
||||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
sigProto "github.com/netbirdio/netbird/shared/signal/proto"
|
sigProto "github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
sig "github.com/netbirdio/netbird/signal/server"
|
sig "github.com/netbirdio/netbird/signal/server"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func startTestingServices(t *testing.T) string {
|
func startTestingServices(t *testing.T) string {
|
||||||
@@ -90,15 +87,20 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
iv, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
t.Cleanup(ctrl.Finish)
|
t.Cleanup(ctrl.Finish)
|
||||||
|
|
||||||
settingsMockManager := settings.NewMockManager(ctrl)
|
|
||||||
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
||||||
|
peersmanager := peers.NewManager(store, permissionsManagerMock)
|
||||||
|
settingsManagerMock := settings.NewMockManager(ctrl)
|
||||||
|
|
||||||
|
iv, _ := integrations.NewIntegratedValidator(context.Background(), peersmanager, settingsManagerMock, eventStore)
|
||||||
|
|
||||||
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
settingsMockManager := settings.NewMockManager(ctrl)
|
||||||
groupsManager := groups.NewManagerMock()
|
groupsManager := groups.NewManagerMock()
|
||||||
|
|
||||||
settingsMockManager.EXPECT().
|
settingsMockManager.EXPECT().
|
||||||
|
|||||||
@@ -19,17 +19,13 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
|
|
||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
|
||||||
"github.com/netbirdio/netbird/management/server/groups"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
@@ -45,9 +41,12 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/ssh"
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/groups"
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||||
|
"github.com/netbirdio/netbird/management/server/peers"
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
@@ -1555,7 +1554,11 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
ia, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
|
||||||
|
permissionsManager := permissions.NewManager(store)
|
||||||
|
peersManager := peers.NewManager(store, permissionsManager)
|
||||||
|
|
||||||
|
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1572,7 +1575,6 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
Return(&types.ExtraSettings{}, nil).
|
Return(&types.ExtraSettings{}, nil).
|
||||||
AnyTimes()
|
AnyTimes()
|
||||||
|
|
||||||
permissionsManager := permissions.NewManager(store)
|
|
||||||
groupsManager := groups.NewManagerMock()
|
groupsManager := groups.NewManagerMock()
|
||||||
|
|
||||||
accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false)
|
accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false)
|
||||||
|
|||||||
@@ -43,13 +43,6 @@ type OfferAnswer struct {
|
|||||||
SessionID *ICESessionID
|
SessionID *ICESessionID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oa *OfferAnswer) SessionIDString() string {
|
|
||||||
if oa.SessionID == nil {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
return oa.SessionID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handshaker struct {
|
type Handshaker struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
@@ -57,7 +50,7 @@ type Handshaker struct {
|
|||||||
signaler *Signaler
|
signaler *Signaler
|
||||||
ice *WorkerICE
|
ice *WorkerICE
|
||||||
relay *WorkerRelay
|
relay *WorkerRelay
|
||||||
onNewOfferListeners []func(*OfferAnswer)
|
onNewOfferListeners []*OfferListener
|
||||||
|
|
||||||
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
|
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
|
||||||
remoteOffersCh chan OfferAnswer
|
remoteOffersCh chan OfferAnswer
|
||||||
@@ -78,7 +71,8 @@ func NewHandshaker(log *log.Entry, config ConnConfig, signaler *Signaler, ice *W
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handshaker) AddOnNewOfferListener(offer func(remoteOfferAnswer *OfferAnswer)) {
|
func (h *Handshaker) AddOnNewOfferListener(offer func(remoteOfferAnswer *OfferAnswer)) {
|
||||||
h.onNewOfferListeners = append(h.onNewOfferListeners, offer)
|
l := NewOfferListener(offer)
|
||||||
|
h.onNewOfferListeners = append(h.onNewOfferListeners, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handshaker) Listen(ctx context.Context) {
|
func (h *Handshaker) Listen(ctx context.Context) {
|
||||||
@@ -91,13 +85,13 @@ func (h *Handshaker) Listen(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, listener := range h.onNewOfferListeners {
|
for _, listener := range h.onNewOfferListeners {
|
||||||
listener(&remoteOfferAnswer)
|
listener.Notify(&remoteOfferAnswer)
|
||||||
}
|
}
|
||||||
h.log.Infof("received offer, running version %s, remote WireGuard listen port %d, session id: %s", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString())
|
h.log.Infof("received offer, running version %s, remote WireGuard listen port %d, session id: %s", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString())
|
||||||
case remoteOfferAnswer := <-h.remoteAnswerCh:
|
case remoteOfferAnswer := <-h.remoteAnswerCh:
|
||||||
h.log.Infof("received answer, running version %s, remote WireGuard listen port %d, session id: %s", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString())
|
h.log.Infof("received answer, running version %s, remote WireGuard listen port %d, session id: %s", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString())
|
||||||
for _, listener := range h.onNewOfferListeners {
|
for _, listener := range h.onNewOfferListeners {
|
||||||
listener(&remoteOfferAnswer)
|
listener.Notify(&remoteOfferAnswer)
|
||||||
}
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
h.log.Infof("stop listening for remote offers and answers")
|
h.log.Infof("stop listening for remote offers and answers")
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type callbackFunc func(remoteOfferAnswer *OfferAnswer)
|
||||||
|
|
||||||
|
func (oa *OfferAnswer) SessionIDString() string {
|
||||||
|
if oa.SessionID == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
return oa.SessionID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type OfferListener struct {
|
||||||
|
fn callbackFunc
|
||||||
|
running bool
|
||||||
|
latest *OfferAnswer
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOfferListener(fn callbackFunc) *OfferListener {
|
||||||
|
return &OfferListener{
|
||||||
|
fn: fn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OfferListener) Notify(remoteOfferAnswer *OfferAnswer) {
|
||||||
|
o.mu.Lock()
|
||||||
|
defer o.mu.Unlock()
|
||||||
|
|
||||||
|
// Store the latest offer
|
||||||
|
o.latest = remoteOfferAnswer
|
||||||
|
|
||||||
|
// If already running, the running goroutine will pick up this latest value
|
||||||
|
if o.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing
|
||||||
|
o.running = true
|
||||||
|
|
||||||
|
// Process in a goroutine to avoid blocking the caller
|
||||||
|
go func(remoteOfferAnswer *OfferAnswer) {
|
||||||
|
for {
|
||||||
|
o.fn(remoteOfferAnswer)
|
||||||
|
|
||||||
|
o.mu.Lock()
|
||||||
|
if o.latest == nil {
|
||||||
|
// No more work to do
|
||||||
|
o.running = false
|
||||||
|
o.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remoteOfferAnswer = o.latest
|
||||||
|
// Clear the latest to mark it as being processed
|
||||||
|
o.latest = nil
|
||||||
|
o.mu.Unlock()
|
||||||
|
}
|
||||||
|
}(remoteOfferAnswer)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_newOfferListener(t *testing.T) {
|
||||||
|
dummyOfferAnswer := &OfferAnswer{}
|
||||||
|
runChan := make(chan struct{}, 10)
|
||||||
|
|
||||||
|
longRunningFn := func(remoteOfferAnswer *OfferAnswer) {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
runChan <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
hl := NewOfferListener(longRunningFn)
|
||||||
|
|
||||||
|
hl.Notify(dummyOfferAnswer)
|
||||||
|
hl.Notify(dummyOfferAnswer)
|
||||||
|
hl.Notify(dummyOfferAnswer)
|
||||||
|
|
||||||
|
// Wait for exactly 2 callbacks
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
select {
|
||||||
|
case <-runChan:
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("Timeout waiting for callback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no additional callbacks happen
|
||||||
|
select {
|
||||||
|
case <-runChan:
|
||||||
|
t.Fatal("Unexpected additional callback")
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Log("Correctly received exactly 2 callbacks")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,7 +122,6 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
w.log.Warnf("failed to close ICE agent: %s", err)
|
w.log.Warnf("failed to close ICE agent: %s", err)
|
||||||
}
|
}
|
||||||
w.agent = nil
|
w.agent = nil
|
||||||
// todo consider to switch to Relay connection while establishing a new ICE connection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var preferredCandidateTypes []ice.CandidateType
|
var preferredCandidateTypes []ice.CandidateType
|
||||||
@@ -410,7 +409,10 @@ func (w *WorkerICE) onConnectionStateChange(agent *icemaker.ThreadSafeAgent, dia
|
|||||||
case ice.ConnectionStateConnected:
|
case ice.ConnectionStateConnected:
|
||||||
w.lastKnownState = ice.ConnectionStateConnected
|
w.lastKnownState = ice.ConnectionStateConnected
|
||||||
return
|
return
|
||||||
case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected:
|
case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected, ice.ConnectionStateClosed:
|
||||||
|
// ice.ConnectionStateClosed happens when we recreate the agent. For the P2P to TURN switch important to
|
||||||
|
// notify the conn.onICEStateDisconnected changes to update the current used priority
|
||||||
|
|
||||||
if w.lastKnownState == ice.ConnectionStateConnected {
|
if w.lastKnownState == ice.ConnectionStateConnected {
|
||||||
w.lastKnownState = ice.ConnectionStateDisconnected
|
w.lastKnownState = ice.ConnectionStateDisconnected
|
||||||
w.conn.onICEStateDisconnected()
|
w.conn.onICEStateDisconnected()
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func NewNetWithDiscover(iFaceDiscover ExternalIFaceDiscover, disallowList []stri
|
|||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
n.iFaceDiscover = pionDiscover{}
|
n.iFaceDiscover = pionDiscover{}
|
||||||
} else {
|
} else {
|
||||||
newMobileIFaceDiscover(iFaceDiscover)
|
n.iFaceDiscover = newMobileIFaceDiscover(iFaceDiscover)
|
||||||
}
|
}
|
||||||
return n, n.UpdateInterfaces()
|
return n, n.UpdateInterfaces()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,25 +10,24 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
|
||||||
"github.com/netbirdio/netbird/management/server/groups"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
|
|
||||||
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
daemonProto "github.com/netbirdio/netbird/client/proto"
|
daemonProto "github.com/netbirdio/netbird/client/proto"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/groups"
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||||
|
"github.com/netbirdio/netbird/management/server/peers"
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
@@ -294,15 +293,20 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
ia, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
t.Cleanup(ctrl.Finish)
|
||||||
|
|
||||||
|
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
||||||
|
peersManager := peers.NewManager(store, permissionsManagerMock)
|
||||||
|
settingsManagerMock := settings.NewMockManager(ctrl)
|
||||||
|
|
||||||
|
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, settingsManagerMock, eventStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
t.Cleanup(ctrl.Finish)
|
|
||||||
settingsMockManager := settings.NewMockManager(ctrl)
|
settingsMockManager := settings.NewMockManager(ctrl)
|
||||||
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
|
||||||
groupsManager := groups.NewManagerMock()
|
groupsManager := groups.NewManagerMock()
|
||||||
|
|
||||||
accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false)
|
accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false)
|
||||||
|
|||||||
@@ -95,14 +95,6 @@ func (i *Info) SetFlags(
|
|||||||
i.LazyConnectionEnabled = lazyConnectionEnabled
|
i.LazyConnectionEnabled = lazyConnectionEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaticInfo is an object that contains machine information that does not change
|
|
||||||
type StaticInfo struct {
|
|
||||||
SystemSerialNumber string
|
|
||||||
SystemProductName string
|
|
||||||
SystemManufacturer string
|
|
||||||
Environment Environment
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context
|
// extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context
|
||||||
func extractUserAgent(ctx context.Context) string {
|
func extractUserAgent(ctx context.Context) string {
|
||||||
md, hasMeta := metadata.FromOutgoingContext(ctx)
|
md, hasMeta := metadata.FromOutgoingContext(ctx)
|
||||||
@@ -195,10 +187,3 @@ func GetInfoWithChecks(ctx context.Context, checks []*proto.Checks) (*Info, erro
|
|||||||
|
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateStaticInfo asynchronously updates static system and platform information
|
|
||||||
func UpdateStaticInfo() {
|
|
||||||
go func() {
|
|
||||||
_ = updateStaticInfo()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateStaticInfoAsync is a no-op on Android as there is no static info to update
|
||||||
|
func UpdateStaticInfoAsync() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
kernel := "android"
|
kernel := "android"
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import (
|
|||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func UpdateStaticInfoAsync() {
|
||||||
|
go updateStaticInfo()
|
||||||
|
}
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
utsname := unix.Utsname{}
|
utsname := unix.Utsname{}
|
||||||
@@ -41,7 +45,7 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
si := updateStaticInfo()
|
si := getStaticInfo()
|
||||||
if time.Since(start) > 1*time.Second {
|
if time.Since(start) > 1*time.Second {
|
||||||
log.Warnf("updateStaticInfo took %s", time.Since(start))
|
log.Warnf("updateStaticInfo took %s", time.Since(start))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateStaticInfoAsync is a no-op on Android as there is no static info to update
|
||||||
|
func UpdateStaticInfoAsync() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
out := _getInfo()
|
out := _getInfo()
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateStaticInfoAsync is a no-op on Android as there is no static info to update
|
||||||
|
func UpdateStaticInfoAsync() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ var (
|
|||||||
getSystemInfo = defaultSysInfoImplementation
|
getSystemInfo = defaultSysInfoImplementation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func UpdateStaticInfoAsync() {
|
||||||
|
go updateStaticInfo()
|
||||||
|
}
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
info := _getInfo()
|
info := _getInfo()
|
||||||
@@ -48,7 +52,7 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
si := updateStaticInfo()
|
si := getStaticInfo()
|
||||||
if time.Since(start) > 1*time.Second {
|
if time.Since(start) > 1*time.Second {
|
||||||
log.Warnf("updateStaticInfo took %s", time.Since(start))
|
log.Warnf("updateStaticInfo took %s", time.Since(start))
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-148
@@ -2,66 +2,48 @@ package system
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/yusufpapurcu/wmi"
|
|
||||||
"golang.org/x/sys/windows/registry"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Win32_OperatingSystem struct {
|
func UpdateStaticInfoAsync() {
|
||||||
Caption string
|
go updateStaticInfo()
|
||||||
}
|
|
||||||
|
|
||||||
type Win32_ComputerSystem struct {
|
|
||||||
Manufacturer string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Win32_ComputerSystemProduct struct {
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Win32_BIOS struct {
|
|
||||||
SerialNumber string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
osName, osVersion := getOSNameAndVersion()
|
|
||||||
buildVersion := getBuildVersion()
|
|
||||||
|
|
||||||
addrs, err := networkAddresses()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to discover network addresses: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
si := updateStaticInfo()
|
si := getStaticInfo()
|
||||||
if time.Since(start) > 1*time.Second {
|
if time.Since(start) > 1*time.Second {
|
||||||
log.Warnf("updateStaticInfo took %s", time.Since(start))
|
log.Warnf("updateStaticInfo took %s", time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
gio := &Info{
|
gio := &Info{
|
||||||
Kernel: "windows",
|
Kernel: "windows",
|
||||||
OSVersion: osVersion,
|
OSVersion: si.OSVersion,
|
||||||
Platform: "unknown",
|
Platform: "unknown",
|
||||||
OS: osName,
|
OS: si.OSName,
|
||||||
GoOS: runtime.GOOS,
|
GoOS: runtime.GOOS,
|
||||||
CPUs: runtime.NumCPU(),
|
CPUs: runtime.NumCPU(),
|
||||||
KernelVersion: buildVersion,
|
KernelVersion: si.BuildVersion,
|
||||||
NetworkAddresses: addrs,
|
|
||||||
SystemSerialNumber: si.SystemSerialNumber,
|
SystemSerialNumber: si.SystemSerialNumber,
|
||||||
SystemProductName: si.SystemProductName,
|
SystemProductName: si.SystemProductName,
|
||||||
SystemManufacturer: si.SystemManufacturer,
|
SystemManufacturer: si.SystemManufacturer,
|
||||||
Environment: si.Environment,
|
Environment: si.Environment,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addrs, err := networkAddresses()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to discover network addresses: %s", err)
|
||||||
|
} else {
|
||||||
|
gio.NetworkAddresses = addrs
|
||||||
|
}
|
||||||
|
|
||||||
systemHostname, _ := os.Hostname()
|
systemHostname, _ := os.Hostname()
|
||||||
gio.Hostname = extractDeviceName(ctx, systemHostname)
|
gio.Hostname = extractDeviceName(ctx, systemHostname)
|
||||||
gio.NetbirdVersion = version.NetbirdVersion()
|
gio.NetbirdVersion = version.NetbirdVersion()
|
||||||
@@ -69,120 +51,3 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
|
|
||||||
return gio
|
return gio
|
||||||
}
|
}
|
||||||
|
|
||||||
func sysInfo() (serialNumber string, productName string, manufacturer string) {
|
|
||||||
var err error
|
|
||||||
serialNumber, err = sysNumber()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to get system serial number: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
productName, err = sysProductName()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to get system product name: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
manufacturer, err = sysManufacturer()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to get system manufacturer: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return serialNumber, productName, manufacturer
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOSNameAndVersion() (string, string) {
|
|
||||||
var dst []Win32_OperatingSystem
|
|
||||||
query := wmi.CreateQuery(&dst, "")
|
|
||||||
err := wmi.Query(query, &dst)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return "Windows", getBuildVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(dst) == 0 {
|
|
||||||
return "Windows", getBuildVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
split := strings.Split(dst[0].Caption, " ")
|
|
||||||
|
|
||||||
if len(split) <= 3 {
|
|
||||||
return "Windows", getBuildVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
name := split[1]
|
|
||||||
version := split[2]
|
|
||||||
if split[2] == "Server" {
|
|
||||||
name = fmt.Sprintf("%s %s", split[1], split[2])
|
|
||||||
version = split[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
return name, version
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBuildVersion() string {
|
|
||||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return "0.0.0.0"
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
deferErr := k.Close()
|
|
||||||
if deferErr != nil {
|
|
||||||
log.Error(deferErr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
major, _, err := k.GetIntegerValue("CurrentMajorVersionNumber")
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
minor, _, err := k.GetIntegerValue("CurrentMinorVersionNumber")
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
build, _, err := k.GetStringValue("CurrentBuildNumber")
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
// Update Build Revision
|
|
||||||
ubr, _, err := k.GetIntegerValue("UBR")
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
ver := fmt.Sprintf("%d.%d.%s.%d", major, minor, build, ubr)
|
|
||||||
return ver
|
|
||||||
}
|
|
||||||
|
|
||||||
func sysNumber() (string, error) {
|
|
||||||
var dst []Win32_BIOS
|
|
||||||
query := wmi.CreateQuery(&dst, "")
|
|
||||||
err := wmi.Query(query, &dst)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return dst[0].SerialNumber, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sysProductName() (string, error) {
|
|
||||||
var dst []Win32_ComputerSystemProduct
|
|
||||||
query := wmi.CreateQuery(&dst, "")
|
|
||||||
err := wmi.Query(query, &dst)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// `ComputerSystemProduct` could be empty on some virtualized systems
|
|
||||||
if len(dst) < 1 {
|
|
||||||
return "unknown", nil
|
|
||||||
}
|
|
||||||
return dst[0].Name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sysManufacturer() (string, error) {
|
|
||||||
var dst []Win32_ComputerSystem
|
|
||||||
query := wmi.CreateQuery(&dst, "")
|
|
||||||
err := wmi.Query(query, &dst)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return dst[0].Manufacturer, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/system/detect_cloud"
|
|
||||||
"github.com/netbirdio/netbird/client/system/detect_platform"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -16,25 +11,26 @@ var (
|
|||||||
once sync.Once
|
once sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
func updateStaticInfo() StaticInfo {
|
// StaticInfo is an object that contains machine information that does not change
|
||||||
|
type StaticInfo struct {
|
||||||
|
SystemSerialNumber string
|
||||||
|
SystemProductName string
|
||||||
|
SystemManufacturer string
|
||||||
|
Environment Environment
|
||||||
|
|
||||||
|
// Windows specific fields
|
||||||
|
OSName string
|
||||||
|
OSVersion string
|
||||||
|
BuildVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateStaticInfo() {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
staticInfo = newStaticInfo()
|
||||||
defer cancel()
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(3)
|
|
||||||
go func() {
|
|
||||||
staticInfo.SystemSerialNumber, staticInfo.SystemProductName, staticInfo.SystemManufacturer = sysInfo()
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
staticInfo.Environment.Cloud = detect_cloud.Detect(ctx)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
staticInfo.Environment.Platform = detect_platform.Detect(ctx)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
wg.Wait()
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStaticInfo() StaticInfo {
|
||||||
|
updateStaticInfo()
|
||||||
return staticInfo
|
return staticInfo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
//go:build android || freebsd || ios
|
|
||||||
|
|
||||||
package system
|
|
||||||
|
|
||||||
// updateStaticInfo returns an empty implementation for unsupported platforms
|
|
||||||
func updateStaticInfo() StaticInfo {
|
|
||||||
return StaticInfo{}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//go:build (linux && !android) || (darwin && !ios)
|
||||||
|
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/system/detect_cloud"
|
||||||
|
"github.com/netbirdio/netbird/client/system/detect_platform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStaticInfo() StaticInfo {
|
||||||
|
si := StaticInfo{}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(3)
|
||||||
|
go func() {
|
||||||
|
si.SystemSerialNumber, si.SystemProductName, si.SystemManufacturer = sysInfo()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
si.Environment.Cloud = detect_cloud.Detect(ctx)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
si.Environment.Platform = detect_platform.Detect(ctx)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return si
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/yusufpapurcu/wmi"
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/system/detect_cloud"
|
||||||
|
"github.com/netbirdio/netbird/client/system/detect_platform"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Win32_OperatingSystem struct {
|
||||||
|
Caption string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Win32_ComputerSystem struct {
|
||||||
|
Manufacturer string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Win32_ComputerSystemProduct struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Win32_BIOS struct {
|
||||||
|
SerialNumber string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStaticInfo() StaticInfo {
|
||||||
|
si := StaticInfo{}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
si.SystemSerialNumber, si.SystemProductName, si.SystemManufacturer = sysInfo()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
si.Environment.Cloud = detect_cloud.Detect(ctx)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
si.Environment.Platform = detect_platform.Detect(ctx)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
si.OSName, si.OSVersion = getOSNameAndVersion()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
si.BuildVersion = getBuildVersion()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return si
|
||||||
|
}
|
||||||
|
|
||||||
|
func sysInfo() (serialNumber string, productName string, manufacturer string) {
|
||||||
|
var err error
|
||||||
|
serialNumber, err = sysNumber()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get system serial number: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
productName, err = sysProductName()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get system product name: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manufacturer, err = sysManufacturer()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get system manufacturer: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialNumber, productName, manufacturer
|
||||||
|
}
|
||||||
|
|
||||||
|
func sysNumber() (string, error) {
|
||||||
|
var dst []Win32_BIOS
|
||||||
|
query := wmi.CreateQuery(&dst, "")
|
||||||
|
err := wmi.Query(query, &dst)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dst[0].SerialNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sysProductName() (string, error) {
|
||||||
|
var dst []Win32_ComputerSystemProduct
|
||||||
|
query := wmi.CreateQuery(&dst, "")
|
||||||
|
err := wmi.Query(query, &dst)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// `ComputerSystemProduct` could be empty on some virtualized systems
|
||||||
|
if len(dst) < 1 {
|
||||||
|
return "unknown", nil
|
||||||
|
}
|
||||||
|
return dst[0].Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sysManufacturer() (string, error) {
|
||||||
|
var dst []Win32_ComputerSystem
|
||||||
|
query := wmi.CreateQuery(&dst, "")
|
||||||
|
err := wmi.Query(query, &dst)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dst[0].Manufacturer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOSNameAndVersion() (string, string) {
|
||||||
|
var dst []Win32_OperatingSystem
|
||||||
|
query := wmi.CreateQuery(&dst, "")
|
||||||
|
err := wmi.Query(query, &dst)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return "Windows", getBuildVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dst) == 0 {
|
||||||
|
return "Windows", getBuildVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(dst[0].Caption, " ")
|
||||||
|
|
||||||
|
if len(split) <= 3 {
|
||||||
|
return "Windows", getBuildVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
name := split[1]
|
||||||
|
version := split[2]
|
||||||
|
if split[2] == "Server" {
|
||||||
|
name = fmt.Sprintf("%s %s", split[1], split[2])
|
||||||
|
version = split[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, version
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBuildVersion() string {
|
||||||
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return "0.0.0.0"
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
deferErr := k.Close()
|
||||||
|
if deferErr != nil {
|
||||||
|
log.Error(deferErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
major, _, err := k.GetIntegerValue("CurrentMajorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
minor, _, err := k.GetIntegerValue("CurrentMinorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
build, _, err := k.GetStringValue("CurrentBuildNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
// Update Build Revision
|
||||||
|
ubr, _, err := k.GetIntegerValue("UBR")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
ver := fmt.Sprintf("%d.%d.%s.%d", major, minor, build, ubr)
|
||||||
|
return ver
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ require (
|
|||||||
github.com/miekg/dns v1.1.59
|
github.com/miekg/dns v1.1.59
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||||
github.com/nadoo/ipset v0.5.0
|
github.com/nadoo/ipset v0.5.0
|
||||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20250820151658-9ee1b34f4190
|
github.com/netbirdio/management-integrations/integrations v0.0.0-20250906095204-f87a07690ba0
|
||||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45
|
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45
|
||||||
github.com/okta/okta-sdk-golang/v2 v2.18.0
|
github.com/okta/okta-sdk-golang/v2 v2.18.0
|
||||||
github.com/oschwald/maxminddb-golang v1.12.0
|
github.com/oschwald/maxminddb-golang v1.12.0
|
||||||
|
|||||||
@@ -503,8 +503,8 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S
|
|||||||
github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ=
|
github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ=
|
||||||
github.com/netbirdio/ice/v4 v4.0.0-20250827161942-426799a23107 h1:ZJwhKexMlK15B/Ld+1T8VYE2Mt1lk1kf2DlXr46EHcw=
|
github.com/netbirdio/ice/v4 v4.0.0-20250827161942-426799a23107 h1:ZJwhKexMlK15B/Ld+1T8VYE2Mt1lk1kf2DlXr46EHcw=
|
||||||
github.com/netbirdio/ice/v4 v4.0.0-20250827161942-426799a23107/go.mod h1:ZSIbPdBn5hePO8CpF1PekH2SfpTxg1PDhEwtbqZS7R8=
|
github.com/netbirdio/ice/v4 v4.0.0-20250827161942-426799a23107/go.mod h1:ZSIbPdBn5hePO8CpF1PekH2SfpTxg1PDhEwtbqZS7R8=
|
||||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20250820151658-9ee1b34f4190 h1:/ZbExdcDwRq6XgTpTf5I1DPqnC3eInEf0fcmkqR8eSg=
|
github.com/netbirdio/management-integrations/integrations v0.0.0-20250906095204-f87a07690ba0 h1:9BUqQHPVOGr0edk8EifUBUfTr2Ob0ypAPxtasUApBxQ=
|
||||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20250820151658-9ee1b34f4190/go.mod h1:v0nUbbHbuQnqR7yKIYnKzsLBCswLtp2JctmKYmGgVhc=
|
github.com/netbirdio/management-integrations/integrations v0.0.0-20250906095204-f87a07690ba0/go.mod h1:v0nUbbHbuQnqR7yKIYnKzsLBCswLtp2JctmKYmGgVhc=
|
||||||
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8=
|
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8=
|
||||||
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ=
|
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ=
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ func (s *BaseServer) PeersUpdateManager() *server.PeersUpdateManager {
|
|||||||
|
|
||||||
func (s *BaseServer) IntegratedValidator() integrated_validator.IntegratedValidator {
|
func (s *BaseServer) IntegratedValidator() integrated_validator.IntegratedValidator {
|
||||||
return Create(s, func() integrated_validator.IntegratedValidator {
|
return Create(s, func() integrated_validator.IntegratedValidator {
|
||||||
integratedPeerValidator, err := integrations.NewIntegratedValidator(context.Background(), s.EventStore())
|
integratedPeerValidator, err := integrations.NewIntegratedValidator(
|
||||||
|
context.Background(),
|
||||||
|
s.PeersManager(),
|
||||||
|
s.SettingsManager(),
|
||||||
|
s.EventStore())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to create integrated peer validator: %v", err)
|
log.Errorf("failed to create integrated peer validator: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1149,7 +1149,18 @@ func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domai
|
|||||||
func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) {
|
func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) {
|
||||||
newUser := types.NewRegularUser(userAuth.UserId)
|
newUser := types.NewRegularUser(userAuth.UserId)
|
||||||
newUser.AccountID = domainAccountID
|
newUser.AccountID = domainAccountID
|
||||||
err := am.Store.SaveUser(ctx, newUser)
|
|
||||||
|
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, domainAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings != nil && settings.Extra != nil && settings.Extra.UserApprovalRequired {
|
||||||
|
newUser.Blocked = true
|
||||||
|
newUser.PendingApproval = true
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.Store.SaveUser(ctx, newUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -1159,7 +1170,11 @@ func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context,
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, domainAccountID, activity.UserJoined, nil)
|
if newUser.PendingApproval {
|
||||||
|
am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, domainAccountID, activity.UserJoined, map[string]any{"pending_approval": true})
|
||||||
|
} else {
|
||||||
|
am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, domainAccountID, activity.UserJoined, nil)
|
||||||
|
}
|
||||||
|
|
||||||
return domainAccountID, nil
|
return domainAccountID, nil
|
||||||
}
|
}
|
||||||
@@ -1712,7 +1727,9 @@ func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, account
|
|||||||
log.WithContext(ctx).Errorf("failed to get invalidated peer %s for account %s: %v", peerID, accountID, err)
|
log.WithContext(ctx).Errorf("failed to get invalidated peer %s for account %s: %v", peerID, accountID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
peers = append(peers, peer)
|
if peer.UserID != "" {
|
||||||
|
peers = append(peers, peer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(peers) > 0 {
|
if len(peers) > 0 {
|
||||||
err := am.expireAndUpdatePeers(ctx, accountID, peers)
|
err := am.expireAndUpdatePeers(ctx, accountID, peers)
|
||||||
@@ -1808,6 +1825,9 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string, dis
|
|||||||
PeerInactivityExpirationEnabled: false,
|
PeerInactivityExpirationEnabled: false,
|
||||||
PeerInactivityExpiration: types.DefaultPeerInactivityExpiration,
|
PeerInactivityExpiration: types.DefaultPeerInactivityExpiration,
|
||||||
RoutingPeerDNSResolutionEnabled: true,
|
RoutingPeerDNSResolutionEnabled: true,
|
||||||
|
Extra: &types.ExtraSettings{
|
||||||
|
UserApprovalRequired: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Onboarding: types.AccountOnboarding{
|
Onboarding: types.AccountOnboarding{
|
||||||
OnboardingFlowPending: true,
|
OnboardingFlowPending: true,
|
||||||
@@ -1914,6 +1934,9 @@ func (am *DefaultAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.C
|
|||||||
PeerInactivityExpirationEnabled: false,
|
PeerInactivityExpirationEnabled: false,
|
||||||
PeerInactivityExpiration: types.DefaultPeerInactivityExpiration,
|
PeerInactivityExpiration: types.DefaultPeerInactivityExpiration,
|
||||||
RoutingPeerDNSResolutionEnabled: true,
|
RoutingPeerDNSResolutionEnabled: true,
|
||||||
|
Extra: &types.ExtraSettings{
|
||||||
|
UserApprovalRequired: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ type Manager interface {
|
|||||||
DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error
|
DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error
|
||||||
DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error
|
DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error
|
||||||
InviteUser(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error
|
InviteUser(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error
|
||||||
|
ApproveUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) (*types.UserInfo, error)
|
||||||
|
RejectUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) error
|
||||||
ListSetupKeys(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error)
|
ListSetupKeys(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error)
|
||||||
SaveUser(ctx context.Context, accountID, initiatorUserID string, update *types.User) (*types.UserInfo, error)
|
SaveUser(ctx context.Context, accountID, initiatorUserID string, update *types.User) (*types.UserInfo, error)
|
||||||
SaveOrAddUser(ctx context.Context, accountID, initiatorUserID string, update *types.User, addIfNotExists bool) (*types.UserInfo, error)
|
SaveOrAddUser(ctx context.Context, accountID, initiatorUserID string, update *types.User, addIfNotExists bool) (*types.UserInfo, error)
|
||||||
|
|||||||
@@ -3606,3 +3606,93 @@ func TestDefaultAccountManager_UpdatePeerIP(t *testing.T) {
|
|||||||
require.Error(t, err, "should fail with invalid peer ID")
|
require.Error(t, err, "should fail with invalid peer ID")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddNewUserToDomainAccountWithApproval(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a domain-based account with user approval enabled
|
||||||
|
existingAccountID := "existing-account"
|
||||||
|
account := newAccountWithId(context.Background(), existingAccountID, "owner-user", "example.com", false)
|
||||||
|
account.Settings.Extra = &types.ExtraSettings{
|
||||||
|
UserApprovalRequired: true,
|
||||||
|
}
|
||||||
|
err = manager.Store.SaveAccount(context.Background(), account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Set the account as domain primary account
|
||||||
|
account.IsDomainPrimaryAccount = true
|
||||||
|
account.DomainCategory = types.PrivateCategory
|
||||||
|
err = manager.Store.SaveAccount(context.Background(), account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test adding new user to existing account with approval required
|
||||||
|
newUserID := "new-user-id"
|
||||||
|
userAuth := nbcontext.UserAuth{
|
||||||
|
UserId: newUserID,
|
||||||
|
Domain: "example.com",
|
||||||
|
DomainCategory: types.PrivateCategory,
|
||||||
|
}
|
||||||
|
|
||||||
|
acc, err := manager.Store.GetAccount(context.Background(), existingAccountID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, acc.IsDomainPrimaryAccount, "Account should be primary for the domain")
|
||||||
|
require.Equal(t, "example.com", acc.Domain, "Account domain should match")
|
||||||
|
|
||||||
|
returnedAccountID, err := manager.getAccountIDWithAuthorizationClaims(context.Background(), userAuth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, existingAccountID, returnedAccountID)
|
||||||
|
|
||||||
|
// Verify user was created with pending approval
|
||||||
|
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, newUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, user.Blocked, "User should be blocked when approval is required")
|
||||||
|
assert.True(t, user.PendingApproval, "User should be pending approval")
|
||||||
|
assert.Equal(t, existingAccountID, user.AccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddNewUserToDomainAccountWithoutApproval(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a domain-based account without user approval
|
||||||
|
ownerUserAuth := nbcontext.UserAuth{
|
||||||
|
UserId: "owner-user",
|
||||||
|
Domain: "example.com",
|
||||||
|
DomainCategory: types.PrivateCategory,
|
||||||
|
}
|
||||||
|
existingAccountID, err := manager.getAccountIDWithAuthorizationClaims(context.Background(), ownerUserAuth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Modify the account to disable user approval
|
||||||
|
account, err := manager.Store.GetAccount(context.Background(), existingAccountID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
account.Settings.Extra = &types.ExtraSettings{
|
||||||
|
UserApprovalRequired: false,
|
||||||
|
}
|
||||||
|
err = manager.Store.SaveAccount(context.Background(), account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test adding new user to existing account without approval required
|
||||||
|
newUserID := "new-user-id"
|
||||||
|
userAuth := nbcontext.UserAuth{
|
||||||
|
UserId: newUserID,
|
||||||
|
Domain: "example.com",
|
||||||
|
DomainCategory: types.PrivateCategory,
|
||||||
|
}
|
||||||
|
|
||||||
|
returnedAccountID, err := manager.getAccountIDWithAuthorizationClaims(context.Background(), userAuth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, existingAccountID, returnedAccountID)
|
||||||
|
|
||||||
|
// Verify user was created without pending approval
|
||||||
|
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, newUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, user.Blocked, "User should not be blocked when approval is not required")
|
||||||
|
assert.False(t, user.PendingApproval, "User should not be pending approval")
|
||||||
|
assert.Equal(t, existingAccountID, user.AccountID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -177,8 +177,10 @@ const (
|
|||||||
|
|
||||||
AccountNetworkRangeUpdated Activity = 87
|
AccountNetworkRangeUpdated Activity = 87
|
||||||
PeerIPUpdated Activity = 88
|
PeerIPUpdated Activity = 88
|
||||||
|
UserApproved Activity = 89
|
||||||
|
UserRejected Activity = 90
|
||||||
|
|
||||||
AccountAutoUpdateVersionUpdated Activity = 89
|
AccountAutoUpdateVersionUpdated Activity = 91
|
||||||
|
|
||||||
AccountDeleted Activity = 99999
|
AccountDeleted Activity = 99999
|
||||||
)
|
)
|
||||||
@@ -287,6 +289,9 @@ var activityMap = map[Activity]Code{
|
|||||||
|
|
||||||
PeerIPUpdated: {"Peer IP updated", "peer.ip.update"},
|
PeerIPUpdated: {"Peer IP updated", "peer.ip.update"},
|
||||||
|
|
||||||
|
UserApproved: {"User approved", "user.approve"},
|
||||||
|
UserRejected: {"User rejected", "user.reject"},
|
||||||
|
|
||||||
AccountAutoUpdateVersionUpdated: {"Account AutoUpdate Version updated", "account.settings.auto.version.update"},
|
AccountAutoUpdateVersionUpdated: {"Account AutoUpdate Version updated", "account.settings.auto.version.update"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS
|
|||||||
if req.Settings.Extra != nil {
|
if req.Settings.Extra != nil {
|
||||||
returnSettings.Extra = &types.ExtraSettings{
|
returnSettings.Extra = &types.ExtraSettings{
|
||||||
PeerApprovalEnabled: req.Settings.Extra.PeerApprovalEnabled,
|
PeerApprovalEnabled: req.Settings.Extra.PeerApprovalEnabled,
|
||||||
|
UserApprovalRequired: req.Settings.Extra.UserApprovalRequired,
|
||||||
FlowEnabled: req.Settings.Extra.NetworkTrafficLogsEnabled,
|
FlowEnabled: req.Settings.Extra.NetworkTrafficLogsEnabled,
|
||||||
FlowGroups: req.Settings.Extra.NetworkTrafficLogsGroups,
|
FlowGroups: req.Settings.Extra.NetworkTrafficLogsGroups,
|
||||||
FlowPacketCounterEnabled: req.Settings.Extra.NetworkTrafficPacketCounterEnabled,
|
FlowPacketCounterEnabled: req.Settings.Extra.NetworkTrafficPacketCounterEnabled,
|
||||||
@@ -351,6 +352,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
|
|||||||
if settings.Extra != nil {
|
if settings.Extra != nil {
|
||||||
apiSettings.Extra = &api.AccountExtraSettings{
|
apiSettings.Extra = &api.AccountExtraSettings{
|
||||||
PeerApprovalEnabled: settings.Extra.PeerApprovalEnabled,
|
PeerApprovalEnabled: settings.Extra.PeerApprovalEnabled,
|
||||||
|
UserApprovalRequired: settings.Extra.UserApprovalRequired,
|
||||||
NetworkTrafficLogsEnabled: settings.Extra.FlowEnabled,
|
NetworkTrafficLogsEnabled: settings.Extra.FlowEnabled,
|
||||||
NetworkTrafficLogsGroups: settings.Extra.FlowGroups,
|
NetworkTrafficLogsGroups: settings.Extra.FlowGroups,
|
||||||
NetworkTrafficPacketCounterEnabled: settings.Extra.FlowPacketCounterEnabled,
|
NetworkTrafficPacketCounterEnabled: settings.Extra.FlowPacketCounterEnabled,
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
||||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initAccountsTestData(t *testing.T, account *types.Account) *handler {
|
func initAccountsTestData(t *testing.T, account *types.Account) *handler {
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/account"
|
"github.com/netbirdio/netbird/management/server/account"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
"github.com/netbirdio/netbird/management/server/users"
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
|
||||||
"github.com/netbirdio/netbird/management/server/users"
|
|
||||||
|
|
||||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||||
)
|
)
|
||||||
@@ -31,6 +31,8 @@ func AddEndpoints(accountManager account.Manager, router *mux.Router) {
|
|||||||
router.HandleFunc("/users/{userId}", userHandler.deleteUser).Methods("DELETE", "OPTIONS")
|
router.HandleFunc("/users/{userId}", userHandler.deleteUser).Methods("DELETE", "OPTIONS")
|
||||||
router.HandleFunc("/users", userHandler.createUser).Methods("POST", "OPTIONS")
|
router.HandleFunc("/users", userHandler.createUser).Methods("POST", "OPTIONS")
|
||||||
router.HandleFunc("/users/{userId}/invite", userHandler.inviteUser).Methods("POST", "OPTIONS")
|
router.HandleFunc("/users/{userId}/invite", userHandler.inviteUser).Methods("POST", "OPTIONS")
|
||||||
|
router.HandleFunc("/users/{userId}/approve", userHandler.approveUser).Methods("POST", "OPTIONS")
|
||||||
|
router.HandleFunc("/users/{userId}/reject", userHandler.rejectUser).Methods("DELETE", "OPTIONS")
|
||||||
addUsersTokensEndpoint(accountManager, router)
|
addUsersTokensEndpoint(accountManager, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,17 +325,76 @@ func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isCurrent := user.ID == currenUserID
|
isCurrent := user.ID == currenUserID
|
||||||
|
|
||||||
return &api.User{
|
return &api.User{
|
||||||
Id: user.ID,
|
Id: user.ID,
|
||||||
Name: user.Name,
|
Name: user.Name,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
AutoGroups: autoGroups,
|
AutoGroups: autoGroups,
|
||||||
Status: userStatus,
|
Status: userStatus,
|
||||||
IsCurrent: &isCurrent,
|
IsCurrent: &isCurrent,
|
||||||
IsServiceUser: &user.IsServiceUser,
|
IsServiceUser: &user.IsServiceUser,
|
||||||
IsBlocked: user.IsBlocked,
|
IsBlocked: user.IsBlocked,
|
||||||
LastLogin: &user.LastLogin,
|
LastLogin: &user.LastLogin,
|
||||||
Issued: &user.Issued,
|
Issued: &user.Issued,
|
||||||
|
PendingApproval: user.PendingApproval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// approveUser is a POST request to approve a user that is pending approval
|
||||||
|
func (h *handler) approveUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
targetUserID := vars["userId"]
|
||||||
|
if len(targetUserID) == 0 {
|
||||||
|
util.WriteErrorResponse("invalid user ID", http.StatusBadRequest, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := h.accountManager.ApproveUser(r.Context(), userAuth.AccountId, userAuth.UserId, targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userResponse := toUserResponse(user, userAuth.UserId)
|
||||||
|
util.WriteJSONObject(r.Context(), w, userResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rejectUser is a DELETE request to reject a user that is pending approval
|
||||||
|
func (h *handler) rejectUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodDelete {
|
||||||
|
util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
targetUserID := vars["userId"]
|
||||||
|
if len(targetUserID) == 0 {
|
||||||
|
util.WriteErrorResponse("invalid user ID", http.StatusBadRequest, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = h.accountManager.RejectUser(r.Context(), userAuth.AccountId, userAuth.UserId, targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
||||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||||
"github.com/netbirdio/netbird/management/server/permissions/roles"
|
"github.com/netbirdio/netbird/management/server/permissions/roles"
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
"github.com/netbirdio/netbird/management/server/users"
|
"github.com/netbirdio/netbird/management/server/users"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -725,3 +725,133 @@ func stringifyPermissionsKeys(permissions roles.Permissions) map[string]map[stri
|
|||||||
}
|
}
|
||||||
return modules
|
return modules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApproveUserEndpoint(t *testing.T) {
|
||||||
|
adminUser := &types.User{
|
||||||
|
Id: "admin-user",
|
||||||
|
Role: types.UserRoleAdmin,
|
||||||
|
AccountID: existingAccountID,
|
||||||
|
AutoGroups: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingUser := &types.User{
|
||||||
|
Id: "pending-user",
|
||||||
|
Role: types.UserRoleUser,
|
||||||
|
AccountID: existingAccountID,
|
||||||
|
Blocked: true,
|
||||||
|
PendingApproval: true,
|
||||||
|
AutoGroups: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
tt := []struct {
|
||||||
|
name string
|
||||||
|
expectedStatus int
|
||||||
|
expectedBody bool
|
||||||
|
requestingUser *types.User
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "approve user as admin should return 200",
|
||||||
|
expectedStatus: 200,
|
||||||
|
expectedBody: true,
|
||||||
|
requestingUser: adminUser,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
am := &mock_server.MockAccountManager{}
|
||||||
|
am.ApproveUserFunc = func(ctx context.Context, accountID, initiatorUserID, targetUserID string) (*types.UserInfo, error) {
|
||||||
|
approvedUserInfo := &types.UserInfo{
|
||||||
|
ID: pendingUser.Id,
|
||||||
|
Email: "pending@example.com",
|
||||||
|
Name: "Pending User",
|
||||||
|
Role: string(pendingUser.Role),
|
||||||
|
AutoGroups: []string{},
|
||||||
|
IsServiceUser: false,
|
||||||
|
IsBlocked: false,
|
||||||
|
PendingApproval: false,
|
||||||
|
LastLogin: time.Now(),
|
||||||
|
Issued: types.UserIssuedAPI,
|
||||||
|
}
|
||||||
|
return approvedUserInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := newHandler(am)
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/users/{userId}/approve", handler.approveUser).Methods("POST")
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "/users/pending-user/approve", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userAuth := nbcontext.UserAuth{
|
||||||
|
AccountId: existingAccountID,
|
||||||
|
UserId: tc.requestingUser.Id,
|
||||||
|
}
|
||||||
|
ctx := nbcontext.SetUserAuthInContext(req.Context(), userAuth)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectedStatus, rr.Code)
|
||||||
|
|
||||||
|
if tc.expectedBody {
|
||||||
|
var response api.User
|
||||||
|
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "pending-user", response.Id)
|
||||||
|
assert.False(t, response.IsBlocked)
|
||||||
|
assert.False(t, response.PendingApproval)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRejectUserEndpoint(t *testing.T) {
|
||||||
|
adminUser := &types.User{
|
||||||
|
Id: "admin-user",
|
||||||
|
Role: types.UserRoleAdmin,
|
||||||
|
AccountID: existingAccountID,
|
||||||
|
AutoGroups: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
tt := []struct {
|
||||||
|
name string
|
||||||
|
expectedStatus int
|
||||||
|
requestingUser *types.User
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "reject user as admin should return 200",
|
||||||
|
expectedStatus: 200,
|
||||||
|
requestingUser: adminUser,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
am := &mock_server.MockAccountManager{}
|
||||||
|
am.RejectUserFunc = func(ctx context.Context, accountID, initiatorUserID, targetUserID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := newHandler(am)
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/users/{userId}/reject", handler.rejectUser).Methods("DELETE")
|
||||||
|
|
||||||
|
req, err := http.NewRequest("DELETE", "/users/pending-user/reject", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userAuth := nbcontext.UserAuth{
|
||||||
|
AccountId: existingAccountID,
|
||||||
|
UserId: tc.requestingUser.Id,
|
||||||
|
}
|
||||||
|
ctx := nbcontext.SetUserAuthInContext(req.Context(), userAuth)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectedStatus, rr.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ type MockAccountManager struct {
|
|||||||
LoginPeerFunc func(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error)
|
LoginPeerFunc func(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error)
|
||||||
SyncPeerFunc func(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error)
|
SyncPeerFunc func(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error)
|
||||||
InviteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserEmail string) error
|
InviteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserEmail string) error
|
||||||
|
ApproveUserFunc func(ctx context.Context, accountID, initiatorUserID, targetUserID string) (*types.UserInfo, error)
|
||||||
|
RejectUserFunc func(ctx context.Context, accountID, initiatorUserID, targetUserID string) error
|
||||||
GetAllConnectedPeersFunc func() (map[string]struct{}, error)
|
GetAllConnectedPeersFunc func() (map[string]struct{}, error)
|
||||||
HasConnectedChannelFunc func(peerID string) bool
|
HasConnectedChannelFunc func(peerID string) bool
|
||||||
GetExternalCacheManagerFunc func() account.ExternalCacheManager
|
GetExternalCacheManagerFunc func() account.ExternalCacheManager
|
||||||
@@ -607,6 +609,20 @@ func (am *MockAccountManager) InviteUser(ctx context.Context, accountID string,
|
|||||||
return status.Errorf(codes.Unimplemented, "method InviteUser is not implemented")
|
return status.Errorf(codes.Unimplemented, "method InviteUser is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (am *MockAccountManager) ApproveUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) (*types.UserInfo, error) {
|
||||||
|
if am.ApproveUserFunc != nil {
|
||||||
|
return am.ApproveUserFunc(ctx, accountID, initiatorUserID, targetUserID)
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ApproveUser is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *MockAccountManager) RejectUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) error {
|
||||||
|
if am.RejectUserFunc != nil {
|
||||||
|
return am.RejectUserFunc(ctx, accountID, initiatorUserID, targetUserID)
|
||||||
|
}
|
||||||
|
return status.Errorf(codes.Unimplemented, "method RejectUser is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
// GetNameServerGroup mocks GetNameServerGroup of the AccountManager interface
|
// GetNameServerGroup mocks GetNameServerGroup of the AccountManager interface
|
||||||
func (am *MockAccountManager) GetNameServerGroup(ctx context.Context, accountID, userID, nsGroupID string) (*nbdns.NameServerGroup, error) {
|
func (am *MockAccountManager) GetNameServerGroup(ctx context.Context, accountID, userID, nsGroupID string) (*nbdns.NameServerGroup, error) {
|
||||||
if am.GetNameServerGroupFunc != nil {
|
if am.GetNameServerGroupFunc != nil {
|
||||||
|
|||||||
@@ -489,6 +489,9 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, status.Errorf(status.NotFound, "failed adding new peer: user not found")
|
return nil, nil, nil, status.Errorf(status.NotFound, "failed adding new peer: user not found")
|
||||||
}
|
}
|
||||||
|
if user.PendingApproval {
|
||||||
|
return nil, nil, nil, status.Errorf(status.PermissionDenied, "user pending approval cannot add peers")
|
||||||
|
}
|
||||||
groupsToAdd = user.AutoGroups
|
groupsToAdd = user.AutoGroups
|
||||||
opEvent.InitiatorID = userID
|
opEvent.InitiatorID = userID
|
||||||
opEvent.Activity = activity.PeerAddedByUser
|
opEvent.Activity = activity.PeerAddedByUser
|
||||||
|
|||||||
@@ -2383,3 +2383,186 @@ func TestBufferUpdateAccountPeers(t *testing.T) {
|
|||||||
assert.Less(t, totalNewRuns, totalOldRuns, "Expected new approach to run less than old approach. New runs: %d, Old runs: %d", totalNewRuns, totalOldRuns)
|
assert.Less(t, totalNewRuns, totalOldRuns, "Expected new approach to run less than old approach. New runs: %d, Old runs: %d", totalNewRuns, totalOldRuns)
|
||||||
t.Logf("New runs: %d, Old runs: %d", totalNewRuns, totalOldRuns)
|
t.Logf("New runs: %d, Old runs: %d", totalNewRuns, totalOldRuns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddPeer_UserPendingApprovalBlocked(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create account
|
||||||
|
account := newAccountWithId(context.Background(), "test-account", "owner", "", false)
|
||||||
|
err = manager.Store.SaveAccount(context.Background(), account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create user pending approval
|
||||||
|
pendingUser := types.NewRegularUser("pending-user")
|
||||||
|
pendingUser.AccountID = account.Id
|
||||||
|
pendingUser.Blocked = true
|
||||||
|
pendingUser.PendingApproval = true
|
||||||
|
err = manager.Store.SaveUser(context.Background(), pendingUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Try to add peer with pending approval user
|
||||||
|
key, err := wgtypes.GenerateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
peer := &nbpeer.Peer{
|
||||||
|
Key: key.PublicKey().String(),
|
||||||
|
Name: "test-peer",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{
|
||||||
|
Hostname: "test-peer",
|
||||||
|
OS: "linux",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, err = manager.AddPeer(context.Background(), "", pendingUser.Id, peer)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "user pending approval cannot add peers")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddPeer_ApprovedUserCanAddPeers(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create account
|
||||||
|
account := newAccountWithId(context.Background(), "test-account", "owner", "", false)
|
||||||
|
err = manager.Store.SaveAccount(context.Background(), account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create regular user (not pending approval)
|
||||||
|
regularUser := types.NewRegularUser("regular-user")
|
||||||
|
regularUser.AccountID = account.Id
|
||||||
|
err = manager.Store.SaveUser(context.Background(), regularUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Try to add peer with regular user
|
||||||
|
key, err := wgtypes.GenerateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
peer := &nbpeer.Peer{
|
||||||
|
Key: key.PublicKey().String(),
|
||||||
|
Name: "test-peer",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{
|
||||||
|
Hostname: "test-peer",
|
||||||
|
OS: "linux",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, err = manager.AddPeer(context.Background(), "", regularUser.Id, peer)
|
||||||
|
require.NoError(t, err, "Regular user should be able to add peers")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPeer_UserPendingApprovalBlocked(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create account
|
||||||
|
account := newAccountWithId(context.Background(), "test-account", "owner", "", false)
|
||||||
|
err = manager.Store.SaveAccount(context.Background(), account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create user pending approval
|
||||||
|
pendingUser := types.NewRegularUser("pending-user")
|
||||||
|
pendingUser.AccountID = account.Id
|
||||||
|
pendingUser.Blocked = true
|
||||||
|
pendingUser.PendingApproval = true
|
||||||
|
err = manager.Store.SaveUser(context.Background(), pendingUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a peer using AddPeer method for the pending user (simulate existing peer)
|
||||||
|
key, err := wgtypes.GenerateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Set the user to not be pending initially so peer can be added
|
||||||
|
pendingUser.Blocked = false
|
||||||
|
pendingUser.PendingApproval = false
|
||||||
|
err = manager.Store.SaveUser(context.Background(), pendingUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Add peer using regular flow
|
||||||
|
newPeer := &nbpeer.Peer{
|
||||||
|
Key: key.PublicKey().String(),
|
||||||
|
Name: "test-peer",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{
|
||||||
|
Hostname: "test-peer",
|
||||||
|
OS: "linux",
|
||||||
|
WtVersion: "0.28.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
existingPeer, _, _, err := manager.AddPeer(context.Background(), "", pendingUser.Id, newPeer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Now set the user back to pending approval after peer was created
|
||||||
|
pendingUser.Blocked = true
|
||||||
|
pendingUser.PendingApproval = true
|
||||||
|
err = manager.Store.SaveUser(context.Background(), pendingUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Try to login with pending approval user
|
||||||
|
login := types.PeerLogin{
|
||||||
|
WireGuardPubKey: existingPeer.Key,
|
||||||
|
UserID: pendingUser.Id,
|
||||||
|
Meta: nbpeer.PeerSystemMeta{
|
||||||
|
Hostname: "test-peer",
|
||||||
|
OS: "linux",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, err = manager.LoginPeer(context.Background(), login)
|
||||||
|
require.Error(t, err)
|
||||||
|
e, ok := status.FromError(err)
|
||||||
|
require.True(t, ok, "error is not a gRPC status error")
|
||||||
|
assert.Equal(t, status.PermissionDenied, e.Type(), "expected PermissionDenied error code")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPeer_ApprovedUserCanLogin(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create account
|
||||||
|
account := newAccountWithId(context.Background(), "test-account", "owner", "", false)
|
||||||
|
err = manager.Store.SaveAccount(context.Background(), account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create regular user (not pending approval)
|
||||||
|
regularUser := types.NewRegularUser("regular-user")
|
||||||
|
regularUser.AccountID = account.Id
|
||||||
|
err = manager.Store.SaveUser(context.Background(), regularUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Add peer using regular flow for the regular user
|
||||||
|
key, err := wgtypes.GenerateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newPeer := &nbpeer.Peer{
|
||||||
|
Key: key.PublicKey().String(),
|
||||||
|
Name: "test-peer",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{
|
||||||
|
Hostname: "test-peer",
|
||||||
|
OS: "linux",
|
||||||
|
WtVersion: "0.28.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
existingPeer, _, _, err := manager.AddPeer(context.Background(), "", regularUser.Id, newPeer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Try to login with regular user
|
||||||
|
login := types.PeerLogin{
|
||||||
|
WireGuardPubKey: existingPeer.Key,
|
||||||
|
UserID: regularUser.Id,
|
||||||
|
Meta: nbpeer.PeerSystemMeta{
|
||||||
|
Hostname: "test-peer",
|
||||||
|
OS: "linux",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, err = manager.LoginPeer(context.Background(), login)
|
||||||
|
require.NoError(t, err, "Regular user should be able to login peers")
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Manager interface {
|
|||||||
GetPeer(ctx context.Context, accountID, userID, peerID string) (*peer.Peer, error)
|
GetPeer(ctx context.Context, accountID, userID, peerID string) (*peer.Peer, error)
|
||||||
GetPeerAccountID(ctx context.Context, peerID string) (string, error)
|
GetPeerAccountID(ctx context.Context, peerID string) (string, error)
|
||||||
GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error)
|
GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error)
|
||||||
|
GetPeersByGroupIDs(ctx context.Context, accountID string, groupsIDs []string) ([]*peer.Peer, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type managerImpl struct {
|
type managerImpl struct {
|
||||||
@@ -61,3 +62,7 @@ func (m *managerImpl) GetAllPeers(ctx context.Context, accountID, userID string)
|
|||||||
func (m *managerImpl) GetPeerAccountID(ctx context.Context, peerID string) (string, error) {
|
func (m *managerImpl) GetPeerAccountID(ctx context.Context, peerID string) (string, error) {
|
||||||
return m.store.GetAccountIDByPeerID(ctx, store.LockingStrengthNone, peerID)
|
return m.store.GetAccountIDByPeerID(ctx, store.LockingStrengthNone, peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *managerImpl) GetPeersByGroupIDs(ctx context.Context, accountID string, groupsIDs []string) ([]*peer.Peer, error) {
|
||||||
|
return m.store.GetPeersByGroupIDs(ctx, accountID, groupsIDs)
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,3 +79,18 @@ func (mr *MockManagerMockRecorder) GetPeerAccountID(ctx, peerID interface{}) *go
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerAccountID", reflect.TypeOf((*MockManager)(nil).GetPeerAccountID), ctx, peerID)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerAccountID", reflect.TypeOf((*MockManager)(nil).GetPeerAccountID), ctx, peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeersByGroupIDs mocks base method.
|
||||||
|
func (m *MockManager) GetPeersByGroupIDs(ctx context.Context, accountID string, groupsIDs []string) ([]*peer.Peer, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetPeersByGroupIDs", ctx, accountID, groupsIDs)
|
||||||
|
ret0, _ := ret[0].([]*peer.Peer)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeersByGroupIDs indicates an expected call of GetPeersByGroupIDs.
|
||||||
|
func (mr *MockManagerMockRecorder) GetPeersByGroupIDs(ctx, accountID, groupsIDs interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeersByGroupIDs", reflect.TypeOf((*MockManager)(nil).GetPeersByGroupIDs), ctx, accountID, groupsIDs)
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,10 +54,14 @@ func (m *managerImpl) ValidateUserPermissions(
|
|||||||
return false, status.NewUserNotFoundError(userID)
|
return false, status.NewUserNotFoundError(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.IsBlocked() {
|
if user.IsBlocked() && !user.PendingApproval {
|
||||||
return false, status.NewUserBlockedError()
|
return false, status.NewUserBlockedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.IsBlocked() && user.PendingApproval {
|
||||||
|
return false, status.NewUserPendingApprovalError()
|
||||||
|
}
|
||||||
|
|
||||||
if err := m.ValidateAccountAccess(ctx, accountID, user, false); err != nil {
|
if err := m.ValidateAccountAccess(ctx, accountID, user, false); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2847,3 +2847,22 @@ func (s *SqlStore) UpdateAccountNetwork(ctx context.Context, accountID string, i
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SqlStore) GetPeersByGroupIDs(ctx context.Context, accountID string, groupIDs []string) ([]*nbpeer.Peer, error) {
|
||||||
|
if len(groupIDs) == 0 {
|
||||||
|
return []*nbpeer.Peer{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var peers []*nbpeer.Peer
|
||||||
|
peerIDsSubquery := s.db.Model(&types.GroupPeer{}).
|
||||||
|
Select("DISTINCT peer_id").
|
||||||
|
Where("account_id = ? AND group_id IN ?", accountID, groupIDs)
|
||||||
|
|
||||||
|
result := s.db.Where("id IN (?)", peerIDsSubquery).Find(&peers)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to get peers by group IDs: %s", result.Error)
|
||||||
|
return nil, status.Errorf(status.Internal, "failed to get peers by group IDs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3607,3 +3607,113 @@ func intToIPv4(n uint32) net.IP {
|
|||||||
binary.BigEndian.PutUint32(ip, n)
|
binary.BigEndian.PutUint32(ip, n)
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSqlStore_GetPeersByGroupIDs(t *testing.T) {
|
||||||
|
accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
|
||||||
|
|
||||||
|
group1ID := "test-group-1"
|
||||||
|
group2ID := "test-group-2"
|
||||||
|
emptyGroupID := "empty-group"
|
||||||
|
|
||||||
|
peer1 := "cfefqs706sqkneg59g4g"
|
||||||
|
peer2 := "cfeg6sf06sqkneg59g50"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
groupIDs []string
|
||||||
|
expectedPeers []string
|
||||||
|
expectedCount int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "retrieve peers from single group with multiple peers",
|
||||||
|
groupIDs: []string{group1ID},
|
||||||
|
expectedPeers: []string{peer1, peer2},
|
||||||
|
expectedCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "retrieve peers from single group with one peer",
|
||||||
|
groupIDs: []string{group2ID},
|
||||||
|
expectedPeers: []string{peer1},
|
||||||
|
expectedCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "retrieve peers from multiple groups (with overlap)",
|
||||||
|
groupIDs: []string{group1ID, group2ID},
|
||||||
|
expectedPeers: []string{peer1, peer2}, // should deduplicate
|
||||||
|
expectedCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "retrieve peers from existing 'All' group",
|
||||||
|
groupIDs: []string{"cfefqs706sqkneg59g3g"}, // All group from test data
|
||||||
|
expectedPeers: []string{peer1, peer2},
|
||||||
|
expectedCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "retrieve peers from empty group",
|
||||||
|
groupIDs: []string{emptyGroupID},
|
||||||
|
expectedPeers: []string{},
|
||||||
|
expectedCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "retrieve peers from non-existing group",
|
||||||
|
groupIDs: []string{"non-existing-group"},
|
||||||
|
expectedPeers: []string{},
|
||||||
|
expectedCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty group IDs list",
|
||||||
|
groupIDs: []string{},
|
||||||
|
expectedPeers: []string{},
|
||||||
|
expectedCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mix of existing and non-existing groups",
|
||||||
|
groupIDs: []string{group1ID, "non-existing-group"},
|
||||||
|
expectedPeers: []string{peer1, peer2},
|
||||||
|
expectedCount: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_policy_migrate.sql", t.TempDir())
|
||||||
|
t.Cleanup(cleanup)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
groups := []*types.Group{
|
||||||
|
{
|
||||||
|
ID: group1ID,
|
||||||
|
AccountID: accountID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: group2ID,
|
||||||
|
AccountID: accountID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.NoError(t, store.CreateGroups(ctx, accountID, groups))
|
||||||
|
|
||||||
|
require.NoError(t, store.AddPeerToGroup(ctx, accountID, peer1, group1ID))
|
||||||
|
require.NoError(t, store.AddPeerToGroup(ctx, accountID, peer2, group1ID))
|
||||||
|
require.NoError(t, store.AddPeerToGroup(ctx, accountID, peer1, group2ID))
|
||||||
|
|
||||||
|
peers, err := store.GetPeersByGroupIDs(ctx, accountID, tt.groupIDs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, peers, tt.expectedCount)
|
||||||
|
|
||||||
|
if tt.expectedCount > 0 {
|
||||||
|
actualPeerIDs := make([]string, len(peers))
|
||||||
|
for i, peer := range peers {
|
||||||
|
actualPeerIDs[i] = peer.ID
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, tt.expectedPeers, actualPeerIDs)
|
||||||
|
|
||||||
|
// Verify all returned peers belong to the correct account
|
||||||
|
for _, peer := range peers {
|
||||||
|
assert.Equal(t, accountID, peer.AccountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ type Store interface {
|
|||||||
GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error)
|
GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error)
|
||||||
GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) (*nbpeer.Peer, error)
|
GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) (*nbpeer.Peer, error)
|
||||||
GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error)
|
GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error)
|
||||||
|
GetPeersByGroupIDs(ctx context.Context, accountID string, groupIDs []string) ([]*nbpeer.Peer, error)
|
||||||
GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error)
|
GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error)
|
||||||
GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error)
|
GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error)
|
||||||
GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error)
|
GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error)
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ type ExtraSettings struct {
|
|||||||
// PeerApprovalEnabled enables or disables the need for peers bo be approved by an administrator
|
// PeerApprovalEnabled enables or disables the need for peers bo be approved by an administrator
|
||||||
PeerApprovalEnabled bool
|
PeerApprovalEnabled bool
|
||||||
|
|
||||||
|
// UserApprovalRequired enables or disables the need for users joining via domain matching to be approved by an administrator
|
||||||
|
UserApprovalRequired bool
|
||||||
|
|
||||||
// IntegratedValidator is the string enum for the integrated validator type
|
// IntegratedValidator is the string enum for the integrated validator type
|
||||||
IntegratedValidator string
|
IntegratedValidator string
|
||||||
// IntegratedValidatorGroups list of group IDs to be used with integrated approval configurations
|
// IntegratedValidatorGroups list of group IDs to be used with integrated approval configurations
|
||||||
@@ -103,6 +106,7 @@ type ExtraSettings struct {
|
|||||||
func (e *ExtraSettings) Copy() *ExtraSettings {
|
func (e *ExtraSettings) Copy() *ExtraSettings {
|
||||||
return &ExtraSettings{
|
return &ExtraSettings{
|
||||||
PeerApprovalEnabled: e.PeerApprovalEnabled,
|
PeerApprovalEnabled: e.PeerApprovalEnabled,
|
||||||
|
UserApprovalRequired: e.UserApprovalRequired,
|
||||||
IntegratedValidatorGroups: slices.Clone(e.IntegratedValidatorGroups),
|
IntegratedValidatorGroups: slices.Clone(e.IntegratedValidatorGroups),
|
||||||
IntegratedValidator: e.IntegratedValidator,
|
IntegratedValidator: e.IntegratedValidator,
|
||||||
FlowEnabled: e.FlowEnabled,
|
FlowEnabled: e.FlowEnabled,
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ type UserInfo struct {
|
|||||||
NonDeletable bool `json:"non_deletable"`
|
NonDeletable bool `json:"non_deletable"`
|
||||||
LastLogin time.Time `json:"last_login"`
|
LastLogin time.Time `json:"last_login"`
|
||||||
Issued string `json:"issued"`
|
Issued string `json:"issued"`
|
||||||
|
PendingApproval bool `json:"pending_approval"`
|
||||||
IntegrationReference integration_reference.IntegrationReference `json:"-"`
|
IntegrationReference integration_reference.IntegrationReference `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +85,8 @@ type User struct {
|
|||||||
PATsG []PersonalAccessToken `json:"-" gorm:"foreignKey:UserID;references:id;constraint:OnDelete:CASCADE;"`
|
PATsG []PersonalAccessToken `json:"-" gorm:"foreignKey:UserID;references:id;constraint:OnDelete:CASCADE;"`
|
||||||
// Blocked indicates whether the user is blocked. Blocked users can't use the system.
|
// Blocked indicates whether the user is blocked. Blocked users can't use the system.
|
||||||
Blocked bool
|
Blocked bool
|
||||||
|
// PendingApproval indicates whether the user requires approval before being activated
|
||||||
|
PendingApproval bool
|
||||||
// LastLogin is the last time the user logged in to IdP
|
// LastLogin is the last time the user logged in to IdP
|
||||||
LastLogin *time.Time
|
LastLogin *time.Time
|
||||||
// CreatedAt records the time the user was created
|
// CreatedAt records the time the user was created
|
||||||
@@ -141,16 +144,17 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
|
|||||||
|
|
||||||
if userData == nil {
|
if userData == nil {
|
||||||
return &UserInfo{
|
return &UserInfo{
|
||||||
ID: u.Id,
|
ID: u.Id,
|
||||||
Email: "",
|
Email: "",
|
||||||
Name: u.ServiceUserName,
|
Name: u.ServiceUserName,
|
||||||
Role: string(u.Role),
|
Role: string(u.Role),
|
||||||
AutoGroups: u.AutoGroups,
|
AutoGroups: u.AutoGroups,
|
||||||
Status: string(UserStatusActive),
|
Status: string(UserStatusActive),
|
||||||
IsServiceUser: u.IsServiceUser,
|
IsServiceUser: u.IsServiceUser,
|
||||||
IsBlocked: u.Blocked,
|
IsBlocked: u.Blocked,
|
||||||
LastLogin: u.GetLastLogin(),
|
LastLogin: u.GetLastLogin(),
|
||||||
Issued: u.Issued,
|
Issued: u.Issued,
|
||||||
|
PendingApproval: u.PendingApproval,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
if userData.ID != u.Id {
|
if userData.ID != u.Id {
|
||||||
@@ -163,16 +167,17 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &UserInfo{
|
return &UserInfo{
|
||||||
ID: u.Id,
|
ID: u.Id,
|
||||||
Email: userData.Email,
|
Email: userData.Email,
|
||||||
Name: userData.Name,
|
Name: userData.Name,
|
||||||
Role: string(u.Role),
|
Role: string(u.Role),
|
||||||
AutoGroups: autoGroups,
|
AutoGroups: autoGroups,
|
||||||
Status: string(userStatus),
|
Status: string(userStatus),
|
||||||
IsServiceUser: u.IsServiceUser,
|
IsServiceUser: u.IsServiceUser,
|
||||||
IsBlocked: u.Blocked,
|
IsBlocked: u.Blocked,
|
||||||
LastLogin: u.GetLastLogin(),
|
LastLogin: u.GetLastLogin(),
|
||||||
Issued: u.Issued,
|
Issued: u.Issued,
|
||||||
|
PendingApproval: u.PendingApproval,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +199,7 @@ func (u *User) Copy() *User {
|
|||||||
ServiceUserName: u.ServiceUserName,
|
ServiceUserName: u.ServiceUserName,
|
||||||
PATs: pats,
|
PATs: pats,
|
||||||
Blocked: u.Blocked,
|
Blocked: u.Blocked,
|
||||||
|
PendingApproval: u.PendingApproval,
|
||||||
LastLogin: u.LastLogin,
|
LastLogin: u.LastLogin,
|
||||||
CreatedAt: u.CreatedAt,
|
CreatedAt: u.CreatedAt,
|
||||||
Issued: u.Issued,
|
Issued: u.Issued,
|
||||||
|
|||||||
@@ -942,6 +942,11 @@ func (am *DefaultAccountManager) expireAndUpdatePeers(ctx context.Context, accou
|
|||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
ctx = context.WithValue(ctx, nbContext.PeerIDKey, peer.Key)
|
ctx = context.WithValue(ctx, nbContext.PeerIDKey, peer.Key)
|
||||||
|
|
||||||
|
if peer.UserID == "" {
|
||||||
|
// we do not want to expire peers that are added via setup key
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if peer.Status.LoginExpired {
|
if peer.Status.LoginExpired {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1207,3 +1212,77 @@ func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, userAut
|
|||||||
|
|
||||||
return userWithPermissions, nil
|
return userWithPermissions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApproveUser approves a user that is pending approval
|
||||||
|
func (am *DefaultAccountManager) ApproveUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) (*types.UserInfo, error) {
|
||||||
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.NewPermissionValidationError(err)
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
return nil, status.NewPermissionDeniedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.AccountID != accountID {
|
||||||
|
return nil, status.NewUserNotFoundError(targetUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.PendingApproval {
|
||||||
|
return nil, status.Errorf(status.InvalidArgument, "user %s is not pending approval", targetUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Blocked = false
|
||||||
|
user.PendingApproval = false
|
||||||
|
|
||||||
|
err = am.Store.SaveUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
am.StoreEvent(ctx, initiatorUserID, targetUserID, accountID, activity.UserApproved, nil)
|
||||||
|
|
||||||
|
userInfo, err := am.getUserInfo(ctx, user, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RejectUser rejects a user that is pending approval by deleting them
|
||||||
|
func (am *DefaultAccountManager) RejectUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) error {
|
||||||
|
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, initiatorUserID, modules.Users, operations.Delete)
|
||||||
|
if err != nil {
|
||||||
|
return status.NewPermissionValidationError(err)
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
return status.NewPermissionDeniedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.AccountID != accountID {
|
||||||
|
return status.NewUserNotFoundError(targetUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.PendingApproval {
|
||||||
|
return status.Errorf(status.InvalidArgument, "user %s is not pending approval", targetUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.DeleteUser(ctx, accountID, initiatorUserID, targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
am.StoreEvent(ctx, initiatorUserID, targetUserID, accountID, activity.UserRejected, nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1746,3 +1746,117 @@ func mergeRolePermissions(role roles.RolePermissions) roles.Permissions {
|
|||||||
|
|
||||||
return permissions
|
return permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApproveUser(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create account with admin and pending approval user
|
||||||
|
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", false)
|
||||||
|
err = manager.Store.SaveAccount(context.Background(), account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create admin user
|
||||||
|
adminUser := types.NewAdminUser("admin-user")
|
||||||
|
adminUser.AccountID = account.Id
|
||||||
|
err = manager.Store.SaveUser(context.Background(), adminUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create user pending approval
|
||||||
|
pendingUser := types.NewRegularUser("pending-user")
|
||||||
|
pendingUser.AccountID = account.Id
|
||||||
|
pendingUser.Blocked = true
|
||||||
|
pendingUser.PendingApproval = true
|
||||||
|
err = manager.Store.SaveUser(context.Background(), pendingUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test successful approval
|
||||||
|
approvedUser, err := manager.ApproveUser(context.Background(), account.Id, adminUser.Id, pendingUser.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, approvedUser.IsBlocked)
|
||||||
|
assert.False(t, approvedUser.PendingApproval)
|
||||||
|
|
||||||
|
// Verify user is updated in store
|
||||||
|
updatedUser, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, pendingUser.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, updatedUser.Blocked)
|
||||||
|
assert.False(t, updatedUser.PendingApproval)
|
||||||
|
|
||||||
|
// Test approval of non-pending user should fail
|
||||||
|
_, err = manager.ApproveUser(context.Background(), account.Id, adminUser.Id, pendingUser.Id)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not pending approval")
|
||||||
|
|
||||||
|
// Test approval by non-admin should fail
|
||||||
|
regularUser := types.NewRegularUser("regular-user")
|
||||||
|
regularUser.AccountID = account.Id
|
||||||
|
err = manager.Store.SaveUser(context.Background(), regularUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pendingUser2 := types.NewRegularUser("pending-user-2")
|
||||||
|
pendingUser2.AccountID = account.Id
|
||||||
|
pendingUser2.Blocked = true
|
||||||
|
pendingUser2.PendingApproval = true
|
||||||
|
err = manager.Store.SaveUser(context.Background(), pendingUser2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = manager.ApproveUser(context.Background(), account.Id, regularUser.Id, pendingUser2.Id)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRejectUser(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create account with admin and pending approval user
|
||||||
|
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", false)
|
||||||
|
err = manager.Store.SaveAccount(context.Background(), account)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create admin user
|
||||||
|
adminUser := types.NewAdminUser("admin-user")
|
||||||
|
adminUser.AccountID = account.Id
|
||||||
|
err = manager.Store.SaveUser(context.Background(), adminUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create user pending approval
|
||||||
|
pendingUser := types.NewRegularUser("pending-user")
|
||||||
|
pendingUser.AccountID = account.Id
|
||||||
|
pendingUser.Blocked = true
|
||||||
|
pendingUser.PendingApproval = true
|
||||||
|
err = manager.Store.SaveUser(context.Background(), pendingUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test successful rejection
|
||||||
|
err = manager.RejectUser(context.Background(), account.Id, adminUser.Id, pendingUser.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify user is deleted from store
|
||||||
|
_, err = manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, pendingUser.Id)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Test rejection of non-pending user should fail
|
||||||
|
regularUser := types.NewRegularUser("regular-user")
|
||||||
|
regularUser.AccountID = account.Id
|
||||||
|
err = manager.Store.SaveUser(context.Background(), regularUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager.RejectUser(context.Background(), account.Id, adminUser.Id, regularUser.Id)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not pending approval")
|
||||||
|
|
||||||
|
// Test rejection by non-admin should fail
|
||||||
|
pendingUser2 := types.NewRegularUser("pending-user-2")
|
||||||
|
pendingUser2.AccountID = account.Id
|
||||||
|
pendingUser2.Blocked = true
|
||||||
|
pendingUser2.PendingApproval = true
|
||||||
|
err = manager.Store.SaveUser(context.Background(), pendingUser2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager.RejectUser(context.Background(), account.Id, regularUser.Id, pendingUser2.Id)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,7 +73,12 @@ func (l *Listener) Shutdown(ctx context.Context) error {
|
|||||||
|
|
||||||
func (l *Listener) onAccept(w http.ResponseWriter, r *http.Request) {
|
func (l *Listener) onAccept(w http.ResponseWriter, r *http.Request) {
|
||||||
connRemoteAddr := remoteAddr(r)
|
connRemoteAddr := remoteAddr(r)
|
||||||
wsConn, err := websocket.Accept(w, r, nil)
|
|
||||||
|
acceptOptions := &websocket.AcceptOptions{
|
||||||
|
OriginPatterns: []string{"*"},
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConn, err := websocket.Accept(w, r, acceptOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to accept ws connection from %s: %s", connRemoteAddr, err)
|
log.Errorf("failed to accept ws connection from %s: %s", connRemoteAddr, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,34 +9,30 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/system"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
|
||||||
"github.com/netbirdio/netbird/management/server/groups"
|
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/encryption"
|
|
||||||
mgmt "github.com/netbirdio/netbird/management/server"
|
|
||||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
|
||||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/encryption"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
mgmt "github.com/netbirdio/netbird/management/server"
|
||||||
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/groups"
|
||||||
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||||
|
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||||
|
"github.com/netbirdio/netbird/management/server/peers"
|
||||||
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,13 +68,31 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
|||||||
|
|
||||||
peersUpdateManager := mgmt.NewPeersUpdateManager(nil)
|
peersUpdateManager := mgmt.NewPeersUpdateManager(nil)
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
ia, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
t.Cleanup(ctrl.Finish)
|
||||||
|
|
||||||
|
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
||||||
|
permissionsManagerMock.
|
||||||
|
EXPECT().
|
||||||
|
ValidateUserPermissions(
|
||||||
|
gomock.Any(),
|
||||||
|
gomock.Any(),
|
||||||
|
gomock.Any(),
|
||||||
|
gomock.Any(),
|
||||||
|
gomock.Any(),
|
||||||
|
).
|
||||||
|
Return(true, nil).
|
||||||
|
AnyTimes()
|
||||||
|
|
||||||
|
peersManger := peers.NewManager(store, permissionsManagerMock)
|
||||||
|
settingsManagerMock := settings.NewMockManager(ctrl)
|
||||||
|
|
||||||
|
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManger, settingsManagerMock, eventStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
t.Cleanup(ctrl.Finish)
|
|
||||||
settingsMockManager := settings.NewMockManager(ctrl)
|
settingsMockManager := settings.NewMockManager(ctrl)
|
||||||
settingsMockManager.
|
settingsMockManager.
|
||||||
EXPECT().
|
EXPECT().
|
||||||
@@ -95,19 +109,6 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
|||||||
Return(&types.ExtraSettings{}, nil).
|
Return(&types.ExtraSettings{}, nil).
|
||||||
AnyTimes()
|
AnyTimes()
|
||||||
|
|
||||||
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
|
||||||
permissionsManagerMock.
|
|
||||||
EXPECT().
|
|
||||||
ValidateUserPermissions(
|
|
||||||
gomock.Any(),
|
|
||||||
gomock.Any(),
|
|
||||||
gomock.Any(),
|
|
||||||
gomock.Any(),
|
|
||||||
gomock.Any(),
|
|
||||||
).
|
|
||||||
Return(true, nil).
|
|
||||||
AnyTimes()
|
|
||||||
|
|
||||||
accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false)
|
accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -162,6 +162,10 @@ components:
|
|||||||
description: (Cloud only) Enables or disables peer approval globally. If enabled, all peers added will be in pending state until approved by an admin.
|
description: (Cloud only) Enables or disables peer approval globally. If enabled, all peers added will be in pending state until approved by an admin.
|
||||||
type: boolean
|
type: boolean
|
||||||
example: true
|
example: true
|
||||||
|
user_approval_required:
|
||||||
|
description: Enables manual approval for new users joining via domain matching. When enabled, users are blocked with pending approval status until explicitly approved by an admin.
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
network_traffic_logs_enabled:
|
network_traffic_logs_enabled:
|
||||||
description: Enables or disables network traffic logging. If enabled, all network traffic events from peers will be stored.
|
description: Enables or disables network traffic logging. If enabled, all network traffic events from peers will be stored.
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -178,6 +182,7 @@ components:
|
|||||||
example: true
|
example: true
|
||||||
required:
|
required:
|
||||||
- peer_approval_enabled
|
- peer_approval_enabled
|
||||||
|
- user_approval_required
|
||||||
- network_traffic_logs_enabled
|
- network_traffic_logs_enabled
|
||||||
- network_traffic_logs_groups
|
- network_traffic_logs_groups
|
||||||
- network_traffic_packet_counter_enabled
|
- network_traffic_packet_counter_enabled
|
||||||
@@ -239,6 +244,10 @@ components:
|
|||||||
description: Is true if this user is blocked. Blocked users can't use the system
|
description: Is true if this user is blocked. Blocked users can't use the system
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
|
pending_approval:
|
||||||
|
description: Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
issued:
|
issued:
|
||||||
description: How user was issued by API or Integration
|
description: How user was issued by API or Integration
|
||||||
type: string
|
type: string
|
||||||
@@ -253,6 +262,7 @@ components:
|
|||||||
- auto_groups
|
- auto_groups
|
||||||
- status
|
- status
|
||||||
- is_blocked
|
- is_blocked
|
||||||
|
- pending_approval
|
||||||
UserPermissions:
|
UserPermissions:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -2548,6 +2558,63 @@ paths:
|
|||||||
"$ref": "#/components/responses/forbidden"
|
"$ref": "#/components/responses/forbidden"
|
||||||
'500':
|
'500':
|
||||||
"$ref": "#/components/responses/internal_error"
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
/api/users/{userId}/approve:
|
||||||
|
post:
|
||||||
|
summary: Approve user
|
||||||
|
description: Approve a user that is pending approval
|
||||||
|
tags: [ Users ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
- TokenAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The unique identifier of a user
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Returns the approved user
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
"$ref": "#/components/schemas/User"
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
/api/users/{userId}/reject:
|
||||||
|
delete:
|
||||||
|
summary: Reject user
|
||||||
|
description: Reject a user that is pending approval by removing them from the account
|
||||||
|
tags: [ Users ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
- TokenAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The unique identifier of a user
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User rejected successfully
|
||||||
|
content: {}
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
/api/users/current:
|
/api/users/current:
|
||||||
get:
|
get:
|
||||||
summary: Retrieve current user
|
summary: Retrieve current user
|
||||||
|
|||||||
@@ -268,6 +268,9 @@ type AccountExtraSettings struct {
|
|||||||
|
|
||||||
// PeerApprovalEnabled (Cloud only) Enables or disables peer approval globally. If enabled, all peers added will be in pending state until approved by an admin.
|
// PeerApprovalEnabled (Cloud only) Enables or disables peer approval globally. If enabled, all peers added will be in pending state until approved by an admin.
|
||||||
PeerApprovalEnabled bool `json:"peer_approval_enabled"`
|
PeerApprovalEnabled bool `json:"peer_approval_enabled"`
|
||||||
|
|
||||||
|
// UserApprovalRequired Enables manual approval for new users joining via domain matching. When enabled, users are blocked with pending approval status until explicitly approved by an admin.
|
||||||
|
UserApprovalRequired bool `json:"user_approval_required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountOnboarding defines model for AccountOnboarding.
|
// AccountOnboarding defines model for AccountOnboarding.
|
||||||
@@ -1018,8 +1021,6 @@ type OSVersionCheck struct {
|
|||||||
|
|
||||||
// Peer defines model for Peer.
|
// Peer defines model for Peer.
|
||||||
type Peer struct {
|
type Peer struct {
|
||||||
// CreatedAt Peer creation date (UTC)
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
// ApprovalRequired (Cloud only) Indicates whether peer needs approval
|
// ApprovalRequired (Cloud only) Indicates whether peer needs approval
|
||||||
ApprovalRequired bool `json:"approval_required"`
|
ApprovalRequired bool `json:"approval_required"`
|
||||||
|
|
||||||
@@ -1035,6 +1036,9 @@ type Peer struct {
|
|||||||
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
|
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
|
||||||
CountryCode CountryCode `json:"country_code"`
|
CountryCode CountryCode `json:"country_code"`
|
||||||
|
|
||||||
|
// CreatedAt Peer creation date (UTC)
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|
||||||
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
|
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
|
||||||
DnsLabel string `json:"dns_label"`
|
DnsLabel string `json:"dns_label"`
|
||||||
|
|
||||||
@@ -1101,8 +1105,6 @@ type Peer struct {
|
|||||||
|
|
||||||
// PeerBatch defines model for PeerBatch.
|
// PeerBatch defines model for PeerBatch.
|
||||||
type PeerBatch struct {
|
type PeerBatch struct {
|
||||||
// CreatedAt Peer creation date (UTC)
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
// AccessiblePeersCount Number of accessible peers
|
// AccessiblePeersCount Number of accessible peers
|
||||||
AccessiblePeersCount int `json:"accessible_peers_count"`
|
AccessiblePeersCount int `json:"accessible_peers_count"`
|
||||||
|
|
||||||
@@ -1121,6 +1123,9 @@ type PeerBatch struct {
|
|||||||
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
|
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country
|
||||||
CountryCode CountryCode `json:"country_code"`
|
CountryCode CountryCode `json:"country_code"`
|
||||||
|
|
||||||
|
// CreatedAt Peer creation date (UTC)
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|
||||||
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
|
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
|
||||||
DnsLabel string `json:"dns_label"`
|
DnsLabel string `json:"dns_label"`
|
||||||
|
|
||||||
@@ -1777,8 +1782,11 @@ type User struct {
|
|||||||
LastLogin *time.Time `json:"last_login,omitempty"`
|
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||||
|
|
||||||
// Name User's name from idp provider
|
// Name User's name from idp provider
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Permissions *UserPermissions `json:"permissions,omitempty"`
|
|
||||||
|
// PendingApproval Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
|
||||||
|
PendingApproval bool `json:"pending_approval"`
|
||||||
|
Permissions *UserPermissions `json:"permissions,omitempty"`
|
||||||
|
|
||||||
// Role User's NetBird account role
|
// Role User's NetBird account role
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ func NewUserBlockedError() error {
|
|||||||
return Errorf(PermissionDenied, "user is blocked")
|
return Errorf(PermissionDenied, "user is blocked")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewUserPendingApprovalError creates a new Error with PermissionDenied type for a blocked user pending approval
|
||||||
|
func NewUserPendingApprovalError() error {
|
||||||
|
return Errorf(PermissionDenied, "user is pending approval")
|
||||||
|
}
|
||||||
|
|
||||||
// NewPeerNotRegisteredError creates a new Error with Unauthenticated type unregistered peer
|
// NewPeerNotRegisteredError creates a new Error with Unauthenticated type unregistered peer
|
||||||
func NewPeerNotRegisteredError() error {
|
func NewPeerNotRegisteredError() error {
|
||||||
return Errorf(Unauthenticated, "peer is not registered")
|
return Errorf(Unauthenticated, "peer is not registered")
|
||||||
|
|||||||
Reference in New Issue
Block a user