\xff\v\x8d?*\x80\xe1qe\xec\xd7\x0f\xb5\xb3O+\xed%\x10\x9e\xfa?J\x7f*\x80\x94\xb2\xbfcc\xbf\x9e\xe9\x06\xdb\xd6b\xeb\xa3\xd5\xcaV#\xc9\xfc;\xff>\x8f\xcd\x17\x15@\n\xd9\xff\x88\xb7\xfb\xb8\x9d\x06w\\\x85*t\x82Ќv\xd45DZ\xf9B\x00\x86\xa7\u05f7\xfb\xb8.\x02U:F\x8c\xcd\x17\x02\x90ޗv|\xec\xd7OV\xac\x88\b`\xf3\x85\x00\xa4\xb4\xf7\xf7d\xec\x87\b\xfc\x82\x95\xb0\xf9\xca\xea\xfe\xa9T\xf0\xfb\xdc\xf8;*H6k\xda\xfa\xe6\xac\xd4-\x97\xa6\xf3\xbc?\x15@j\xe4e\xf3UT%PFC\x11l\xbe\xa8\x00Ra\xe5\xd3(\n\x12}U\xf6\xefi\u05cfi\xeb\xd1\xe9RT\x02\xc1\xf8F\xeb\x9d\xcf\xff\x82\x00P\x01\xa4\xf0E\xad\xc7c\xbf~\x14\xbd\xbeQ\n/\x013\xdaQwk\x92\xc6\x1f\x02\x90B\xf6/\xd0\xe6\xab\b\xc2\x13/\xfcw\x15\x1a\xed\xdcf\xec\x87\x00\f\x8d\v6_\x85\x89\x80\xbf^\x02\xd8|!\x00)\xed\x89\x03]\xadR\xf6\x7fM\x04<5\x14\t\xde_\xc6\xe6+\xaf\xadVٳ\x7fY\xc7~\xfd\xe0\x93\xb5\x18c?*\x80\xd4(\xf3د\xac\x95\x006_\b@*\xac܉\xae\xe4\xf9v\x1f/D\xc0qW\xa1`|\x83\xe7\xfd\x11\x80\x94\xbeX@\xf6\x7fC\x04\xdcv\x15\xc2\xe6\v\x01H\x87\xb5\xb9\xa8\xb2\x8d\xbf\xa3\xa8\x9d[\x92\x99x\xe9\xde\xde\x1f\x9b\xafbֽl_\xc8G\x9b\xaf\xdcq\xccP\xc4Ԓ\xf6\xd4\x7f\xcc\xd3\xf8\xa3\x02\x18\x1e\x1fm\xbe\xf2\xdf\v\xec\x18\x8a8b-\x16\x9e}J\xe9O\x05\x90R\xf6g\xec\xd73v\xb3\xa6Σ3\xb2\x9bŽ\x1e\x02\x9b/*\x80\x14S\x89\xeesI\xfbP\xff\x82\xbd\x04\xb0\xf9B\x00Rc\xe5Nt\xc5J\x17\xb9\xa4\xfe\x88\x80\x19\xe92\xf6C\x00R\xfa\"\x8c\xfd|\x13\x01\xc6~\b@J{\xff\x92\xd9|\x15&\x02\xe7\x7f\xcc\xcdP\x04\x9b/G\xae\xbb\xf7\xc1ߌ\x1aI\xa8\xaf\x8c4\xcd\xe5L!0s0\x14\xe1\xed>T\x00\xa9\x91\xd4t\x8b\xe0O18\xeb\x1b\x1a9\xffc\xb67\xdd\xd4\x06.?T\x00\xe9d\x7f\xc6~\xd9\xd0}6\xa9\xee\xe2\xa9\xf4o8cZS\x7f\xfa\x13\x02@\x05\x90B\xb9Z\xd3C.a6d\xe1*dF;JFFh\xfc!\x00\xc3S5\x9b\xaf\xc2D \xcdc\xc4\xd8|!\x00\xa9d\xfef4\xcd\xd8/'\x11H\xcfK\x00\x9b/\x04 \x1d^\x86ⴟg\"\x10\x1c\xdf\xc2\xe6\xcbA\xbck\x02\xd2\xf8+\x8eA\xadŰ\xf9\xa2\x02H\rl\xbe\x8a\xad\x04\x061\x14\xc1\xe6\v\x01H\x85չ\xe8\x126_\xc5R;\xb7ԗ\b`\xf3\x85\x00\xa4\xb7_1\xfa\x8cK\xe6\x86\b\xf4\xe4%\x10&\xcb#'W\x19\xfb!\x00\xc3\xc3\xd8\xcf-z1\x141\xf5\xcd{\xf5\xbb\vd\x7f\x97\x93\xaa\x0f\x1f\x12\x9b/G\xe9\x06\xda\xfa\xe6\xecA\x86\"\xed\xe9?\xff\x99\xc6\x1f\x15\xc0\xf0`\xf3\xe5(ar\xe01\xe2Zc\x89ҟ\n \xa5\xec\xcf\xd8\xcfi\xf6[\x8ba\xf3E\x05\x90\xde\xcd\x15\xd2\xf8s>\x8b\xec1\x141a\x82͗G\xd4\\\xfep\xabs\xd1%\x19E\x92\xda\\*\xc7E\xe0XG#\xff\xf0X\x1b\x8b\xa7\xbe\x9c\xf9\xc3<\xd7\v\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\xe4\xff\x01\xf6P(\xf3)+S\x1f\x00\x00\x00\x00IEND\xaeB`\x82"),
-}
diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go
index 49b0f53cf..51eec59a5 100644
--- a/client/ui/client_ui.go
+++ b/client/ui/client_ui.go
@@ -1,4 +1,4 @@
-//go:build !(linux && 386) && !freebsd
+//go:build !(linux && 386)
package main
@@ -21,6 +21,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/dialog"
+ "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"fyne.io/systray"
"github.com/cenkalti/backoff/v4"
@@ -32,7 +33,8 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto"
- "github.com/netbirdio/netbird/client/system"
+ "github.com/netbirdio/netbird/client/ui/desktop"
+ "github.com/netbirdio/netbird/client/ui/event"
"github.com/netbirdio/netbird/util"
"github.com/netbirdio/netbird/version"
)
@@ -82,7 +84,7 @@ func main() {
}
a := app.NewWithID("NetBird")
- a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnectedPNG))
+ a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnected))
if errorMSG != "" {
showErrorMSG(errorMSG)
@@ -90,6 +92,14 @@ func main() {
}
client := newServiceClient(daemonAddr, a, showSettings, showRoutes)
+ settingsChangeChan := make(chan fyne.Settings)
+ a.Settings().AddChangeListener(settingsChangeChan)
+ go func() {
+ for range settingsChangeChan {
+ client.updateIcon()
+ }
+ }()
+
if showSettings || showRoutes {
a.Run()
} else {
@@ -106,63 +116,56 @@ func main() {
}
}
-//go:embed netbird-systemtray-connected.ico
-var iconConnectedICO []byte
+//go:embed netbird-systemtray-connected-macos.png
+var iconConnectedMacOS []byte
-//go:embed netbird-systemtray-connected.png
-var iconConnectedPNG []byte
+//go:embed netbird-systemtray-disconnected-macos.png
+var iconDisconnectedMacOS []byte
-//go:embed netbird-systemtray-disconnected.ico
-var iconDisconnectedICO []byte
+//go:embed netbird-systemtray-update-disconnected-macos.png
+var iconUpdateDisconnectedMacOS []byte
-//go:embed netbird-systemtray-disconnected.png
-var iconDisconnectedPNG []byte
+//go:embed netbird-systemtray-update-connected-macos.png
+var iconUpdateConnectedMacOS []byte
-//go:embed netbird-systemtray-update-disconnected.ico
-var iconUpdateDisconnectedICO []byte
+//go:embed netbird-systemtray-connecting-macos.png
+var iconConnectingMacOS []byte
-//go:embed netbird-systemtray-update-disconnected.png
-var iconUpdateDisconnectedPNG []byte
-
-//go:embed netbird-systemtray-update-connected.ico
-var iconUpdateConnectedICO []byte
-
-//go:embed netbird-systemtray-update-connected.png
-var iconUpdateConnectedPNG []byte
-
-//go:embed netbird-systemtray-update-cloud.ico
-var iconUpdateCloudICO []byte
-
-//go:embed netbird-systemtray-update-cloud.png
-var iconUpdateCloudPNG []byte
+//go:embed netbird-systemtray-error-macos.png
+var iconErrorMacOS []byte
type serviceClient struct {
ctx context.Context
addr string
conn proto.DaemonServiceClient
+ icAbout []byte
icConnected []byte
icDisconnected []byte
icUpdateConnected []byte
icUpdateDisconnected []byte
- icUpdateCloud []byte
+ icConnecting []byte
+ icError []byte
// systray menu items
- mStatus *systray.MenuItem
- mUp *systray.MenuItem
- mDown *systray.MenuItem
- mAdminPanel *systray.MenuItem
- mSettings *systray.MenuItem
- mAbout *systray.MenuItem
- mVersionUI *systray.MenuItem
- mVersionDaemon *systray.MenuItem
- mUpdate *systray.MenuItem
- mQuit *systray.MenuItem
- mRoutes *systray.MenuItem
- mAllowSSH *systray.MenuItem
- mAutoConnect *systray.MenuItem
- mEnableRosenpass *systray.MenuItem
- mAdvancedSettings *systray.MenuItem
+ mStatus *systray.MenuItem
+ mUp *systray.MenuItem
+ mDown *systray.MenuItem
+ mAdminPanel *systray.MenuItem
+ mSettings *systray.MenuItem
+ mAbout *systray.MenuItem
+ mVersionUI *systray.MenuItem
+ mVersionDaemon *systray.MenuItem
+ mUpdate *systray.MenuItem
+ mQuit *systray.MenuItem
+ mNetworks *systray.MenuItem
+ mAllowSSH *systray.MenuItem
+ mAutoConnect *systray.MenuItem
+ mEnableRosenpass *systray.MenuItem
+ mNotifications *systray.MenuItem
+ mAdvancedSettings *systray.MenuItem
+ mCreateDebugBundle *systray.MenuItem
+ mExitNode *systray.MenuItem
// application with main windows.
app fyne.App
@@ -197,6 +200,16 @@ type serviceClient struct {
isUpdateIconActive bool
showRoutes bool
wRoutes fyne.Window
+
+ eventManager *event.Manager
+
+ exitNodeMu sync.Mutex
+ mExitNodeItems []menuHandler
+}
+
+type menuHandler struct {
+ *systray.MenuItem
+ cancel context.CancelFunc
}
// newServiceClient instance constructor
@@ -214,20 +227,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo
update: version.NewUpdate(),
}
- if runtime.GOOS == "windows" {
- s.icConnected = iconConnectedICO
- s.icDisconnected = iconDisconnectedICO
- s.icUpdateConnected = iconUpdateConnectedICO
- s.icUpdateDisconnected = iconUpdateDisconnectedICO
- s.icUpdateCloud = iconUpdateCloudICO
-
- } else {
- s.icConnected = iconConnectedPNG
- s.icDisconnected = iconDisconnectedPNG
- s.icUpdateConnected = iconUpdateConnectedPNG
- s.icUpdateDisconnected = iconUpdateDisconnectedPNG
- s.icUpdateCloud = iconUpdateCloudPNG
- }
+ s.setNewIcons()
if showSettings {
s.showSettingsUI()
@@ -239,6 +239,44 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo
return s
}
+func (s *serviceClient) setNewIcons() {
+ s.icAbout = iconAbout
+ if s.app.Settings().ThemeVariant() == theme.VariantDark {
+ s.icConnected = iconConnectedDark
+ s.icDisconnected = iconDisconnected
+ s.icUpdateConnected = iconUpdateConnectedDark
+ s.icUpdateDisconnected = iconUpdateDisconnectedDark
+ s.icConnecting = iconConnectingDark
+ s.icError = iconErrorDark
+ } else {
+ s.icConnected = iconConnected
+ s.icDisconnected = iconDisconnected
+ s.icUpdateConnected = iconUpdateConnected
+ s.icUpdateDisconnected = iconUpdateDisconnected
+ s.icConnecting = iconConnecting
+ s.icError = iconError
+ }
+}
+
+func (s *serviceClient) updateIcon() {
+ s.setNewIcons()
+ s.updateIndicationLock.Lock()
+ if s.connected {
+ if s.isUpdateIconActive {
+ systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected)
+ } else {
+ systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected)
+ }
+ } else {
+ if s.isUpdateIconActive {
+ systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected)
+ } else {
+ systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected)
+ }
+ }
+ s.updateIndicationLock.Unlock()
+}
+
func (s *serviceClient) showSettingsUI() {
// add settings window UI elements.
s.wSettings = s.app.NewWindow("NetBird Settings")
@@ -376,8 +414,10 @@ func (s *serviceClient) login() error {
}
func (s *serviceClient) menuUpClick() error {
+ systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting)
conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil {
+ systray.SetTemplateIcon(iconErrorMacOS, s.icError)
log.Errorf("get client: %v", err)
return err
}
@@ -403,10 +443,12 @@ func (s *serviceClient) menuUpClick() error {
log.Errorf("up service: %v", err)
return err
}
+
return nil
}
func (s *serviceClient) menuDownClick() error {
+ systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting)
conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil {
log.Errorf("get client: %v", err)
@@ -441,6 +483,9 @@ func (s *serviceClient) updateStatus() error {
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
if err != nil {
log.Errorf("get service status: %v", err)
+ if s.connected {
+ s.app.SendNotification(fyne.NewNotification("Error", "Connection to service lost"))
+ }
s.setDisconnectedStatus()
return err
}
@@ -458,15 +503,16 @@ func (s *serviceClient) updateStatus() error {
s.connected = true
s.sendNotification = true
if s.isUpdateIconActive {
- systray.SetIcon(s.icUpdateConnected)
+ systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected)
} else {
- systray.SetIcon(s.icConnected)
+ systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected)
}
systray.SetTooltip("NetBird (Connected)")
s.mStatus.SetTitle("Connected")
s.mUp.Disable()
s.mDown.Enable()
- s.mRoutes.Enable()
+ s.mNetworks.Enable()
+ go s.updateExitNodes()
systrayIconState = true
} else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() {
s.setDisconnectedStatus()
@@ -482,11 +528,9 @@ func (s *serviceClient) updateStatus() error {
s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion)
if !s.isUpdateIconActive {
if systrayIconState {
- systray.SetIcon(s.icConnected)
- s.mAbout.SetIcon(s.icConnected)
+ systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected)
} else {
- systray.SetIcon(s.icDisconnected)
- s.mAbout.SetIcon(s.icDisconnected)
+ systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected)
}
}
@@ -506,7 +550,6 @@ func (s *serviceClient) updateStatus() error {
Stop: backoff.Stop,
Clock: backoff.SystemClock,
})
-
if err != nil {
return err
}
@@ -517,19 +560,21 @@ func (s *serviceClient) updateStatus() error {
func (s *serviceClient) setDisconnectedStatus() {
s.connected = false
if s.isUpdateIconActive {
- systray.SetIcon(s.icUpdateDisconnected)
+ systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected)
} else {
- systray.SetIcon(s.icDisconnected)
+ systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected)
}
systray.SetTooltip("NetBird (Disconnected)")
s.mStatus.SetTitle("Disconnected")
s.mDown.Disable()
s.mUp.Enable()
- s.mRoutes.Disable()
+ s.mNetworks.Disable()
+ s.mExitNode.Disable()
+ go s.updateExitNodes()
}
func (s *serviceClient) onTrayReady() {
- systray.SetIcon(s.icDisconnected)
+ systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected)
systray.SetTooltip("NetBird")
// setup systray menu items
@@ -546,15 +591,22 @@ func (s *serviceClient) onTrayReady() {
s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", "Allow SSH connections", false)
s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", "Connect automatically when the service starts", false)
s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", "Enable post-quantum security via Rosenpass", false)
+ s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", "Enable notifications", false)
s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", "Advanced settings of the application")
+ s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", "Create and open debug information bundle")
s.loadSettings()
- s.mRoutes = systray.AddMenuItem("Networks", "Open the networks management window")
- s.mRoutes.Disable()
+ s.exitNodeMu.Lock()
+ s.mExitNode = systray.AddMenuItem("Exit Node", "Select exit node for routing traffic")
+ s.mExitNode.Disable()
+ s.exitNodeMu.Unlock()
+
+ s.mNetworks = systray.AddMenuItem("Networks", "Open the networks management window")
+ s.mNetworks.Disable()
systray.AddSeparator()
s.mAbout = systray.AddMenuItem("About", "About")
- s.mAbout.SetIcon(s.icDisconnected)
+ s.mAbout.SetIcon(s.icAbout)
versionString := normalizedVersion(version.NetbirdVersion())
s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString))
s.mVersionUI.Disable()
@@ -569,6 +621,9 @@ func (s *serviceClient) onTrayReady() {
systray.AddSeparator()
s.mQuit = systray.AddMenuItem("Quit", "Quit the client app")
+ // update exit node menu in case service is already connected
+ go s.updateExitNodes()
+
s.update.SetOnUpdateListener(s.onUpdateAvailable)
go func() {
s.getSrvConfig()
@@ -582,6 +637,16 @@ func (s *serviceClient) onTrayReady() {
}
}()
+ s.eventManager = event.NewManager(s.app, s.addr)
+ s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
+ s.eventManager.AddHandler(func(event *proto.SystemEvent) {
+ if event.Category == proto.SystemEvent_SYSTEM {
+ s.updateExitNodes()
+ }
+ })
+
+ go s.eventManager.Start(s.ctx)
+
go func() {
var err error
for {
@@ -594,7 +659,7 @@ func (s *serviceClient) onTrayReady() {
defer s.mUp.Enable()
err := s.menuUpClick()
if err != nil {
- s.runSelfCommand("error-msg", err.Error())
+ s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service"))
return
}
}()
@@ -604,7 +669,7 @@ func (s *serviceClient) onTrayReady() {
defer s.mDown.Enable()
err := s.menuDownClick()
if err != nil {
- s.runSelfCommand("error-msg", err.Error())
+ s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service"))
return
}
}()
@@ -616,7 +681,6 @@ func (s *serviceClient) onTrayReady() {
}
if err := s.updateConfig(); err != nil {
log.Errorf("failed to update config: %v", err)
- return
}
case <-s.mAutoConnect.ClickedCh:
if s.mAutoConnect.Checked() {
@@ -626,7 +690,6 @@ func (s *serviceClient) onTrayReady() {
}
if err := s.updateConfig(); err != nil {
log.Errorf("failed to update config: %v", err)
- return
}
case <-s.mEnableRosenpass.ClickedCh:
if s.mEnableRosenpass.Checked() {
@@ -636,7 +699,6 @@ func (s *serviceClient) onTrayReady() {
}
if err := s.updateConfig(); err != nil {
log.Errorf("failed to update config: %v", err)
- return
}
case <-s.mAdvancedSettings.ClickedCh:
s.mAdvancedSettings.Disable()
@@ -645,6 +707,13 @@ func (s *serviceClient) onTrayReady() {
defer s.getSrvConfig()
s.runSelfCommand("settings", "true")
}()
+ case <-s.mCreateDebugBundle.ClickedCh:
+ go func() {
+ if err := s.createAndOpenDebugBundle(); err != nil {
+ log.Errorf("Failed to create debug bundle: %v", err)
+ s.app.SendNotification(fyne.NewNotification("Error", "Failed to create debug bundle"))
+ }
+ }()
case <-s.mQuit.ClickedCh:
systray.Quit()
return
@@ -653,13 +722,26 @@ func (s *serviceClient) onTrayReady() {
if err != nil {
log.Errorf("%s", err)
}
- case <-s.mRoutes.ClickedCh:
- s.mRoutes.Disable()
+ case <-s.mNetworks.ClickedCh:
+ s.mNetworks.Disable()
go func() {
- defer s.mRoutes.Enable()
+ defer s.mNetworks.Enable()
s.runSelfCommand("networks", "true")
}()
+ case <-s.mNotifications.ClickedCh:
+ if s.mNotifications.Checked() {
+ s.mNotifications.Uncheck()
+ } else {
+ s.mNotifications.Check()
+ }
+ if s.eventManager != nil {
+ s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
+ }
+ if err := s.updateConfig(); err != nil {
+ log.Errorf("failed to update config: %v", err)
+ }
}
+
if err != nil {
log.Errorf("process connection: %v", err)
}
@@ -674,7 +756,11 @@ func (s *serviceClient) runSelfCommand(command, arg string) {
return
}
- cmd := exec.Command(proc, fmt.Sprintf("--%s=%s", command, arg))
+ cmd := exec.Command(proc,
+ fmt.Sprintf("--%s=%s", command, arg),
+ fmt.Sprintf("--daemon-addr=%s", s.addr),
+ )
+
out, err := cmd.CombinedOutput()
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
log.Errorf("start %s UI: %v, %s", command, err, string(out))
@@ -693,7 +779,12 @@ func normalizedVersion(version string) string {
return versionString
}
-func (s *serviceClient) onTrayExit() {}
+// onTrayExit is called when the tray icon is closed.
+func (s *serviceClient) onTrayExit() {
+ for _, item := range s.mExitNodeItems {
+ item.cancel()
+ }
+}
// getSrvClient connection to the service.
func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonServiceClient, error) {
@@ -709,7 +800,7 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService
strings.TrimPrefix(s.addr, "tcp://"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
- grpc.WithUserAgent(system.GetDesktopUIUserAgent()),
+ grpc.WithUserAgent(desktop.GetUIUserAgent()),
)
if err != nil {
return nil, fmt.Errorf("dial service: %w", err)
@@ -759,8 +850,20 @@ func (s *serviceClient) getSrvConfig() {
if !cfg.RosenpassEnabled {
s.sRosenpassPermissive.Disable()
}
-
}
+
+ if s.mNotifications == nil {
+ return
+ }
+ if cfg.DisableNotifications {
+ s.mNotifications.Uncheck()
+ } else {
+ s.mNotifications.Check()
+ }
+ if s.eventManager != nil {
+ s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
+ }
+
}
func (s *serviceClient) onUpdateAvailable() {
@@ -771,9 +874,9 @@ func (s *serviceClient) onUpdateAvailable() {
s.isUpdateIconActive = true
if s.connected {
- systray.SetIcon(s.icUpdateConnected)
+ systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected)
} else {
- systray.SetIcon(s.icUpdateDisconnected)
+ systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected)
}
}
@@ -825,6 +928,15 @@ func (s *serviceClient) loadSettings() {
} else {
s.mEnableRosenpass.Uncheck()
}
+
+ if cfg.DisableNotifications {
+ s.mNotifications.Uncheck()
+ } else {
+ s.mNotifications.Check()
+ }
+ if s.eventManager != nil {
+ s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
+ }
}
// updateConfig updates the configuration parameters
@@ -833,12 +945,14 @@ func (s *serviceClient) updateConfig() error {
disableAutoStart := !s.mAutoConnect.Checked()
sshAllowed := s.mAllowSSH.Checked()
rosenpassEnabled := s.mEnableRosenpass.Checked()
+ notificationsDisabled := !s.mNotifications.Checked()
loginRequest := proto.LoginRequest{
IsLinuxDesktopClient: runtime.GOOS == "linux",
ServerSSHAllowed: &sshAllowed,
RosenpassEnabled: &rosenpassEnabled,
DisableAutoConnect: &disableAutoStart,
+ DisableNotifications: ¬ificationsDisabled,
}
if err := s.restartClient(&loginRequest); err != nil {
@@ -851,17 +965,20 @@ func (s *serviceClient) updateConfig() error {
// restartClient restarts the client connection.
func (s *serviceClient) restartClient(loginRequest *proto.LoginRequest) error {
+ ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout)
+ defer cancel()
+
client, err := s.getSrvClient(failFastTimeout)
if err != nil {
return err
}
- _, err = client.Login(s.ctx, loginRequest)
+ _, err = client.Login(ctx, loginRequest)
if err != nil {
return err
}
- _, err = client.Up(s.ctx, &proto.UpRequest{})
+ _, err = client.Up(ctx, &proto.UpRequest{})
if err != nil {
return err
}
@@ -876,7 +993,7 @@ func openURL(url string) error {
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
- case "linux":
+ case "linux", "freebsd":
err = exec.Command("xdg-open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
diff --git a/client/ui/config/config.go b/client/ui/config/config.go
deleted file mode 100644
index fc3361b61..000000000
--- a/client/ui/config/config.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package config
-
-import (
- "os"
- "runtime"
-)
-
-// ClientConfig basic settings for the UI application.
-type ClientConfig struct {
- configPath string
- logFile string
- daemonAddr string
-}
-
-// Config object with default settings.
-//
-// We are creating this package to extract utility functions from the cmd package
-// reading and parsing the configurations for the client should be done here
-func Config() *ClientConfig {
- defaultConfigPath := "/etc/wiretrustee/config.json"
- defaultLogFile := "/var/log/wiretrustee/client.log"
- if runtime.GOOS == "windows" {
- defaultConfigPath = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "config.json"
- defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "client.log"
- }
-
- defaultDaemonAddr := "unix:///var/run/wiretrustee.sock"
- if runtime.GOOS == "windows" {
- defaultDaemonAddr = "tcp://127.0.0.1:41731"
- }
- return &ClientConfig{
- configPath: defaultConfigPath,
- logFile: defaultLogFile,
- daemonAddr: defaultDaemonAddr,
- }
-}
-
-// DaemonAddr of the gRPC API.
-func (c *ClientConfig) DaemonAddr() string {
- return c.daemonAddr
-}
-
-// LogFile path.
-func (c *ClientConfig) LogFile() string {
- return c.logFile
-}
diff --git a/client/ui/debug.go b/client/ui/debug.go
new file mode 100644
index 000000000..845ea284c
--- /dev/null
+++ b/client/ui/debug.go
@@ -0,0 +1,50 @@
+//go:build !(linux && 386)
+
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "fyne.io/fyne/v2"
+ "github.com/skratchdot/open-golang/open"
+
+ "github.com/netbirdio/netbird/client/proto"
+ nbstatus "github.com/netbirdio/netbird/client/status"
+)
+
+func (s *serviceClient) createAndOpenDebugBundle() error {
+ conn, err := s.getSrvClient(failFastTimeout)
+ if err != nil {
+ return fmt.Errorf("get client: %v", err)
+ }
+
+ statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true})
+ if err != nil {
+ return fmt.Errorf("failed to get status: %v", err)
+ }
+
+ overview := nbstatus.ConvertToStatusOutputOverview(statusResp, true, "", nil, nil, nil)
+ statusOutput := nbstatus.ParseToFullDetailSummary(overview)
+
+ resp, err := conn.DebugBundle(s.ctx, &proto.DebugBundleRequest{
+ Anonymize: true,
+ Status: statusOutput,
+ SystemInfo: true,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to create debug bundle: %v", err)
+ }
+
+ bundleDir := filepath.Dir(resp.GetPath())
+ if err := open.Start(bundleDir); err != nil {
+ return fmt.Errorf("failed to open debug bundle directory: %v", err)
+ }
+
+ s.app.SendNotification(fyne.NewNotification(
+ "Debug Bundle",
+ fmt.Sprintf("Debug bundle created at %s. Administrator privileges are required to access it.", resp.GetPath()),
+ ))
+
+ return nil
+}
diff --git a/client/ui/desktop/desktop.go b/client/ui/desktop/desktop.go
new file mode 100644
index 000000000..0c99e2f38
--- /dev/null
+++ b/client/ui/desktop/desktop.go
@@ -0,0 +1,8 @@
+package desktop
+
+import "github.com/netbirdio/netbird/version"
+
+// GetUIUserAgent returns the Desktop ui user agent
+func GetUIUserAgent() string {
+ return "netbird-desktop-ui/" + version.NetbirdVersion()
+}
diff --git a/client/ui/event/event.go b/client/ui/event/event.go
new file mode 100644
index 000000000..4d949416d
--- /dev/null
+++ b/client/ui/event/event.go
@@ -0,0 +1,176 @@
+package event
+
+import (
+ "context"
+ "fmt"
+ "slices"
+ "strings"
+ "sync"
+ "time"
+
+ "fyne.io/fyne/v2"
+ "github.com/cenkalti/backoff/v4"
+ log "github.com/sirupsen/logrus"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "github.com/netbirdio/netbird/client/proto"
+ "github.com/netbirdio/netbird/client/ui/desktop"
+)
+
+type Handler func(*proto.SystemEvent)
+
+type Manager struct {
+ app fyne.App
+ addr string
+
+ mu sync.Mutex
+ ctx context.Context
+ cancel context.CancelFunc
+ enabled bool
+ handlers []Handler
+}
+
+func NewManager(app fyne.App, addr string) *Manager {
+ return &Manager{
+ app: app,
+ addr: addr,
+ }
+}
+
+func (e *Manager) Start(ctx context.Context) {
+ e.mu.Lock()
+ e.ctx, e.cancel = context.WithCancel(ctx)
+ e.mu.Unlock()
+
+ expBackOff := backoff.WithContext(&backoff.ExponentialBackOff{
+ InitialInterval: time.Second,
+ RandomizationFactor: backoff.DefaultRandomizationFactor,
+ Multiplier: backoff.DefaultMultiplier,
+ MaxInterval: 10 * time.Second,
+ MaxElapsedTime: 0,
+ Stop: backoff.Stop,
+ Clock: backoff.SystemClock,
+ }, ctx)
+
+ if err := backoff.Retry(e.streamEvents, expBackOff); err != nil {
+ log.Errorf("event stream ended: %v", err)
+ }
+}
+
+func (e *Manager) streamEvents() error {
+ e.mu.Lock()
+ ctx := e.ctx
+ e.mu.Unlock()
+
+ client, err := getClient(e.addr)
+ if err != nil {
+ return fmt.Errorf("create client: %w", err)
+ }
+
+ stream, err := client.SubscribeEvents(ctx, &proto.SubscribeRequest{})
+ if err != nil {
+ return fmt.Errorf("failed to subscribe to events: %w", err)
+ }
+
+ log.Info("subscribed to daemon events")
+ defer func() {
+ log.Info("unsubscribed from daemon events")
+ }()
+
+ for {
+ event, err := stream.Recv()
+ if err != nil {
+ return fmt.Errorf("error receiving event: %w", err)
+ }
+ e.handleEvent(event)
+ }
+}
+
+func (e *Manager) Stop() {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ if e.cancel != nil {
+ e.cancel()
+ }
+}
+
+func (e *Manager) SetNotificationsEnabled(enabled bool) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ e.enabled = enabled
+}
+
+func (e *Manager) handleEvent(event *proto.SystemEvent) {
+ e.mu.Lock()
+ enabled := e.enabled
+ handlers := slices.Clone(e.handlers)
+ e.mu.Unlock()
+
+ // critical events are always shown
+ if !enabled && event.Severity != proto.SystemEvent_CRITICAL {
+ return
+ }
+
+ if event.UserMessage != "" {
+ title := e.getEventTitle(event)
+ body := event.UserMessage
+ id := event.Metadata["id"]
+ if id != "" {
+ body += fmt.Sprintf(" ID: %s", id)
+ }
+ e.app.SendNotification(fyne.NewNotification(title, body))
+ }
+
+ for _, handler := range handlers {
+ go handler(event)
+ }
+}
+
+func (e *Manager) AddHandler(handler Handler) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ e.handlers = append(e.handlers, handler)
+}
+
+func (e *Manager) getEventTitle(event *proto.SystemEvent) string {
+ var prefix string
+ switch event.Severity {
+ case proto.SystemEvent_CRITICAL:
+ prefix = "Critical"
+ case proto.SystemEvent_ERROR:
+ prefix = "Error"
+ case proto.SystemEvent_WARNING:
+ prefix = "Warning"
+ default:
+ prefix = "Info"
+ }
+
+ var category string
+ switch event.Category {
+ case proto.SystemEvent_DNS:
+ category = "DNS"
+ case proto.SystemEvent_NETWORK:
+ category = "Network"
+ case proto.SystemEvent_AUTHENTICATION:
+ category = "Authentication"
+ case proto.SystemEvent_CONNECTIVITY:
+ category = "Connectivity"
+ default:
+ category = "System"
+ }
+
+ return fmt.Sprintf("%s: %s", prefix, category)
+}
+
+func getClient(addr string) (proto.DaemonServiceClient, error) {
+ conn, err := grpc.NewClient(
+ strings.TrimPrefix(addr, "tcp://"),
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ grpc.WithUserAgent(desktop.GetUIUserAgent()),
+ )
+ if err != nil {
+ return nil, err
+ }
+ return proto.NewDaemonServiceClient(conn), nil
+}
diff --git a/client/ui/font_bsd.go b/client/ui/font_bsd.go
index 84cb5993d..139f38f40 100644
--- a/client/ui/font_bsd.go
+++ b/client/ui/font_bsd.go
@@ -1,4 +1,4 @@
-//go:build darwin
+//go:build freebsd || openbsd || netbsd || dragonfly
package main
@@ -9,18 +9,22 @@ import (
log "github.com/sirupsen/logrus"
)
-const defaultFontPath = "/Library/Fonts/Arial Unicode.ttf"
-
func (s *serviceClient) setDefaultFonts() {
- // TODO: add other bsd paths
- if runtime.GOOS != "darwin" {
- return
+ paths := []string{
+ "/usr/local/share/fonts/TTF/DejaVuSans.ttf",
+ "/usr/local/share/fonts/dejavu/DejaVuSans.ttf",
+ "/usr/local/share/noto/NotoSans-Regular.ttf",
+ "/usr/local/share/fonts/noto/NotoSans-Regular.ttf",
+ "/usr/local/share/fonts/liberation-fonts-ttf/LiberationSans-Regular.ttf",
}
- if _, err := os.Stat(defaultFontPath); err != nil {
- log.Errorf("Failed to find default font file: %v", err)
- return
+ for _, fontPath := range paths {
+ if _, err := os.Stat(fontPath); err == nil {
+ os.Setenv("FYNE_FONT", fontPath)
+ log.Debugf("Using font: %s", fontPath)
+ return
+ }
}
- os.Setenv("FYNE_FONT", defaultFontPath)
+ log.Errorf("Failed to find any suitable font files for %s", runtime.GOOS)
}
diff --git a/client/ui/font_darwin.go b/client/ui/font_darwin.go
new file mode 100644
index 000000000..cafb72f59
--- /dev/null
+++ b/client/ui/font_darwin.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+ "os"
+
+ log "github.com/sirupsen/logrus"
+)
+
+const defaultFontPath = "/Library/Fonts/Arial Unicode.ttf"
+
+func (s *serviceClient) setDefaultFonts() {
+ if _, err := os.Stat(defaultFontPath); err != nil {
+ log.Errorf("Failed to find default font file: %v", err)
+ return
+ }
+
+ os.Setenv("FYNE_FONT", defaultFontPath)
+}
diff --git a/client/ui/icons.go b/client/ui/icons.go
new file mode 100644
index 000000000..6f3a9dbc9
--- /dev/null
+++ b/client/ui/icons.go
@@ -0,0 +1,43 @@
+//go:build !(linux && 386) && !windows
+
+package main
+
+import (
+ _ "embed"
+)
+
+//go:embed netbird.png
+var iconAbout []byte
+
+//go:embed netbird-systemtray-connected.png
+var iconConnected []byte
+
+//go:embed netbird-systemtray-connected-dark.png
+var iconConnectedDark []byte
+
+//go:embed netbird-systemtray-disconnected.png
+var iconDisconnected []byte
+
+//go:embed netbird-systemtray-update-disconnected.png
+var iconUpdateDisconnected []byte
+
+//go:embed netbird-systemtray-update-disconnected-dark.png
+var iconUpdateDisconnectedDark []byte
+
+//go:embed netbird-systemtray-update-connected.png
+var iconUpdateConnected []byte
+
+//go:embed netbird-systemtray-update-connected-dark.png
+var iconUpdateConnectedDark []byte
+
+//go:embed netbird-systemtray-connecting.png
+var iconConnecting []byte
+
+//go:embed netbird-systemtray-connecting-dark.png
+var iconConnectingDark []byte
+
+//go:embed netbird-systemtray-error.png
+var iconError []byte
+
+//go:embed netbird-systemtray-error-dark.png
+var iconErrorDark []byte
diff --git a/client/ui/icons_windows.go b/client/ui/icons_windows.go
new file mode 100644
index 000000000..a2a924763
--- /dev/null
+++ b/client/ui/icons_windows.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+ _ "embed"
+)
+
+//go:embed netbird.ico
+var iconAbout []byte
+
+//go:embed netbird-systemtray-connected.ico
+var iconConnected []byte
+
+//go:embed netbird-systemtray-connected-dark.ico
+var iconConnectedDark []byte
+
+//go:embed netbird-systemtray-disconnected.ico
+var iconDisconnected []byte
+
+//go:embed netbird-systemtray-update-disconnected.ico
+var iconUpdateDisconnected []byte
+
+//go:embed netbird-systemtray-update-disconnected-dark.ico
+var iconUpdateDisconnectedDark []byte
+
+//go:embed netbird-systemtray-update-connected.ico
+var iconUpdateConnected []byte
+
+//go:embed netbird-systemtray-update-connected-dark.ico
+var iconUpdateConnectedDark []byte
+
+//go:embed netbird-systemtray-connecting.ico
+var iconConnecting []byte
+
+//go:embed netbird-systemtray-connecting-dark.ico
+var iconConnectingDark []byte
+
+//go:embed netbird-systemtray-error.ico
+var iconError []byte
+
+//go:embed netbird-systemtray-error-dark.ico
+var iconErrorDark []byte
diff --git a/client/ui/netbird-systemtray-connected-dark.ico b/client/ui/netbird-systemtray-connected-dark.ico
new file mode 100644
index 000000000..0db8a0862
Binary files /dev/null and b/client/ui/netbird-systemtray-connected-dark.ico differ
diff --git a/client/ui/netbird-systemtray-connected-dark.png b/client/ui/netbird-systemtray-connected-dark.png
new file mode 100644
index 000000000..f18a929a0
Binary files /dev/null and b/client/ui/netbird-systemtray-connected-dark.png differ
diff --git a/client/ui/netbird-systemtray-connected-macos.png b/client/ui/netbird-systemtray-connected-macos.png
new file mode 100644
index 000000000..ead210250
Binary files /dev/null and b/client/ui/netbird-systemtray-connected-macos.png differ
diff --git a/client/ui/netbird-systemtray-connected.ico b/client/ui/netbird-systemtray-connected.ico
index 80550aa37..c16bec3f5 100644
Binary files a/client/ui/netbird-systemtray-connected.ico and b/client/ui/netbird-systemtray-connected.ico differ
diff --git a/client/ui/netbird-systemtray-connected.png b/client/ui/netbird-systemtray-connected.png
index f4d156da8..4258a5c1c 100644
Binary files a/client/ui/netbird-systemtray-connected.png and b/client/ui/netbird-systemtray-connected.png differ
diff --git a/client/ui/netbird-systemtray-connecting-dark.ico b/client/ui/netbird-systemtray-connecting-dark.ico
new file mode 100644
index 000000000..615d40f07
Binary files /dev/null and b/client/ui/netbird-systemtray-connecting-dark.ico differ
diff --git a/client/ui/netbird-systemtray-connecting-dark.png b/client/ui/netbird-systemtray-connecting-dark.png
new file mode 100644
index 000000000..a665eb61c
Binary files /dev/null and b/client/ui/netbird-systemtray-connecting-dark.png differ
diff --git a/client/ui/netbird-systemtray-connecting-macos.png b/client/ui/netbird-systemtray-connecting-macos.png
new file mode 100644
index 000000000..0fe7fa0db
Binary files /dev/null and b/client/ui/netbird-systemtray-connecting-macos.png differ
diff --git a/client/ui/netbird-systemtray-connecting.ico b/client/ui/netbird-systemtray-connecting.ico
new file mode 100644
index 000000000..4e4c3a9b1
Binary files /dev/null and b/client/ui/netbird-systemtray-connecting.ico differ
diff --git a/client/ui/netbird-systemtray-connecting.png b/client/ui/netbird-systemtray-connecting.png
new file mode 100644
index 000000000..4f607c997
Binary files /dev/null and b/client/ui/netbird-systemtray-connecting.png differ
diff --git a/client/ui/netbird-systemtray-disconnected-macos.png b/client/ui/netbird-systemtray-disconnected-macos.png
new file mode 100644
index 000000000..36b9a488f
Binary files /dev/null and b/client/ui/netbird-systemtray-disconnected-macos.png differ
diff --git a/client/ui/netbird-systemtray-disconnected.ico b/client/ui/netbird-systemtray-disconnected.ico
index aa75268b0..dcb9f4bf8 100644
Binary files a/client/ui/netbird-systemtray-disconnected.ico and b/client/ui/netbird-systemtray-disconnected.ico differ
diff --git a/client/ui/netbird-systemtray-disconnected.png b/client/ui/netbird-systemtray-disconnected.png
index 3aae73231..a92e9ed4c 100644
Binary files a/client/ui/netbird-systemtray-disconnected.png and b/client/ui/netbird-systemtray-disconnected.png differ
diff --git a/client/ui/netbird-systemtray-error-dark.ico b/client/ui/netbird-systemtray-error-dark.ico
new file mode 100644
index 000000000..083816188
Binary files /dev/null and b/client/ui/netbird-systemtray-error-dark.ico differ
diff --git a/client/ui/netbird-systemtray-error-dark.png b/client/ui/netbird-systemtray-error-dark.png
new file mode 100644
index 000000000..969554b16
Binary files /dev/null and b/client/ui/netbird-systemtray-error-dark.png differ
diff --git a/client/ui/netbird-systemtray-error-macos.png b/client/ui/netbird-systemtray-error-macos.png
new file mode 100644
index 000000000..9a9998bcf
Binary files /dev/null and b/client/ui/netbird-systemtray-error-macos.png differ
diff --git a/client/ui/netbird-systemtray-error.ico b/client/ui/netbird-systemtray-error.ico
new file mode 100644
index 000000000..1abc45c2a
Binary files /dev/null and b/client/ui/netbird-systemtray-error.ico differ
diff --git a/client/ui/netbird-systemtray-error.png b/client/ui/netbird-systemtray-error.png
new file mode 100644
index 000000000..722342989
Binary files /dev/null and b/client/ui/netbird-systemtray-error.png differ
diff --git a/client/ui/netbird-systemtray-update-cloud.ico b/client/ui/netbird-systemtray-update-cloud.ico
deleted file mode 100644
index b87c6f4b5..000000000
Binary files a/client/ui/netbird-systemtray-update-cloud.ico and /dev/null differ
diff --git a/client/ui/netbird-systemtray-update-cloud.png b/client/ui/netbird-systemtray-update-cloud.png
deleted file mode 100644
index e9d0b8035..000000000
Binary files a/client/ui/netbird-systemtray-update-cloud.png and /dev/null differ
diff --git a/client/ui/netbird-systemtray-update-connected-dark.ico b/client/ui/netbird-systemtray-update-connected-dark.ico
new file mode 100644
index 000000000..b11bb5492
Binary files /dev/null and b/client/ui/netbird-systemtray-update-connected-dark.ico differ
diff --git a/client/ui/netbird-systemtray-update-connected-dark.png b/client/ui/netbird-systemtray-update-connected-dark.png
new file mode 100644
index 000000000..52ae621ac
Binary files /dev/null and b/client/ui/netbird-systemtray-update-connected-dark.png differ
diff --git a/client/ui/netbird-systemtray-update-connected-macos.png b/client/ui/netbird-systemtray-update-connected-macos.png
new file mode 100644
index 000000000..8a6b2f2db
Binary files /dev/null and b/client/ui/netbird-systemtray-update-connected-macos.png differ
diff --git a/client/ui/netbird-systemtray-update-connected.ico b/client/ui/netbird-systemtray-update-connected.ico
index cc056e68e..d3ce2f0f3 100644
Binary files a/client/ui/netbird-systemtray-update-connected.ico and b/client/ui/netbird-systemtray-update-connected.ico differ
diff --git a/client/ui/netbird-systemtray-update-connected.png b/client/ui/netbird-systemtray-update-connected.png
index a0c453340..90bb0b7f1 100644
Binary files a/client/ui/netbird-systemtray-update-connected.png and b/client/ui/netbird-systemtray-update-connected.png differ
diff --git a/client/ui/netbird-systemtray-update-disconnected-dark.ico b/client/ui/netbird-systemtray-update-disconnected-dark.ico
new file mode 100644
index 000000000..123237f66
Binary files /dev/null and b/client/ui/netbird-systemtray-update-disconnected-dark.ico differ
diff --git a/client/ui/netbird-systemtray-update-disconnected-dark.png b/client/ui/netbird-systemtray-update-disconnected-dark.png
new file mode 100644
index 000000000..9e05351f1
Binary files /dev/null and b/client/ui/netbird-systemtray-update-disconnected-dark.png differ
diff --git a/client/ui/netbird-systemtray-update-disconnected-macos.png b/client/ui/netbird-systemtray-update-disconnected-macos.png
new file mode 100644
index 000000000..8b190034e
Binary files /dev/null and b/client/ui/netbird-systemtray-update-disconnected-macos.png differ
diff --git a/client/ui/netbird-systemtray-update-disconnected.ico b/client/ui/netbird-systemtray-update-disconnected.ico
index 04c35b058..968dc4105 100644
Binary files a/client/ui/netbird-systemtray-update-disconnected.ico and b/client/ui/netbird-systemtray-update-disconnected.ico differ
diff --git a/client/ui/netbird-systemtray-update-disconnected.png b/client/ui/netbird-systemtray-update-disconnected.png
index 3fbe88953..3adc39034 100644
Binary files a/client/ui/netbird-systemtray-update-disconnected.png and b/client/ui/netbird-systemtray-update-disconnected.png differ
diff --git a/client/ui/netbird.png b/client/ui/netbird.png
new file mode 100644
index 000000000..a92e9ed4c
Binary files /dev/null and b/client/ui/netbird.png differ
diff --git a/client/ui/network.go b/client/ui/network.go
index e6f027f0e..750788cf3 100644
--- a/client/ui/network.go
+++ b/client/ui/network.go
@@ -1,9 +1,11 @@
-//go:build !(linux && 386) && !freebsd
+//go:build !(linux && 386)
package main
import (
+ "context"
"fmt"
+ "runtime"
"sort"
"strings"
"time"
@@ -13,6 +15,7 @@ import (
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
+ "fyne.io/systray"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/proto"
@@ -237,14 +240,14 @@ func (s *serviceClient) selectNetwork(id string, checked bool) {
s.showError(fmt.Errorf("failed to select network: %v", err))
return
}
- log.Infof("Route %s selected", id)
+ log.Infof("Network '%s' selected", id)
} else {
if _, err := conn.DeselectNetworks(s.ctx, req); err != nil {
log.Errorf("failed to deselect network: %v", err)
s.showError(fmt.Errorf("failed to deselect network: %v", err))
return
}
- log.Infof("Network %s deselected", id)
+ log.Infof("Network '%s' deselected", id)
}
}
@@ -324,6 +327,201 @@ func (s *serviceClient) updateNetworksBasedOnDisplayTab(tabs *container.AppTabs,
s.updateNetworks(grid, f)
}
+func (s *serviceClient) updateExitNodes() {
+ conn, err := s.getSrvClient(defaultFailTimeout)
+ if err != nil {
+ log.Errorf("get client: %v", err)
+ return
+ }
+
+ exitNodes, err := s.getExitNodes(conn)
+ if err != nil {
+ log.Errorf("get exit nodes: %v", err)
+ return
+ }
+
+ s.exitNodeMu.Lock()
+ defer s.exitNodeMu.Unlock()
+
+ s.recreateExitNodeMenu(exitNodes)
+
+ if len(s.mExitNodeItems) > 0 {
+ s.mExitNode.Enable()
+ } else {
+ s.mExitNode.Disable()
+ }
+
+ log.Debugf("Exit nodes updated: %d", len(s.mExitNodeItems))
+}
+
+func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) {
+ for _, node := range s.mExitNodeItems {
+ node.cancel()
+ node.Remove()
+ }
+ s.mExitNodeItems = nil
+
+ if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" {
+ s.mExitNode.Remove()
+ s.mExitNode = systray.AddMenuItem("Exit Node", "Select exit node for routing traffic")
+ }
+
+ for _, node := range exitNodes {
+ menuItem := s.mExitNode.AddSubMenuItemCheckbox(
+ node.ID,
+ fmt.Sprintf("Use exit node %s", node.ID),
+ node.Selected,
+ )
+
+ ctx, cancel := context.WithCancel(context.Background())
+ s.mExitNodeItems = append(s.mExitNodeItems, menuHandler{
+ MenuItem: menuItem,
+ cancel: cancel,
+ })
+ go s.handleChecked(ctx, node.ID, menuItem)
+ }
+
+}
+
+func (s *serviceClient) getExitNodes(conn proto.DaemonServiceClient) ([]*proto.Network, error) {
+ ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout)
+ defer cancel()
+
+ resp, err := conn.ListNetworks(ctx, &proto.ListNetworksRequest{})
+ if err != nil {
+ return nil, fmt.Errorf("list networks: %v", err)
+ }
+
+ var exitNodes []*proto.Network
+ for _, network := range resp.Routes {
+ if network.Range == "0.0.0.0/0" {
+ exitNodes = append(exitNodes, network)
+ }
+ }
+ return exitNodes, nil
+}
+
+func (s *serviceClient) handleChecked(ctx context.Context, id string, item *systray.MenuItem) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case _, ok := <-item.ClickedCh:
+ if !ok {
+ return
+ }
+ if err := s.toggleExitNode(id, item); err != nil {
+ log.Errorf("failed to toggle exit node: %v", err)
+ continue
+ }
+ }
+ }
+}
+
+// Add function to toggle exit node selection
+func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) error {
+ conn, err := s.getSrvClient(defaultFailTimeout)
+ if err != nil {
+ return fmt.Errorf("get client: %v", err)
+ }
+
+ log.Infof("Toggling exit node '%s'", nodeID)
+
+ s.exitNodeMu.Lock()
+ defer s.exitNodeMu.Unlock()
+
+ exitNodes, err := s.getExitNodes(conn)
+ if err != nil {
+ return fmt.Errorf("get exit nodes: %v", err)
+ }
+
+ var exitNode *proto.Network
+ // find other selected nodes and ours
+ ids := make([]string, 0, len(exitNodes))
+ for _, node := range exitNodes {
+ if node.ID == nodeID {
+ // preserve original state
+ cp := *node //nolint:govet
+ exitNode = &cp
+
+ // set desired state for recreation
+ node.Selected = true
+ continue
+ }
+ if node.Selected {
+ ids = append(ids, node.ID)
+
+ // set desired state for recreation
+ node.Selected = false
+ }
+ }
+
+ if item.Checked() && len(ids) == 0 {
+ // exit node is the only selected node, deselect it
+ ids = append(ids, nodeID)
+ exitNode = nil
+ }
+
+ // deselect all other selected exit nodes
+ if err := s.deselectOtherExitNodes(conn, ids, item); err != nil {
+ return err
+ }
+
+ if err := s.selectNewExitNode(conn, exitNode, nodeID, item); err != nil {
+ return err
+ }
+
+ // linux/bsd doesn't handle Check/Uncheck well, so we recreate the menu
+ if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" {
+ s.recreateExitNodeMenu(exitNodes)
+ }
+
+ return nil
+}
+
+func (s *serviceClient) deselectOtherExitNodes(conn proto.DaemonServiceClient, ids []string, currentItem *systray.MenuItem) error {
+ // deselect all other selected exit nodes
+ if len(ids) > 0 {
+ deselectReq := &proto.SelectNetworksRequest{
+ NetworkIDs: ids,
+ }
+ if _, err := conn.DeselectNetworks(s.ctx, deselectReq); err != nil {
+ return fmt.Errorf("deselect networks: %v", err)
+ }
+
+ log.Infof("Deselected exit nodes: %v", ids)
+ }
+
+ // uncheck all other exit node menu items
+ for _, i := range s.mExitNodeItems {
+ if i.MenuItem == currentItem {
+ continue
+ }
+ i.Uncheck()
+ log.Infof("Unchecked exit node %v", i)
+ }
+
+ return nil
+}
+
+func (s *serviceClient) selectNewExitNode(conn proto.DaemonServiceClient, exitNode *proto.Network, nodeID string, item *systray.MenuItem) error {
+ if exitNode != nil && !exitNode.Selected {
+ selectReq := &proto.SelectNetworksRequest{
+ NetworkIDs: []string{exitNode.ID},
+ Append: true,
+ }
+ if _, err := conn.SelectNetworks(s.ctx, selectReq); err != nil {
+ return fmt.Errorf("select network: %v", err)
+ }
+
+ log.Infof("Selected exit node '%s'", nodeID)
+ }
+
+ item.Check()
+
+ return nil
+}
+
func getGridAndFilterFromTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) (*fyne.Container, filter) {
switch tabs.Selected().Text {
case overlappingNetworksText:
diff --git a/go.mod b/go.mod
index 74b160b50..3d71e8eb1 100644
--- a/go.mod
+++ b/go.mod
@@ -19,13 +19,13 @@ require (
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/vishvananda/netlink v1.2.1-beta.2
- golang.org/x/crypto v0.31.0
- golang.org/x/sys v0.28.0
+ golang.org/x/crypto v0.32.0
+ golang.org/x/sys v0.29.0
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
golang.zx2c4.com/wireguard/windows v0.5.3
- google.golang.org/grpc v1.64.1
- google.golang.org/protobuf v1.34.2
+ google.golang.org/grpc v1.70.0
+ google.golang.org/protobuf v1.36.4
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
@@ -36,12 +36,13 @@ require (
github.com/c-robinson/iplib v1.0.3
github.com/caddyserver/certmagic v0.21.3
github.com/cilium/ebpf v0.15.0
+ github.com/coder/websocket v1.8.12
github.com/coreos/go-iptables v0.7.0
github.com/creack/pty v1.1.18
github.com/davecgh/go-spew v1.1.1
github.com/eko/gocache/v3 v3.1.1
github.com/fsnotify/fsnotify v1.7.0
- github.com/gliderlabs/ssh v0.3.4
+ github.com/gliderlabs/ssh v0.3.8
github.com/godbus/dbus/v5 v5.1.0
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.6.0
@@ -54,13 +55,12 @@ require (
github.com/hashicorp/go-version v1.6.0
github.com/libdns/route53 v1.5.0
github.com/libp2p/go-netroute v0.2.1
- github.com/magiconair/properties v1.8.7
- github.com/mattn/go-sqlite3 v1.14.19
+ github.com/mattn/go-sqlite3 v1.14.22
github.com/mdlayher/socket v0.5.1
github.com/miekg/dns v1.1.59
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/nadoo/ipset v0.5.0
- github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6
+ github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d
github.com/okta/okta-sdk-golang/v2 v2.18.0
github.com/oschwald/maxminddb-golang v1.12.0
@@ -71,43 +71,44 @@ require (
github.com/pion/transport/v3 v3.0.1
github.com/pion/turn/v3 v3.0.1
github.com/prometheus/client_golang v1.19.1
+ github.com/quic-go/quic-go v0.48.2
github.com/rs/xid v1.3.0
github.com/shirou/gopsutil/v3 v3.24.4
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
- github.com/stretchr/testify v1.9.0
+ github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.31.0
github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0
github.com/things-go/go-socks5 v0.0.4
github.com/yusufpapurcu/wmi v1.2.4
github.com/zcalusic/sysinfo v1.1.3
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0
- go.opentelemetry.io/otel v1.26.0
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0
+ go.opentelemetry.io/otel v1.34.0
go.opentelemetry.io/otel/exporters/prometheus v0.48.0
- go.opentelemetry.io/otel/metric v1.26.0
- go.opentelemetry.io/otel/sdk/metric v1.26.0
+ go.opentelemetry.io/otel/metric v1.34.0
+ go.opentelemetry.io/otel/sdk/metric v1.32.0
go.uber.org/zap v1.27.0
goauthentik.io/api/v3 v3.2023051.3
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a
- golang.org/x/net v0.30.0
- golang.org/x/oauth2 v0.19.0
+ golang.org/x/net v0.34.0
+ golang.org/x/oauth2 v0.26.0
golang.org/x/sync v0.10.0
- golang.org/x/term v0.27.0
- google.golang.org/api v0.177.0
+ golang.org/x/term v0.28.0
+ google.golang.org/api v0.220.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.7
- gorm.io/driver/sqlite v1.5.3
- gorm.io/gorm v1.25.7
- nhooyr.io/websocket v1.8.11
+ gorm.io/driver/sqlite v1.5.7
+ gorm.io/gorm v1.25.12
+ gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1
)
require (
- cloud.google.com/go/auth v0.3.0 // indirect
- cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
- cloud.google.com/go/compute/metadata v0.3.0 // indirect
+ cloud.google.com/go/auth v0.14.1 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
+ cloud.google.com/go/compute/metadata v0.6.0 // indirect
dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
@@ -150,19 +151,20 @@ require (
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
- github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
+ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
- github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.2 // indirect
- github.com/google/s2a-go v0.1.7 // indirect
- github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
- github.com/googleapis/gax-go/v2 v2.12.3 // indirect
+ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
+ github.com/google/s2a-go v0.1.9 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
+ github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
@@ -182,6 +184,7 @@ require (
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mholt/acmez/v2 v2.0.1 // indirect
@@ -217,23 +220,22 @@ require (
github.com/vishvananda/netns v0.0.4 // indirect
github.com/yuin/goldmark v1.7.1 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
- go.opencensus.io v0.24.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
- go.opentelemetry.io/otel/sdk v1.26.0 // indirect
- go.opentelemetry.io/otel/trace v1.26.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.34.0 // indirect
+ go.opentelemetry.io/otel/trace v1.34.0 // indirect
+ go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/text v0.21.0 // indirect
- golang.org/x/time v0.5.0 // indirect
+ golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
- gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 // indirect
k8s.io/apimachinery v0.26.2 // indirect
)
@@ -241,7 +243,7 @@ replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-2024
replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949
-replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20241125150134-f9cdce5e32e9
+replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6
replace github.com/cloudflare/circl => github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6
diff --git a/go.sum b/go.sum
index f6d4590ee..36bca22d3 100644
--- a/go.sum
+++ b/go.sum
@@ -18,10 +18,10 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs=
-cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w=
-cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
-cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
+cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0=
+cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM=
+cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
+cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -29,8 +29,8 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
-cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
-cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
@@ -137,6 +137,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
+github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/containerd/containerd v1.7.16 h1:7Zsfe8Fkj4Wi2My6DXGQ87hiqIrmOXolm72ZEkFU5Mg=
github.com/containerd/containerd v1.7.16/go.mod h1:NL49g7A/Fui7ccmxV6zkBWwqMgmMxFWzujYCc+JLt7k=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
@@ -212,8 +214,8 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
-github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw=
-github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
+github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
+github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -223,8 +225,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -261,14 +263,12 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
-github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM=
+github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -343,18 +343,18 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
-github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
-github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
-github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
+github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
+github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gopacket/gopacket v1.1.1 h1:zbx9F9d6A7sWNkFKrvMBZTfGgxFoY4NgUudFVVHMfcw=
github.com/gopacket/gopacket v1.1.1/go.mod h1:HavMeONEl7W9036of9LbSWoonqhH7HA1+ZRO+rMIvFs=
@@ -405,6 +405,7 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@@ -474,8 +475,8 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
-github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
-github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
@@ -526,14 +527,14 @@ 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/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c=
github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q=
-github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6 h1:I/ODkZ8rSDOzlJbhEjD2luSI71zl+s5JgNvFHY0+mBU=
-github.com/netbirdio/management-integrations/integrations v0.0.0-20250115083837-a09722b8d2a6/go.mod h1:izUUs1NT7ja+PwSX3kJ7ox8Kkn478tboBJSjL4kU6J0=
+github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc h1:18xvjOy2tZVIK7rihNpf9DF/3mAiljYKWaQlWa9vJgI=
+github.com/netbirdio/management-integrations/integrations v0.0.0-20250220173202-e599d83524fc/go.mod h1:izUUs1NT7ja+PwSX3kJ7ox8Kkn478tboBJSjL4kU6J0=
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/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d h1:bRq5TKgC7Iq20pDiuC54yXaWnAVeS5PdGpSokFTlR28=
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
-github.com/netbirdio/wireguard-go v0.0.0-20241125150134-f9cdce5e32e9 h1:Pu/7EukijT09ynHUOzQYW7cC3M/BKU8O4qyN/TvTGoY=
-github.com/netbirdio/wireguard-go v0.0.0-20241125150134-f9cdce5e32e9/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
+github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6 h1:X5h5QgP7uHAv78FWgHV8+WYLjHxK9v3ilkVXT1cpCrQ=
+github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -610,10 +611,12 @@ github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+a
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
+github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
+github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
-github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
@@ -678,11 +681,11 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U=
github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI=
@@ -734,33 +737,35 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
-go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
-go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
-go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/exporters/prometheus v0.48.0 h1:sBQe3VNGUjY9IKWQC6z2lNqa5iGbDSxhs60ABwK4y0s=
go.opentelemetry.io/otel/exporters/prometheus v0.48.0/go.mod h1:DtrbMzoZWwQHyrQmCfLam5DZbnmorsGbOtTbYHycU5o=
-go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
-go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
-go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8=
-go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs=
-go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y=
-go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE=
-go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
-go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
+go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
+go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
@@ -776,14 +781,13 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -879,8 +883,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
-golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -894,8 +898,8 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
-golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
-golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
+golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -971,6 +975,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -982,8 +987,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -991,8 +996,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1012,8 +1017,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
+golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1107,8 +1112,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
-google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk=
-google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw=
+google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns=
+google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1157,10 +1162,11 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 h1:OpXbo8JnN8+jZGPrL4SSfaDjSCjupr8lXyBAbexEm/U=
-google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
+google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1181,8 +1187,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
-google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
+google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
+google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1197,8 +1203,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
+google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1236,10 +1242,11 @@ gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
-gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g=
-gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
-gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
+gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
+gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
+gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs=
@@ -1258,8 +1265,6 @@ k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
-nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=
-nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/infrastructure_files/docker-compose.yml.tmpl.traefik b/infrastructure_files/docker-compose.yml.tmpl.traefik
index 71471c3ef..dcd3f955c 100644
--- a/infrastructure_files/docker-compose.yml.tmpl.traefik
+++ b/infrastructure_files/docker-compose.yml.tmpl.traefik
@@ -67,6 +67,10 @@ services:
options:
max-size: "500m"
max-file: "2"
+ labels:
+ - traefik.enable=true
+ - traefik.http.routers.netbird-relay.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/relay`)
+ - traefik.http.services.netbird-relay.loadbalancer.server.port=$NETBIRD_RELAY_PORT
# Management
management:
diff --git a/management/README.md b/management/README.md
index f0eb0cb70..1122a9e76 100644
--- a/management/README.md
+++ b/management/README.md
@@ -111,4 +111,3 @@ Generate gRpc code:
#!/bin/bash
protoc -I proto/ proto/management.proto --go_out=. --go-grpc_out=.
```
-
diff --git a/management/client/client.go b/management/client/client.go
index e79884292..950f6137e 100644
--- a/management/client/client.go
+++ b/management/client/client.go
@@ -7,6 +7,7 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/system"
+ "github.com/netbirdio/netbird/management/domain"
"github.com/netbirdio/netbird/management/proto"
)
@@ -14,8 +15,8 @@ type Client interface {
io.Closer
Sync(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error
GetServerPublicKey() (*wgtypes.Key, error)
- Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
- Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
+ Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
+ Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
GetNetworkMap(sysInfo *system.Info) (*proto.NetworkMap, error)
diff --git a/management/client/client_test.go b/management/client/client_test.go
index 8bd8af8d2..21f6b79ad 100644
--- a/management/client/client_test.go
+++ b/management/client/client_test.go
@@ -78,7 +78,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
}
secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
- mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
+ mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
@@ -177,7 +177,7 @@ func TestClient_LoginUnregistered_ShouldThrow_401(t *testing.T) {
t.Fatal(err)
}
sysInfo := system.GetInfo(context.TODO())
- _, err = client.Login(*key, sysInfo, nil)
+ _, err = client.Login(*key, sysInfo, nil, nil)
if err == nil {
t.Error("expecting err on unregistered login, got nil")
}
@@ -205,7 +205,7 @@ func TestClient_LoginRegistered(t *testing.T) {
t.Error(err)
}
info := system.GetInfo(context.TODO())
- resp, err := client.Register(*key, ValidKey, "", info, nil)
+ resp, err := client.Register(*key, ValidKey, "", info, nil, nil)
if err != nil {
t.Error(err)
}
@@ -235,7 +235,7 @@ func TestClient_Sync(t *testing.T) {
}
info := system.GetInfo(context.TODO())
- _, err = client.Register(*serverKey, ValidKey, "", info, nil)
+ _, err = client.Register(*serverKey, ValidKey, "", info, nil, nil)
if err != nil {
t.Error(err)
}
@@ -251,15 +251,18 @@ func TestClient_Sync(t *testing.T) {
}
info = system.GetInfo(context.TODO())
- _, err = remoteClient.Register(*serverKey, ValidKey, "", info, nil)
+ _, err = remoteClient.Register(*serverKey, ValidKey, "", info, nil, nil)
if err != nil {
t.Fatal(err)
}
ch := make(chan *mgmtProto.SyncResponse, 1)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
go func() {
- err = client.Sync(context.Background(), info, func(msg *mgmtProto.SyncResponse) error {
+ err = client.Sync(ctx, info, func(msg *mgmtProto.SyncResponse) error {
ch <- msg
return nil
})
@@ -273,8 +276,8 @@ func TestClient_Sync(t *testing.T) {
if resp.GetPeerConfig() == nil {
t.Error("expecting non nil PeerConfig got nil")
}
- if resp.GetWiretrusteeConfig() == nil {
- t.Error("expecting non nil WiretrusteeConfig got nil")
+ if resp.GetNetbirdConfig() == nil {
+ t.Error("expecting non nil NetbirdConfig got nil")
}
if len(resp.GetRemotePeers()) != 1 {
t.Errorf("expecting RemotePeers size %d got %d", 1, len(resp.GetRemotePeers()))
@@ -349,7 +352,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) {
}
info := system.GetInfo(context.TODO())
- _, err = testClient.Register(*key, ValidKey, "", info, nil)
+ _, err = testClient.Register(*key, ValidKey, "", info, nil, nil)
if err != nil {
t.Errorf("error while trying to register client: %v", err)
}
@@ -366,15 +369,15 @@ func Test_SystemMetaDataFromClient(t *testing.T) {
}
expectedMeta := &mgmtProto.PeerSystemMeta{
- Hostname: info.Hostname,
- GoOS: info.GoOS,
- Kernel: info.Kernel,
- Platform: info.Platform,
- OS: info.OS,
- Core: info.OSVersion,
- OSVersion: info.OSVersion,
- WiretrusteeVersion: info.WiretrusteeVersion,
- KernelVersion: info.KernelVersion,
+ Hostname: info.Hostname,
+ GoOS: info.GoOS,
+ Kernel: info.Kernel,
+ Platform: info.Platform,
+ OS: info.OS,
+ Core: info.OSVersion,
+ OSVersion: info.OSVersion,
+ NetbirdVersion: info.NetbirdVersion,
+ KernelVersion: info.KernelVersion,
NetworkAddresses: protoNetAddr,
SysSerialNumber: info.SystemSerialNumber,
@@ -417,7 +420,7 @@ func isEqual(a, b *mgmtProto.PeerSystemMeta) bool {
a.GetPlatform() == b.GetPlatform() &&
a.GetOS() == b.GetOS() &&
a.GetOSVersion() == b.GetOSVersion() &&
- a.GetWiretrusteeVersion() == b.GetWiretrusteeVersion() &&
+ a.GetNetbirdVersion() == b.GetNetbirdVersion() &&
a.GetUiVersion() == b.GetUiVersion() &&
a.GetSysSerialNumber() == b.GetSysSerialNumber() &&
a.GetSysProductName() == b.GetSysProductName() &&
diff --git a/management/client/grpc.go b/management/client/grpc.go
index 74e808c32..d3aaffec0 100644
--- a/management/client/grpc.go
+++ b/management/client/grpc.go
@@ -19,6 +19,7 @@ import (
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/encryption"
+ "github.com/netbirdio/netbird/management/domain"
"github.com/netbirdio/netbird/management/proto"
nbgrpc "github.com/netbirdio/netbird/util/grpc"
)
@@ -364,21 +365,21 @@ func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*pro
// Register registers peer on Management Server. It actually calls a Login endpoint with a provided setup key
// Takes care of encrypting and decrypting messages.
// This method will also collect system info and send it with the request (e.g. hostname, os, etc)
-func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, pubSSHKey []byte) (*proto.LoginResponse, error) {
+func (c *GrpcClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, pubSSHKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) {
keys := &proto.PeerKeys{
SshPubKey: pubSSHKey,
WgPubKey: []byte(c.key.PublicKey().String()),
}
- return c.login(serverKey, &proto.LoginRequest{SetupKey: setupKey, Meta: infoToMetaData(sysInfo), JwtToken: jwtToken, PeerKeys: keys})
+ return c.login(serverKey, &proto.LoginRequest{SetupKey: setupKey, Meta: infoToMetaData(sysInfo), JwtToken: jwtToken, PeerKeys: keys, DnsLabels: dnsLabels.ToPunycodeList()})
}
// Login attempts login to Management Server. Takes care of encrypting and decrypting messages.
-func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info, pubSSHKey []byte) (*proto.LoginResponse, error) {
+func (c *GrpcClient) Login(serverKey wgtypes.Key, sysInfo *system.Info, pubSSHKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) {
keys := &proto.PeerKeys{
SshPubKey: pubSSHKey,
WgPubKey: []byte(c.key.PublicKey().String()),
}
- return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys})
+ return c.login(serverKey, &proto.LoginRequest{Meta: infoToMetaData(sysInfo), PeerKeys: keys, DnsLabels: dnsLabels.ToPunycodeList()})
}
// GetDeviceAuthorizationFlow returns a device authorization flow information.
@@ -521,24 +522,34 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
}
return &proto.PeerSystemMeta{
- Hostname: info.Hostname,
- GoOS: info.GoOS,
- OS: info.OS,
- Core: info.OSVersion,
- OSVersion: info.OSVersion,
- Platform: info.Platform,
- Kernel: info.Kernel,
- WiretrusteeVersion: info.WiretrusteeVersion,
- UiVersion: info.UIVersion,
- KernelVersion: info.KernelVersion,
- NetworkAddresses: addresses,
- SysSerialNumber: info.SystemSerialNumber,
- SysManufacturer: info.SystemManufacturer,
- SysProductName: info.SystemProductName,
+ Hostname: info.Hostname,
+ GoOS: info.GoOS,
+ OS: info.OS,
+ Core: info.OSVersion,
+ OSVersion: info.OSVersion,
+ Platform: info.Platform,
+ Kernel: info.Kernel,
+ NetbirdVersion: info.NetbirdVersion,
+ UiVersion: info.UIVersion,
+ KernelVersion: info.KernelVersion,
+ NetworkAddresses: addresses,
+ SysSerialNumber: info.SystemSerialNumber,
+ SysManufacturer: info.SystemManufacturer,
+ SysProductName: info.SystemProductName,
Environment: &proto.Environment{
Cloud: info.Environment.Cloud,
Platform: info.Environment.Platform,
},
Files: files,
+
+ Flags: &proto.Flags{
+ RosenpassEnabled: info.RosenpassEnabled,
+ RosenpassPermissive: info.RosenpassPermissive,
+ ServerSSHAllowed: info.ServerSSHAllowed,
+ DisableClientRoutes: info.DisableClientRoutes,
+ DisableServerRoutes: info.DisableServerRoutes,
+ DisableDNS: info.DisableDNS,
+ DisableFirewall: info.DisableFirewall,
+ },
}
}
diff --git a/management/client/mock.go b/management/client/mock.go
index 73a7ac38f..9e1786f82 100644
--- a/management/client/mock.go
+++ b/management/client/mock.go
@@ -6,6 +6,7 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/system"
+ "github.com/netbirdio/netbird/management/domain"
"github.com/netbirdio/netbird/management/proto"
)
@@ -13,8 +14,8 @@ type MockClient struct {
CloseFunc func() error
SyncFunc func(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error
GetServerPublicKeyFunc func() (*wgtypes.Key, error)
- RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
- LoginFunc func(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
+ RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
+ LoginFunc func(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
GetDeviceAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
GetPKCEAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
SyncMetaFunc func(sysInfo *system.Info) error
@@ -45,18 +46,18 @@ func (m *MockClient) GetServerPublicKey() (*wgtypes.Key, error) {
return m.GetServerPublicKeyFunc()
}
-func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) {
+func (m *MockClient) Register(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) {
if m.RegisterFunc == nil {
return nil, nil
}
- return m.RegisterFunc(serverKey, setupKey, jwtToken, info, sshKey)
+ return m.RegisterFunc(serverKey, setupKey, jwtToken, info, sshKey, dnsLabels)
}
-func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error) {
+func (m *MockClient) Login(serverKey wgtypes.Key, info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error) {
if m.LoginFunc == nil {
return nil, nil
}
- return m.LoginFunc(serverKey, info, sshKey)
+ return m.LoginFunc(serverKey, info, sshKey, dnsLabels)
}
func (m *MockClient) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error) {
diff --git a/management/client/rest/accounts.go b/management/client/rest/accounts.go
new file mode 100644
index 000000000..f38b19f70
--- /dev/null
+++ b/management/client/rest/accounts.go
@@ -0,0 +1,54 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// AccountsAPI APIs for accounts, do not use directly
+type AccountsAPI struct {
+ c *Client
+}
+
+// List list all accounts, only returns one account always
+// See more: https://docs.netbird.io/api/resources/accounts#list-all-accounts
+func (a *AccountsAPI) List(ctx context.Context) ([]api.Account, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/accounts", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.Account](resp)
+ return ret, err
+}
+
+// Update update account settings
+// See more: https://docs.netbird.io/api/resources/accounts#update-an-account
+func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api.PutApiAccountsAccountIdJSONRequestBody) (*api.Account, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Account](resp)
+ return &ret, err
+}
+
+// Delete delete account
+// See more: https://docs.netbird.io/api/resources/accounts#delete-an-account
+func (a *AccountsAPI) Delete(ctx context.Context, accountID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/management/client/rest/accounts_test.go b/management/client/rest/accounts_test.go
new file mode 100644
index 000000000..621228261
--- /dev/null
+++ b/management/client/rest/accounts_test.go
@@ -0,0 +1,174 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testAccount = api.Account{
+ Id: "Test",
+ Settings: api.AccountSettings{
+ Extra: &api.AccountExtraSettings{
+ PeerApprovalEnabled: ptr(false),
+ },
+ GroupsPropagationEnabled: ptr(true),
+ JwtGroupsEnabled: ptr(false),
+ PeerInactivityExpiration: 7,
+ PeerInactivityExpirationEnabled: true,
+ PeerLoginExpiration: 24,
+ PeerLoginExpirationEnabled: true,
+ RegularUsersViewBlocked: false,
+ RoutingPeerDnsResolutionEnabled: ptr(false),
+ },
+ }
+)
+
+func TestAccounts_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.Account{testAccount})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Accounts.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testAccount, ret[0])
+ })
+}
+
+func TestAccounts_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Accounts.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestAccounts_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiAccountsAccountIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, true, *req.Settings.RoutingPeerDnsResolutionEnabled)
+ retBytes, _ := json.Marshal(testAccount)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{
+ Settings: api.AccountSettings{
+ RoutingPeerDnsResolutionEnabled: ptr(true),
+ },
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testAccount, *ret)
+ })
+
+}
+
+func TestAccounts_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{
+ Settings: api.AccountSettings{
+ RoutingPeerDnsResolutionEnabled: ptr(true),
+ },
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestAccounts_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Accounts.Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestAccounts_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Accounts.Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestAccounts_Integration_List(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ accounts, err := c.Accounts.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, accounts, 1)
+ assert.Equal(t, "bf1c8084-ba50-4ce7-9439-34653001fc3b", accounts[0].Id)
+ assert.Equal(t, false, *accounts[0].Settings.Extra.PeerApprovalEnabled)
+ })
+}
+
+func TestAccounts_Integration_Update(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ accounts, err := c.Accounts.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, accounts, 1)
+ accounts[0].Settings.JwtAllowGroups = ptr([]string{"test"})
+ account, err := c.Accounts.Update(context.Background(), accounts[0].Id, api.AccountRequest{
+ Settings: accounts[0].Settings,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, accounts[0].Id, account.Id)
+ assert.Equal(t, []string{"test"}, *account.Settings.JwtAllowGroups)
+ })
+}
+
+// Account deletion on MySQL and PostgreSQL databases causes unknown errors
+// func TestAccounts_Integration_Delete(t *testing.T) {
+// withBlackBoxServer(t, func(c *rest.Client) {
+// accounts, err := c.Accounts.List(context.Background())
+// require.NoError(t, err)
+// assert.Len(t, accounts, 1)
+// err = c.Accounts.Delete(context.Background(), accounts[0].Id)
+// require.NoError(t, err)
+// _, err = c.Accounts.List(context.Background())
+// assert.Error(t, err)
+// })
+// }
diff --git a/management/client/rest/client.go b/management/client/rest/client.go
new file mode 100644
index 000000000..f55e2d11e
--- /dev/null
+++ b/management/client/rest/client.go
@@ -0,0 +1,133 @@
+package rest
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+// Client Management service HTTP REST API Client
+type Client struct {
+ managementURL string
+ authHeader string
+
+ // Accounts NetBird account APIs
+ // see more: https://docs.netbird.io/api/resources/accounts
+ Accounts *AccountsAPI
+
+ // Users NetBird users APIs
+ // see more: https://docs.netbird.io/api/resources/users
+ Users *UsersAPI
+
+ // Tokens NetBird tokens APIs
+ // see more: https://docs.netbird.io/api/resources/tokens
+ Tokens *TokensAPI
+
+ // Peers NetBird peers APIs
+ // see more: https://docs.netbird.io/api/resources/peers
+ Peers *PeersAPI
+
+ // SetupKeys NetBird setup keys APIs
+ // see more: https://docs.netbird.io/api/resources/setup-keys
+ SetupKeys *SetupKeysAPI
+
+ // Groups NetBird groups APIs
+ // see more: https://docs.netbird.io/api/resources/groups
+ Groups *GroupsAPI
+
+ // Policies NetBird policies APIs
+ // see more: https://docs.netbird.io/api/resources/policies
+ Policies *PoliciesAPI
+
+ // PostureChecks NetBird posture checks APIs
+ // see more: https://docs.netbird.io/api/resources/posture-checks
+ PostureChecks *PostureChecksAPI
+
+ // Networks NetBird networks APIs
+ // see more: https://docs.netbird.io/api/resources/networks
+ Networks *NetworksAPI
+
+ // Routes NetBird routes APIs
+ // see more: https://docs.netbird.io/api/resources/routes
+ Routes *RoutesAPI
+
+ // DNS NetBird DNS APIs
+ // see more: https://docs.netbird.io/api/resources/routes
+ DNS *DNSAPI
+
+ // GeoLocation NetBird Geo Location APIs
+ // see more: https://docs.netbird.io/api/resources/geo-locations
+ GeoLocation *GeoLocationAPI
+
+ // Events NetBird Events APIs
+ // see more: https://docs.netbird.io/api/resources/events
+ Events *EventsAPI
+}
+
+// New initialize new Client instance
+func New(managementURL, token string) *Client {
+ client := &Client{
+ managementURL: managementURL,
+ authHeader: "Token " + token,
+ }
+ client.Accounts = &AccountsAPI{client}
+ client.Users = &UsersAPI{client}
+ client.Tokens = &TokensAPI{client}
+ client.Peers = &PeersAPI{client}
+ client.SetupKeys = &SetupKeysAPI{client}
+ client.Groups = &GroupsAPI{client}
+ client.Policies = &PoliciesAPI{client}
+ client.PostureChecks = &PostureChecksAPI{client}
+ client.Networks = &NetworksAPI{client}
+ client.Routes = &RoutesAPI{client}
+ client.DNS = &DNSAPI{client}
+ client.GeoLocation = &GeoLocationAPI{client}
+ client.Events = &EventsAPI{client}
+ return client
+}
+
+func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
+ req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Authorization", c.authHeader)
+ req.Header.Add("Accept", "application/json")
+ if body != nil {
+ req.Header.Add("Content-Type", "application/json")
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode > 299 {
+ parsedErr, pErr := parseResponse[util.ErrorResponse](resp)
+ if pErr != nil {
+ return nil, err
+ }
+ return nil, errors.New(parsedErr.Message)
+ }
+
+ return resp, nil
+}
+
+func parseResponse[T any](resp *http.Response) (T, error) {
+ var ret T
+ if resp.Body == nil {
+ return ret, errors.New("No body")
+ }
+ bs, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(bs, &ret)
+
+ return ret, err
+}
diff --git a/management/client/rest/client_test.go b/management/client/rest/client_test.go
new file mode 100644
index 000000000..70e6c73e1
--- /dev/null
+++ b/management/client/rest/client_test.go
@@ -0,0 +1,34 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
+)
+
+func withMockClient(callback func(*rest.Client, *http.ServeMux)) {
+ mux := &http.ServeMux{}
+ server := httptest.NewServer(mux)
+ defer server.Close()
+ c := rest.New(server.URL, "ABC")
+ callback(c, mux)
+}
+
+func ptr[T any, PT *T](x T) PT {
+ return &x
+}
+
+func withBlackBoxServer(t *testing.T, callback func(*rest.Client)) {
+ t.Helper()
+ handler, _, _ := testing_tools.BuildApiBlackBoxWithDBState(t, "../../server/testdata/store.sql", nil, false)
+ server := httptest.NewServer(handler)
+ defer server.Close()
+ c := rest.New(server.URL, "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp")
+ callback(c)
+}
diff --git a/management/client/rest/dns.go b/management/client/rest/dns.go
new file mode 100644
index 000000000..ef9923b1f
--- /dev/null
+++ b/management/client/rest/dns.go
@@ -0,0 +1,110 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// DNSAPI APIs for DNS Management, do not use directly
+type DNSAPI struct {
+ c *Client
+}
+
+// ListNameserverGroups list all nameserver groups
+// See more: https://docs.netbird.io/api/resources/dns#list-all-nameserver-groups
+func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGroup, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.NameserverGroup](resp)
+ return ret, err
+}
+
+// GetNameserverGroup get nameserver group info
+// See more: https://docs.netbird.io/api/resources/dns#retrieve-a-nameserver-group
+func (a *DNSAPI) GetNameserverGroup(ctx context.Context, nameserverGroupID string) (*api.NameserverGroup, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.NameserverGroup](resp)
+ return &ret, err
+}
+
+// CreateNameserverGroup create new nameserver group
+// See more: https://docs.netbird.io/api/resources/dns#create-a-nameserver-group
+func (a *DNSAPI) CreateNameserverGroup(ctx context.Context, request api.PostApiDnsNameserversJSONRequestBody) (*api.NameserverGroup, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.NameserverGroup](resp)
+ return &ret, err
+}
+
+// UpdateNameserverGroup update nameserver group info
+// See more: https://docs.netbird.io/api/resources/dns#update-a-nameserver-group
+func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID string, request api.PutApiDnsNameserversNsgroupIdJSONRequestBody) (*api.NameserverGroup, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.NameserverGroup](resp)
+ return &ret, err
+}
+
+// DeleteNameserverGroup delete nameserver group
+// See more: https://docs.netbird.io/api/resources/dns#delete-a-nameserver-group
+func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
+
+// GetSettings get DNS settings
+// See more: https://docs.netbird.io/api/resources/dns#retrieve-dns-settings
+func (a *DNSAPI) GetSettings(ctx context.Context) (*api.DNSSettings, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/dns/settings", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.DNSSettings](resp)
+ return &ret, err
+}
+
+// UpdateSettings update DNS settings
+// See more: https://docs.netbird.io/api/resources/dns#update-dns-settings
+func (a *DNSAPI) UpdateSettings(ctx context.Context, request api.PutApiDnsSettingsJSONRequestBody) (*api.DNSSettings, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.DNSSettings](resp)
+ return &ret, err
+}
diff --git a/management/client/rest/dns_test.go b/management/client/rest/dns_test.go
new file mode 100644
index 000000000..b2e0a0bee
--- /dev/null
+++ b/management/client/rest/dns_test.go
@@ -0,0 +1,301 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testNameserverGroup = api.NameserverGroup{
+ Id: "Test",
+ Name: "wow",
+ }
+
+ testSettings = api.DNSSettings{
+ DisabledManagementGroups: []string{"gone"},
+ }
+)
+
+func TestDNSNameserverGroup_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.NameserverGroup{testNameserverGroup})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.ListNameserverGroups(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testNameserverGroup, ret[0])
+ })
+}
+
+func TestDNSNameserverGroup_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.ListNameserverGroups(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestDNSNameserverGroup_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testNameserverGroup)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.GetNameserverGroup(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testNameserverGroup, *ret)
+ })
+}
+
+func TestDNSNameserverGroup_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.GetNameserverGroup(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestDNSNameserverGroup_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostApiDnsNameserversJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testNameserverGroup)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.CreateNameserverGroup(context.Background(), api.PostApiDnsNameserversJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testNameserverGroup, *ret)
+ })
+}
+
+func TestDNSNameserverGroup_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.CreateNameserverGroup(context.Background(), api.PostApiDnsNameserversJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestDNSNameserverGroup_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiDnsNameserversNsgroupIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testNameserverGroup)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.UpdateNameserverGroup(context.Background(), "Test", api.PutApiDnsNameserversNsgroupIdJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testNameserverGroup, *ret)
+ })
+}
+
+func TestDNSNameserverGroup_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.UpdateNameserverGroup(context.Background(), "Test", api.PutApiDnsNameserversNsgroupIdJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestDNSNameserverGroup_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.DNS.DeleteNameserverGroup(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestDNSNameserverGroup_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.DNS.DeleteNameserverGroup(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestDNSSettings_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testSettings)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.GetSettings(context.Background())
+ require.NoError(t, err)
+ assert.Equal(t, testSettings, *ret)
+ })
+}
+
+func TestDNSSettings_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.GetSettings(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestDNSSettings_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiDnsSettingsJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, []string{"test"}, req.DisabledManagementGroups)
+ retBytes, _ := json.Marshal(testSettings)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.UpdateSettings(context.Background(), api.PutApiDnsSettingsJSONRequestBody{
+ DisabledManagementGroups: []string{"test"},
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testSettings, *ret)
+ })
+}
+
+func TestDNSSettings_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.DNS.UpdateSettings(context.Background(), api.PutApiDnsSettingsJSONRequestBody{
+ DisabledManagementGroups: []string{"test"},
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestDNS_Integration(t *testing.T) {
+ nsGroupReq := api.NameserverGroupRequest{
+ Description: "Test",
+ Enabled: true,
+ Domains: []string{},
+ Groups: []string{"cs1tnh0hhcjnqoiuebeg"},
+ Name: "test",
+ Nameservers: []api.Nameserver{
+ {
+ Ip: "8.8.8.8",
+ NsType: api.NameserverNsTypeUdp,
+ Port: 53,
+ },
+ },
+ Primary: true,
+ SearchDomainsEnabled: false,
+ }
+ withBlackBoxServer(t, func(c *rest.Client) {
+ // Create
+ nsGroup, err := c.DNS.CreateNameserverGroup(context.Background(), nsGroupReq)
+ require.NoError(t, err)
+
+ // List
+ nsGroups, err := c.DNS.ListNameserverGroups(context.Background())
+ require.NoError(t, err)
+ assert.Equal(t, *nsGroup, nsGroups[0])
+
+ // Update
+ nsGroupReq.Description = "TestUpdate"
+ nsGroup, err = c.DNS.UpdateNameserverGroup(context.Background(), nsGroup.Id, nsGroupReq)
+ require.NoError(t, err)
+ assert.Equal(t, "TestUpdate", nsGroup.Description)
+
+ // Delete
+ err = c.DNS.DeleteNameserverGroup(context.Background(), nsGroup.Id)
+ require.NoError(t, err)
+
+ // List again to ensure deletion
+ nsGroups, err = c.DNS.ListNameserverGroups(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, nsGroups, 0)
+ })
+}
diff --git a/management/client/rest/events.go b/management/client/rest/events.go
new file mode 100644
index 000000000..1157700ff
--- /dev/null
+++ b/management/client/rest/events.go
@@ -0,0 +1,24 @@
+package rest
+
+import (
+ "context"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// EventsAPI APIs for Events, do not use directly
+type EventsAPI struct {
+ c *Client
+}
+
+// List list all events
+// See more: https://docs.netbird.io/api/resources/events#list-all-events
+func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/events", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.Event](resp)
+ return ret, err
+}
diff --git a/management/client/rest/events_test.go b/management/client/rest/events_test.go
new file mode 100644
index 000000000..2589193a2
--- /dev/null
+++ b/management/client/rest/events_test.go
@@ -0,0 +1,70 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testEvent = api.Event{
+ Activity: "AccountCreate",
+ ActivityCode: api.EventActivityCodeAccountCreate,
+ }
+)
+
+func TestEvents_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.Event{testEvent})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Events.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testEvent, ret[0])
+ })
+}
+
+func TestEvents_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Events.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestEvents_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ // Do something that would trigger any event
+ _, err := c.SetupKeys.Create(context.Background(), api.CreateSetupKeyRequest{
+ Ephemeral: ptr(true),
+ Name: "TestSetupKey",
+ Type: "reusable",
+ })
+ require.NoError(t, err)
+
+ events, err := c.Events.List(context.Background())
+ require.NoError(t, err)
+ assert.NotEmpty(t, events)
+ })
+}
diff --git a/management/client/rest/geo.go b/management/client/rest/geo.go
new file mode 100644
index 000000000..ed9090fe2
--- /dev/null
+++ b/management/client/rest/geo.go
@@ -0,0 +1,36 @@
+package rest
+
+import (
+ "context"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// GeoLocationAPI APIs for Geo-Location, do not use directly
+type GeoLocationAPI struct {
+ c *Client
+}
+
+// ListCountries list all country codes
+// See more: https://docs.netbird.io/api/resources/geo-locations#list-all-country-codes
+func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.Country](resp)
+ return ret, err
+}
+
+// ListCountryCities Get a list of all English city names for a given country code
+// See more: https://docs.netbird.io/api/resources/geo-locations#list-all-city-names-by-country
+func (a *GeoLocationAPI) ListCountryCities(ctx context.Context, countryCode string) ([]api.City, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.City](resp)
+ return ret, err
+}
diff --git a/management/client/rest/geo_test.go b/management/client/rest/geo_test.go
new file mode 100644
index 000000000..d24405094
--- /dev/null
+++ b/management/client/rest/geo_test.go
@@ -0,0 +1,101 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testCountry = api.Country{
+ CountryCode: "DE",
+ CountryName: "Germany",
+ }
+
+ testCity = api.City{
+ CityName: "Berlin",
+ GeonameId: 2950158,
+ }
+)
+
+func TestGeo_ListCountries_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.Country{testCountry})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.GeoLocation.ListCountries(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testCountry, ret[0])
+ })
+}
+
+func TestGeo_ListCountries_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.GeoLocation.ListCountries(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestGeo_ListCountryCities_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.City{testCity})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.GeoLocation.ListCountryCities(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testCity, ret[0])
+ })
+}
+
+func TestGeo_ListCountryCities_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.GeoLocation.ListCountryCities(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestGeo_Integration(t *testing.T) {
+ // Blackbox is initialized with empty GeoLocations
+ withBlackBoxServer(t, func(c *rest.Client) {
+ countries, err := c.GeoLocation.ListCountries(context.Background())
+ require.NoError(t, err)
+ assert.Empty(t, countries)
+
+ cities, err := c.GeoLocation.ListCountryCities(context.Background(), "DE")
+ require.NoError(t, err)
+ assert.Empty(t, cities)
+ })
+}
diff --git a/management/client/rest/groups.go b/management/client/rest/groups.go
new file mode 100644
index 000000000..feb664273
--- /dev/null
+++ b/management/client/rest/groups.go
@@ -0,0 +1,82 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// GroupsAPI APIs for Groups, do not use directly
+type GroupsAPI struct {
+ c *Client
+}
+
+// List list all groups
+// See more: https://docs.netbird.io/api/resources/groups#list-all-groups
+func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/groups", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.Group](resp)
+ return ret, err
+}
+
+// Get get group info
+// See more: https://docs.netbird.io/api/resources/groups#retrieve-a-group
+func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/groups/"+groupID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Group](resp)
+ return &ret, err
+}
+
+// Create create new group
+// See more: https://docs.netbird.io/api/resources/groups#create-a-group
+func (a *GroupsAPI) Create(ctx context.Context, request api.PostApiGroupsJSONRequestBody) (*api.Group, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Group](resp)
+ return &ret, err
+}
+
+// Update update group info
+// See more: https://docs.netbird.io/api/resources/groups#update-a-group
+func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutApiGroupsGroupIdJSONRequestBody) (*api.Group, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Group](resp)
+ return &ret, err
+}
+
+// Delete delete group
+// See more: https://docs.netbird.io/api/resources/groups#delete-a-group
+func (a *GroupsAPI) Delete(ctx context.Context, groupID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/groups/"+groupID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/management/client/rest/groups_test.go b/management/client/rest/groups_test.go
new file mode 100644
index 000000000..d6a5410e0
--- /dev/null
+++ b/management/client/rest/groups_test.go
@@ -0,0 +1,215 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testGroup = api.Group{
+ Id: "Test",
+ Name: "wow",
+ PeersCount: 0,
+ }
+)
+
+func TestGroups_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.Group{testGroup})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Groups.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testGroup, ret[0])
+ })
+}
+
+func TestGroups_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Groups.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestGroups_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testGroup)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Groups.Get(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testGroup, *ret)
+ })
+}
+
+func TestGroups_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Groups.Get(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestGroups_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostApiGroupsJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testGroup)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Groups.Create(context.Background(), api.PostApiGroupsJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testGroup, *ret)
+ })
+}
+
+func TestGroups_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Groups.Create(context.Background(), api.PostApiGroupsJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestGroups_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiGroupsGroupIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testGroup)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Groups.Update(context.Background(), "Test", api.PutApiGroupsGroupIdJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testGroup, *ret)
+ })
+}
+
+func TestGroups_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Groups.Update(context.Background(), "Test", api.PutApiGroupsGroupIdJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestGroups_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Groups.Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestGroups_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Groups.Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestGroups_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ groups, err := c.Groups.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, groups, 1)
+
+ group, err := c.Groups.Create(context.Background(), api.GroupRequest{
+ Name: "Test",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, "Test", group.Name)
+ assert.NotEmpty(t, group.Id)
+
+ group, err = c.Groups.Update(context.Background(), group.Id, api.GroupRequest{
+ Name: "Testnt",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, "Testnt", group.Name)
+
+ err = c.Groups.Delete(context.Background(), group.Id)
+ require.NoError(t, err)
+
+ groups, err = c.Groups.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, groups, 1)
+ })
+}
diff --git a/management/client/rest/networks.go b/management/client/rest/networks.go
new file mode 100644
index 000000000..2cdd6d73d
--- /dev/null
+++ b/management/client/rest/networks.go
@@ -0,0 +1,246 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// NetworksAPI APIs for Networks, do not use directly
+type NetworksAPI struct {
+ c *Client
+}
+
+// List list all networks
+// See more: https://docs.netbird.io/api/resources/networks#list-all-networks
+func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/networks", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.Network](resp)
+ return ret, err
+}
+
+// Get get network info
+// See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network
+func (a *NetworksAPI) Get(ctx context.Context, networkID string) (*api.Network, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+networkID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Network](resp)
+ return &ret, err
+}
+
+// Create create new network
+// See more: https://docs.netbird.io/api/resources/networks#create-a-network
+func (a *NetworksAPI) Create(ctx context.Context, request api.PostApiNetworksJSONRequestBody) (*api.Network, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Network](resp)
+ return &ret, err
+}
+
+// Update update network
+// See more: https://docs.netbird.io/api/resources/networks#update-a-network
+func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api.PutApiNetworksNetworkIdJSONRequestBody) (*api.Network, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Network](resp)
+ return &ret, err
+}
+
+// Delete delete network
+// See more: https://docs.netbird.io/api/resources/networks#delete-a-network
+func (a *NetworksAPI) Delete(ctx context.Context, networkID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+networkID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
+
+// NetworkResourcesAPI APIs for Network Resources, do not use directly
+type NetworkResourcesAPI struct {
+ c *Client
+ networkID string
+}
+
+// Resources APIs for network resources
+func (a *NetworksAPI) Resources(networkID string) *NetworkResourcesAPI {
+ return &NetworkResourcesAPI{
+ c: a.c,
+ networkID: networkID,
+ }
+}
+
+// List list all resources in networks
+// See more: https://docs.netbird.io/api/resources/networks#list-all-network-resources
+func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.NetworkResource](resp)
+ return ret, err
+}
+
+// Get get network resource info
+// See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network-resource
+func (a *NetworkResourcesAPI) Get(ctx context.Context, networkResourceID string) (*api.NetworkResource, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.NetworkResource](resp)
+ return &ret, err
+}
+
+// Create create new network resource
+// See more: https://docs.netbird.io/api/resources/networks#create-a-network-resource
+func (a *NetworkResourcesAPI) Create(ctx context.Context, request api.PostApiNetworksNetworkIdResourcesJSONRequestBody) (*api.NetworkResource, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.NetworkResource](resp)
+ return &ret, err
+}
+
+// Update update network resource
+// See more: https://docs.netbird.io/api/resources/networks#update-a-network-resource
+func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID string, request api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody) (*api.NetworkResource, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.NetworkResource](resp)
+ return &ret, err
+}
+
+// Delete delete network resource
+// See more: https://docs.netbird.io/api/resources/networks#delete-a-network-resource
+func (a *NetworkResourcesAPI) Delete(ctx context.Context, networkResourceID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
+
+// NetworkRoutersAPI APIs for Network Routers, do not use directly
+type NetworkRoutersAPI struct {
+ c *Client
+ networkID string
+}
+
+// Routers APIs for network routers
+func (a *NetworksAPI) Routers(networkID string) *NetworkRoutersAPI {
+ return &NetworkRoutersAPI{
+ c: a.c,
+ networkID: networkID,
+ }
+}
+
+// List list all routers in networks
+// See more: https://docs.netbird.io/api/routers/networks#list-all-network-routers
+func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.NetworkRouter](resp)
+ return ret, err
+}
+
+// Get get network router info
+// See more: https://docs.netbird.io/api/routers/networks#retrieve-a-network-router
+func (a *NetworkRoutersAPI) Get(ctx context.Context, networkRouterID string) (*api.NetworkRouter, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.NetworkRouter](resp)
+ return &ret, err
+}
+
+// Create create new network router
+// See more: https://docs.netbird.io/api/routers/networks#create-a-network-router
+func (a *NetworkRoutersAPI) Create(ctx context.Context, request api.PostApiNetworksNetworkIdRoutersJSONRequestBody) (*api.NetworkRouter, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.NetworkRouter](resp)
+ return &ret, err
+}
+
+// Update update network router
+// See more: https://docs.netbird.io/api/routers/networks#update-a-network-router
+func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string, request api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody) (*api.NetworkRouter, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.NetworkRouter](resp)
+ return &ret, err
+}
+
+// Delete delete network router
+// See more: https://docs.netbird.io/api/routers/networks#delete-a-network-router
+func (a *NetworkRoutersAPI) Delete(ctx context.Context, networkRouterID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/management/client/rest/networks_test.go b/management/client/rest/networks_test.go
new file mode 100644
index 000000000..0772d7540
--- /dev/null
+++ b/management/client/rest/networks_test.go
@@ -0,0 +1,594 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testNetwork = api.Network{
+ Id: "Test",
+ Name: "wow",
+ }
+
+ testNetworkResource = api.NetworkResource{
+ Description: ptr("meaw"),
+ Id: "awa",
+ }
+
+ testNetworkRouter = api.NetworkRouter{
+ Id: "ouch",
+ }
+)
+
+func TestNetworks_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.Network{testNetwork})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testNetwork, ret[0])
+ })
+}
+
+func TestNetworks_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestNetworks_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testNetwork)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Get(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testNetwork, *ret)
+ })
+}
+
+func TestNetworks_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Get(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestNetworks_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostApiNetworksJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testNetwork)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Create(context.Background(), api.PostApiNetworksJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testNetwork, *ret)
+ })
+}
+
+func TestNetworks_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Create(context.Background(), api.PostApiNetworksJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestNetworks_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiNetworksNetworkIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testNetwork)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Update(context.Background(), "Test", api.PutApiNetworksNetworkIdJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testNetwork, *ret)
+ })
+}
+
+func TestNetworks_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Update(context.Background(), "Test", api.PutApiNetworksNetworkIdJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestNetworks_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Networks.Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestNetworks_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Networks.Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestNetworks_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ network, err := c.Networks.Create(context.Background(), api.NetworkRequest{
+ Description: ptr("TestNetwork"),
+ Name: "Test",
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, "Test", network.Name)
+
+ networks, err := c.Networks.List(context.Background())
+ assert.NoError(t, err)
+ assert.Empty(t, networks)
+
+ network, err = c.Networks.Update(context.Background(), "TestID", api.NetworkRequest{
+ Description: ptr("TestNetwork?"),
+ Name: "Test",
+ })
+
+ assert.NoError(t, err)
+ assert.Equal(t, "TestNetwork?", *network.Description)
+
+ err = c.Networks.Delete(context.Background(), "TestID")
+ assert.NoError(t, err)
+ })
+}
+
+func TestNetworkResources_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.NetworkResource{testNetworkResource})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Resources("Meow").List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testNetworkResource, ret[0])
+ })
+}
+
+func TestNetworkResources_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Resources("Meow").List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestNetworkResources_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testNetworkResource)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Resources("Meow").Get(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testNetworkResource, *ret)
+ })
+}
+
+func TestNetworkResources_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Resources("Meow").Get(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestNetworkResources_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostApiNetworksNetworkIdResourcesJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testNetworkResource)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Resources("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdResourcesJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testNetworkResource, *ret)
+ })
+}
+
+func TestNetworkResources_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Resources("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdResourcesJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestNetworkResources_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testNetworkResource)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Resources("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testNetworkResource, *ret)
+ })
+}
+
+func TestNetworkResources_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Resources("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestNetworkResources_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Networks.Resources("Meow").Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestNetworkResources_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Networks.Resources("Meow").Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestNetworkResources_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ _, err := c.Networks.Resources("TestNetwork").Create(context.Background(), api.NetworkResourceRequest{
+ Address: "test.com",
+ Description: ptr("Description"),
+ Enabled: false,
+ Groups: []string{"test"},
+ Name: "test",
+ })
+ assert.NoError(t, err)
+
+ _, err = c.Networks.Resources("TestNetwork").List(context.Background())
+ assert.NoError(t, err)
+
+ _, err = c.Networks.Resources("TestNetwork").Get(context.Background(), "TestResource")
+ assert.NoError(t, err)
+
+ _, err = c.Networks.Resources("TestNetwork").Update(context.Background(), "TestResource", api.NetworkResourceRequest{
+ Address: "testnt.com",
+ })
+ assert.NoError(t, err)
+
+ err = c.Networks.Resources("TestNetwork").Delete(context.Background(), "TestResource")
+ assert.NoError(t, err)
+ })
+}
+
+func TestNetworkRouters_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.NetworkRouter{testNetworkRouter})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Routers("Meow").List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testNetworkRouter, ret[0])
+ })
+}
+
+func TestNetworkRouters_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Routers("Meow").List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestNetworkRouters_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testNetworkRouter)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Routers("Meow").Get(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testNetworkRouter, *ret)
+ })
+}
+
+func TestNetworkRouters_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Routers("Meow").Get(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestNetworkRouters_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostApiNetworksNetworkIdRoutersJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "test", *req.Peer)
+ retBytes, _ := json.Marshal(testNetworkRouter)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Routers("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdRoutersJSONRequestBody{
+ Peer: ptr("test"),
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testNetworkRouter, *ret)
+ })
+}
+
+func TestNetworkRouters_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Routers("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdRoutersJSONRequestBody{
+ Peer: ptr("test"),
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestNetworkRouters_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "test", *req.Peer)
+ retBytes, _ := json.Marshal(testNetworkRouter)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Routers("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody{
+ Peer: ptr("test"),
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testNetworkRouter, *ret)
+ })
+}
+
+func TestNetworkRouters_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Networks.Routers("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody{
+ Peer: ptr("test"),
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestNetworkRouters_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Networks.Routers("Meow").Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestNetworkRouters_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Networks.Routers("Meow").Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestNetworkRouters_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ _, err := c.Networks.Routers("TestNetwork").Create(context.Background(), api.NetworkRouterRequest{
+ Enabled: false,
+ Masquerade: false,
+ Metric: 9999,
+ PeerGroups: ptr([]string{"test"}),
+ })
+ assert.NoError(t, err)
+
+ _, err = c.Networks.Routers("TestNetwork").List(context.Background())
+ assert.NoError(t, err)
+
+ _, err = c.Networks.Routers("TestNetwork").Get(context.Background(), "TestRouter")
+ assert.NoError(t, err)
+
+ _, err = c.Networks.Routers("TestNetwork").Update(context.Background(), "TestRouter", api.NetworkRouterRequest{
+ Enabled: true,
+ })
+ assert.NoError(t, err)
+
+ err = c.Networks.Routers("TestNetwork").Delete(context.Background(), "TestRouter")
+ assert.NoError(t, err)
+ })
+}
diff --git a/management/client/rest/peers.go b/management/client/rest/peers.go
new file mode 100644
index 000000000..9d35f013c
--- /dev/null
+++ b/management/client/rest/peers.go
@@ -0,0 +1,78 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// PeersAPI APIs for peers, do not use directly
+type PeersAPI struct {
+ c *Client
+}
+
+// List list all peers
+// See more: https://docs.netbird.io/api/resources/peers#list-all-peers
+func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/peers", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.Peer](resp)
+ return ret, err
+}
+
+// Get retrieve a peer
+// See more: https://docs.netbird.io/api/resources/peers#retrieve-a-peer
+func (a *PeersAPI) Get(ctx context.Context, peerID string) (*api.Peer, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Peer](resp)
+ return &ret, err
+}
+
+// Update update information for a peer
+// See more: https://docs.netbird.io/api/resources/peers#update-a-peer
+func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApiPeersPeerIdJSONRequestBody) (*api.Peer, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Peer](resp)
+ return &ret, err
+}
+
+// Delete delete a peer
+// See more: https://docs.netbird.io/api/resources/peers#delete-a-peer
+func (a *PeersAPI) Delete(ctx context.Context, peerID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/peers/"+peerID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
+
+// ListAccessiblePeers list all peers that the specified peer can connect to within the network
+// See more: https://docs.netbird.io/api/resources/peers#list-accessible-peers
+func (a *PeersAPI) ListAccessiblePeers(ctx context.Context, peerID string) ([]api.Peer, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.Peer](resp)
+ return ret, err
+}
diff --git a/management/client/rest/peers_test.go b/management/client/rest/peers_test.go
new file mode 100644
index 000000000..4c5cd1e60
--- /dev/null
+++ b/management/client/rest/peers_test.go
@@ -0,0 +1,208 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testPeer = api.Peer{
+ ApprovalRequired: false,
+ Connected: false,
+ ConnectionIp: "127.0.0.1",
+ DnsLabel: "test",
+ Id: "Test",
+ }
+)
+
+func TestPeers_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.Peer{testPeer})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Peers.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testPeer, ret[0])
+ })
+}
+
+func TestPeers_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Peers.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestPeers_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testPeer)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Peers.Get(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testPeer, *ret)
+ })
+}
+
+func TestPeers_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Peers.Get(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestPeers_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiPeersPeerIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, true, req.InactivityExpirationEnabled)
+ retBytes, _ := json.Marshal(testPeer)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Peers.Update(context.Background(), "Test", api.PutApiPeersPeerIdJSONRequestBody{
+ InactivityExpirationEnabled: true,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testPeer, *ret)
+ })
+}
+
+func TestPeers_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Peers.Update(context.Background(), "Test", api.PutApiPeersPeerIdJSONRequestBody{
+ InactivityExpirationEnabled: false,
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestPeers_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Peers.Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestPeers_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Peers.Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestPeers_ListAccessiblePeers_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.Peer{testPeer})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Peers.ListAccessiblePeers(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testPeer, ret[0])
+ })
+}
+
+func TestPeers_ListAccessiblePeers_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Peers.ListAccessiblePeers(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestPeers_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ peers, err := c.Peers.List(context.Background())
+ require.NoError(t, err)
+ require.NotEmpty(t, peers)
+
+ peer, err := c.Peers.Get(context.Background(), peers[0].Id)
+ require.NoError(t, err)
+ assert.Equal(t, peers[0].Id, peer.Id)
+
+ peer, err = c.Peers.Update(context.Background(), peer.Id, api.PeerRequest{
+ LoginExpirationEnabled: true,
+ Name: "Test",
+ SshEnabled: false,
+ ApprovalRequired: ptr(false),
+ InactivityExpirationEnabled: false,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, true, peer.LoginExpirationEnabled)
+
+ accessiblePeers, err := c.Peers.ListAccessiblePeers(context.Background(), peer.Id)
+ require.NoError(t, err)
+ assert.Empty(t, accessiblePeers)
+
+ err = c.Peers.Delete(context.Background(), peer.Id)
+ require.NoError(t, err)
+ })
+}
diff --git a/management/client/rest/policies.go b/management/client/rest/policies.go
new file mode 100644
index 000000000..be6abafaf
--- /dev/null
+++ b/management/client/rest/policies.go
@@ -0,0 +1,82 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// PoliciesAPI APIs for Policies, do not use directly
+type PoliciesAPI struct {
+ c *Client
+}
+
+// List list all policies
+// See more: https://docs.netbird.io/api/resources/policies#list-all-policies
+func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/policies", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.Policy](resp)
+ return ret, err
+}
+
+// Get get policy info
+// See more: https://docs.netbird.io/api/resources/policies#retrieve-a-policy
+func (a *PoliciesAPI) Get(ctx context.Context, policyID string) (*api.Policy, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/policies/"+policyID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Policy](resp)
+ return &ret, err
+}
+
+// Create create new policy
+// See more: https://docs.netbird.io/api/resources/policies#create-a-policy
+func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSONRequestBody) (*api.Policy, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Policy](resp)
+ return &ret, err
+}
+
+// Update update policy info
+// See more: https://docs.netbird.io/api/resources/policies#update-a-policy
+func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.PutApiPoliciesPolicyIdJSONRequestBody) (*api.Policy, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/policies/"+policyID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Policy](resp)
+ return &ret, err
+}
+
+// Delete delete policy
+// See more: https://docs.netbird.io/api/resources/policies#delete-a-policy
+func (a *PoliciesAPI) Delete(ctx context.Context, policyID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/policies/"+policyID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/management/client/rest/policies_test.go b/management/client/rest/policies_test.go
new file mode 100644
index 000000000..5792048df
--- /dev/null
+++ b/management/client/rest/policies_test.go
@@ -0,0 +1,241 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testPolicy = api.Policy{
+ Name: "wow",
+ Id: ptr("Test"),
+ Enabled: false,
+ }
+)
+
+func TestPolicies_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.Policy{testPolicy})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Policies.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testPolicy, ret[0])
+ })
+}
+
+func TestPolicies_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Policies.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestPolicies_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testPolicy)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Policies.Get(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testPolicy, *ret)
+ })
+}
+
+func TestPolicies_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Policies.Get(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestPolicies_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiPoliciesPolicyIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testPolicy)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Policies.Create(context.Background(), api.PostApiPoliciesJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testPolicy, *ret)
+ })
+}
+
+func TestPolicies_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Policies.Create(context.Background(), api.PostApiPoliciesJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestPolicies_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiPoliciesPolicyIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testPolicy)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Policies.Update(context.Background(), "Test", api.PutApiPoliciesPolicyIdJSONRequestBody{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testPolicy, *ret)
+ })
+}
+
+func TestPolicies_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Policies.Update(context.Background(), "Test", api.PutApiPoliciesPolicyIdJSONRequestBody{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestPolicies_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Policies.Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestPolicies_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Policies.Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestPolicies_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ policies, err := c.Policies.List(context.Background())
+ require.NoError(t, err)
+ require.NotEmpty(t, policies)
+
+ policy, err := c.Policies.Get(context.Background(), *policies[0].Id)
+ require.NoError(t, err)
+ assert.Equal(t, *policies[0].Id, *policy.Id)
+
+ policy, err = c.Policies.Update(context.Background(), *policy.Id, api.PolicyCreate{
+ Description: ptr("Test Policy"),
+ Enabled: false,
+ Name: "Test",
+ Rules: []api.PolicyRuleUpdate{
+ {
+ Action: api.PolicyRuleUpdateAction(policy.Rules[0].Action),
+ Bidirectional: true,
+ Description: ptr("Test Policy"),
+ Sources: ptr([]string{(*policy.Rules[0].Sources)[0].Id}),
+ Destinations: ptr([]string{(*policy.Rules[0].Destinations)[0].Id}),
+ Enabled: false,
+ Protocol: api.PolicyRuleUpdateProtocolAll,
+ },
+ },
+ SourcePostureChecks: nil,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, "Test Policy", *policy.Rules[0].Description)
+
+ policy, err = c.Policies.Create(context.Background(), api.PolicyUpdate{
+ Description: ptr("Test Policy 2"),
+ Enabled: false,
+ Name: "Test",
+ Rules: []api.PolicyRuleUpdate{
+ {
+ Action: api.PolicyRuleUpdateAction(policy.Rules[0].Action),
+ Bidirectional: true,
+ Description: ptr("Test Policy 2"),
+ Sources: ptr([]string{(*policy.Rules[0].Sources)[0].Id}),
+ Destinations: ptr([]string{(*policy.Rules[0].Destinations)[0].Id}),
+ Enabled: false,
+ Protocol: api.PolicyRuleUpdateProtocolAll,
+ },
+ },
+ SourcePostureChecks: nil,
+ })
+ require.NoError(t, err)
+
+ err = c.Policies.Delete(context.Background(), *policy.Id)
+ require.NoError(t, err)
+ })
+}
diff --git a/management/client/rest/posturechecks.go b/management/client/rest/posturechecks.go
new file mode 100644
index 000000000..950d17ba0
--- /dev/null
+++ b/management/client/rest/posturechecks.go
@@ -0,0 +1,82 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// PostureChecksAPI APIs for PostureChecks, do not use directly
+type PostureChecksAPI struct {
+ c *Client
+}
+
+// List list all posture checks
+// See more: https://docs.netbird.io/api/resources/posture-checks#list-all-posture-checks
+func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.PostureCheck](resp)
+ return ret, err
+}
+
+// Get get posture check info
+// See more: https://docs.netbird.io/api/resources/posture-checks#retrieve-a-posture-check
+func (a *PostureChecksAPI) Get(ctx context.Context, postureCheckID string) (*api.PostureCheck, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.PostureCheck](resp)
+ return &ret, err
+}
+
+// Create create new posture check
+// See more: https://docs.netbird.io/api/resources/posture-checks#create-a-posture-check
+func (a *PostureChecksAPI) Create(ctx context.Context, request api.PostApiPostureChecksJSONRequestBody) (*api.PostureCheck, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.PostureCheck](resp)
+ return &ret, err
+}
+
+// Update update posture check info
+// See more: https://docs.netbird.io/api/resources/posture-checks#update-a-posture-check
+func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, request api.PutApiPostureChecksPostureCheckIdJSONRequestBody) (*api.PostureCheck, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.PostureCheck](resp)
+ return &ret, err
+}
+
+// Delete delete posture check
+// See more: https://docs.netbird.io/api/resources/posture-checks#delete-a-posture-check
+func (a *PostureChecksAPI) Delete(ctx context.Context, postureCheckID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/management/client/rest/posturechecks_test.go b/management/client/rest/posturechecks_test.go
new file mode 100644
index 000000000..a891d6ac9
--- /dev/null
+++ b/management/client/rest/posturechecks_test.go
@@ -0,0 +1,233 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testPostureCheck = api.PostureCheck{
+ Id: "Test",
+ Name: "wow",
+ }
+)
+
+func TestPostureChecks_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.PostureCheck{testPostureCheck})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.PostureChecks.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testPostureCheck, ret[0])
+ })
+}
+
+func TestPostureChecks_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.PostureChecks.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestPostureChecks_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testPostureCheck)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.PostureChecks.Get(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testPostureCheck, *ret)
+ })
+}
+
+func TestPostureChecks_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.PostureChecks.Get(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestPostureChecks_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostureCheckUpdate
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testPostureCheck)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testPostureCheck, *ret)
+ })
+}
+
+func TestPostureChecks_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestPostureChecks_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostureCheckUpdate
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "weaw", req.Name)
+ retBytes, _ := json.Marshal(testPostureCheck)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.PostureChecks.Update(context.Background(), "Test", api.PostureCheckUpdate{
+ Name: "weaw",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testPostureCheck, *ret)
+ })
+}
+
+func TestPostureChecks_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.PostureChecks.Update(context.Background(), "Test", api.PostureCheckUpdate{
+ Name: "weaw",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestPostureChecks_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.PostureChecks.Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestPostureChecks_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.PostureChecks.Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestPostureChecks_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ check, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{
+ Name: "Test",
+ Description: "Testing",
+ Checks: &api.Checks{
+ OsVersionCheck: &api.OSVersionCheck{
+ Windows: &api.MinKernelVersionCheck{
+ MinKernelVersion: "0.0.0",
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+ assert.Equal(t, "Test", check.Name)
+
+ checks, err := c.PostureChecks.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, checks, 1)
+
+ check, err = c.PostureChecks.Update(context.Background(), check.Id, api.PostureCheckUpdate{
+ Name: "Tests",
+ Description: "Testings",
+ Checks: &api.Checks{
+ GeoLocationCheck: &api.GeoLocationCheck{
+ Action: api.GeoLocationCheckActionAllow, Locations: []api.Location{
+ {
+ CityName: ptr("Cairo"),
+ CountryCode: "EG",
+ },
+ },
+ },
+ },
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, "Testings", *check.Description)
+
+ check, err = c.PostureChecks.Get(context.Background(), check.Id)
+ require.NoError(t, err)
+ assert.Equal(t, "Tests", check.Name)
+
+ err = c.PostureChecks.Delete(context.Background(), check.Id)
+ require.NoError(t, err)
+ })
+}
diff --git a/management/client/rest/routes.go b/management/client/rest/routes.go
new file mode 100644
index 000000000..bccbb8847
--- /dev/null
+++ b/management/client/rest/routes.go
@@ -0,0 +1,82 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// RoutesAPI APIs for Routes, do not use directly
+type RoutesAPI struct {
+ c *Client
+}
+
+// List list all routes
+// See more: https://docs.netbird.io/api/resources/routes#list-all-routes
+func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/routes", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.Route](resp)
+ return ret, err
+}
+
+// Get get route info
+// See more: https://docs.netbird.io/api/resources/routes#retrieve-a-route
+func (a *RoutesAPI) Get(ctx context.Context, routeID string) (*api.Route, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/routes/"+routeID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Route](resp)
+ return &ret, err
+}
+
+// Create create new route
+// See more: https://docs.netbird.io/api/resources/routes#create-a-route
+func (a *RoutesAPI) Create(ctx context.Context, request api.PostApiRoutesJSONRequestBody) (*api.Route, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Route](resp)
+ return &ret, err
+}
+
+// Update update route info
+// See more: https://docs.netbird.io/api/resources/routes#update-a-route
+func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutApiRoutesRouteIdJSONRequestBody) (*api.Route, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.Route](resp)
+ return &ret, err
+}
+
+// Delete delete route
+// See more: https://docs.netbird.io/api/resources/routes#delete-a-route
+func (a *RoutesAPI) Delete(ctx context.Context, routeID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/routes/"+routeID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/management/client/rest/routes_test.go b/management/client/rest/routes_test.go
new file mode 100644
index 000000000..1c698a7fb
--- /dev/null
+++ b/management/client/rest/routes_test.go
@@ -0,0 +1,231 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testRoute = api.Route{
+ Id: "Test",
+ Domains: ptr([]string{"google.com"}),
+ }
+)
+
+func TestRoutes_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.Route{testRoute})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Routes.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testRoute, ret[0])
+ })
+}
+
+func TestRoutes_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Routes.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestRoutes_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testRoute)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Routes.Get(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testRoute, *ret)
+ })
+}
+
+func TestRoutes_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Routes.Get(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestRoutes_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostApiRoutesJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "meow", req.Description)
+ retBytes, _ := json.Marshal(testRoute)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Routes.Create(context.Background(), api.PostApiRoutesJSONRequestBody{
+ Description: "meow",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testRoute, *ret)
+ })
+}
+
+func TestRoutes_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Routes.Create(context.Background(), api.PostApiRoutesJSONRequestBody{
+ Description: "meow",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestRoutes_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiRoutesRouteIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, "meow", req.Description)
+ retBytes, _ := json.Marshal(testRoute)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Routes.Update(context.Background(), "Test", api.PutApiRoutesRouteIdJSONRequestBody{
+ Description: "meow",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testRoute, *ret)
+ })
+}
+
+func TestRoutes_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Routes.Update(context.Background(), "Test", api.PutApiRoutesRouteIdJSONRequestBody{
+ Description: "meow",
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestRoutes_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Routes.Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestRoutes_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Routes.Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestRoutes_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ route, err := c.Routes.Create(context.Background(), api.RouteRequest{
+ Description: "Meow",
+ Enabled: false,
+ Groups: []string{"cs1tnh0hhcjnqoiuebeg"},
+ PeerGroups: ptr([]string{"cs1tnh0hhcjnqoiuebeg"}),
+ Domains: ptr([]string{"google.com"}),
+ Masquerade: true,
+ Metric: 9999,
+ KeepRoute: false,
+ NetworkId: "Test",
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, "Test", route.NetworkId)
+
+ routes, err := c.Routes.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, routes, 1)
+
+ route, err = c.Routes.Update(context.Background(), route.Id, api.RouteRequest{
+ Description: "Testings",
+ Enabled: false,
+ Groups: []string{"cs1tnh0hhcjnqoiuebeg"},
+ PeerGroups: ptr([]string{"cs1tnh0hhcjnqoiuebeg"}),
+ Domains: ptr([]string{"google.com"}),
+ Masquerade: true,
+ Metric: 9999,
+ KeepRoute: false,
+ NetworkId: "Tests",
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, "Testings", route.Description)
+
+ route, err = c.Routes.Get(context.Background(), route.Id)
+ require.NoError(t, err)
+ assert.Equal(t, "Tests", route.NetworkId)
+
+ err = c.Routes.Delete(context.Background(), route.Id)
+ require.NoError(t, err)
+ })
+}
diff --git a/management/client/rest/setupkeys.go b/management/client/rest/setupkeys.go
new file mode 100644
index 000000000..645614fcf
--- /dev/null
+++ b/management/client/rest/setupkeys.go
@@ -0,0 +1,82 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// SetupKeysAPI APIs for Setup keys, do not use directly
+type SetupKeysAPI struct {
+ c *Client
+}
+
+// List list all setup keys
+// See more: https://docs.netbird.io/api/resources/setup-keys#list-all-setup-keys
+func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.SetupKey](resp)
+ return ret, err
+}
+
+// Get get setup key info
+// See more: https://docs.netbird.io/api/resources/setup-keys#retrieve-a-setup-key
+func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKey, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.SetupKey](resp)
+ return &ret, err
+}
+
+// Create generate new Setup Key
+// See more: https://docs.netbird.io/api/resources/setup-keys#create-a-setup-key
+func (a *SetupKeysAPI) Create(ctx context.Context, request api.PostApiSetupKeysJSONRequestBody) (*api.SetupKeyClear, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/setup-keys", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.SetupKeyClear](resp)
+ return &ret, err
+}
+
+// Update generate new Setup Key
+// See more: https://docs.netbird.io/api/resources/setup-keys#update-a-setup-key
+func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request api.PutApiSetupKeysKeyIdJSONRequestBody) (*api.SetupKey, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.SetupKey](resp)
+ return &ret, err
+}
+
+// Delete delete setup key
+// See more: https://docs.netbird.io/api/resources/setup-keys#delete-a-setup-key
+func (a *SetupKeysAPI) Delete(ctx context.Context, setupKeyID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/management/client/rest/setupkeys_test.go b/management/client/rest/setupkeys_test.go
new file mode 100644
index 000000000..8edce8428
--- /dev/null
+++ b/management/client/rest/setupkeys_test.go
@@ -0,0 +1,232 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testSetupKey = api.SetupKey{
+ Id: "Test",
+ Name: "wow",
+ AutoGroups: []string{"meow"},
+ Ephemeral: true,
+ }
+
+ testSteupKeyGenerated = api.SetupKeyClear{
+ Id: "Test",
+ Name: "wow",
+ AutoGroups: []string{"meow"},
+ Ephemeral: true,
+ Key: "shhh",
+ }
+)
+
+func TestSetupKeys_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.SetupKey{testSetupKey})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.SetupKeys.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testSetupKey, ret[0])
+ })
+}
+
+func TestSetupKeys_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.SetupKeys.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestSetupKeys_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testSetupKey)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.SetupKeys.Get(context.Background(), "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testSetupKey, *ret)
+ })
+}
+
+func TestSetupKeys_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.SetupKeys.Get(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestSetupKeys_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostApiSetupKeysJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, 5, req.ExpiresIn)
+ retBytes, _ := json.Marshal(testSteupKeyGenerated)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.SetupKeys.Create(context.Background(), api.PostApiSetupKeysJSONRequestBody{
+ ExpiresIn: 5,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testSteupKeyGenerated, *ret)
+ })
+}
+
+func TestSetupKeys_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.SetupKeys.Create(context.Background(), api.PostApiSetupKeysJSONRequestBody{
+ ExpiresIn: 5,
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestSetupKeys_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiSetupKeysKeyIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, true, req.Revoked)
+ retBytes, _ := json.Marshal(testSetupKey)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.SetupKeys.Update(context.Background(), "Test", api.PutApiSetupKeysKeyIdJSONRequestBody{
+ Revoked: true,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testSetupKey, *ret)
+ })
+}
+
+func TestSetupKeys_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.SetupKeys.Update(context.Background(), "Test", api.PutApiSetupKeysKeyIdJSONRequestBody{
+ Revoked: true,
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestSetupKeys_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.SetupKeys.Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestSetupKeys_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.SetupKeys.Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestSetupKeys_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ group, err := c.Groups.Create(context.Background(), api.GroupRequest{
+ Name: "Test",
+ })
+ require.NoError(t, err)
+
+ skClear, err := c.SetupKeys.Create(context.Background(), api.CreateSetupKeyRequest{
+ AutoGroups: []string{group.Id},
+ Ephemeral: ptr(false),
+ Name: "test",
+ Type: "reusable",
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, true, skClear.Valid)
+
+ keys, err := c.SetupKeys.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, keys, 2)
+
+ sk, err := c.SetupKeys.Update(context.Background(), skClear.Id, api.SetupKeyRequest{
+ Revoked: true,
+ AutoGroups: []string{group.Id},
+ })
+ require.NoError(t, err)
+
+ sk, err = c.SetupKeys.Get(context.Background(), sk.Id)
+ require.NoError(t, err)
+ assert.Equal(t, false, sk.Valid)
+
+ err = c.SetupKeys.Delete(context.Background(), sk.Id)
+ require.NoError(t, err)
+ })
+}
diff --git a/management/client/rest/tokens.go b/management/client/rest/tokens.go
new file mode 100644
index 000000000..3275bea81
--- /dev/null
+++ b/management/client/rest/tokens.go
@@ -0,0 +1,66 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// TokensAPI APIs for PATs, do not use directly
+type TokensAPI struct {
+ c *Client
+}
+
+// List list user tokens
+// See more: https://docs.netbird.io/api/resources/tokens#list-all-tokens
+func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAccessToken, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.PersonalAccessToken](resp)
+ return ret, err
+}
+
+// Get get user token info
+// See more: https://docs.netbird.io/api/resources/tokens#retrieve-a-token
+func (a *TokensAPI) Get(ctx context.Context, userID, tokenID string) (*api.PersonalAccessToken, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.PersonalAccessToken](resp)
+ return &ret, err
+}
+
+// Create generate new PAT for user
+// See more: https://docs.netbird.io/api/resources/tokens#create-a-token
+func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostApiUsersUserIdTokensJSONRequestBody) (*api.PersonalAccessTokenGenerated, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.PersonalAccessTokenGenerated](resp)
+ return &ret, err
+}
+
+// Delete delete user token
+// See more: https://docs.netbird.io/api/resources/tokens#delete-a-token
+func (a *TokensAPI) Delete(ctx context.Context, userID, tokenID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/management/client/rest/tokens_test.go b/management/client/rest/tokens_test.go
new file mode 100644
index 000000000..eea55d22f
--- /dev/null
+++ b/management/client/rest/tokens_test.go
@@ -0,0 +1,180 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testToken = api.PersonalAccessToken{
+ Id: "Test",
+ CreatedAt: time.Time{},
+ CreatedBy: "meow",
+ ExpirationDate: time.Time{},
+ LastUsed: nil,
+ Name: "wow",
+ }
+
+ testTokenGenerated = api.PersonalAccessTokenGenerated{
+ PersonalAccessToken: testToken,
+ PlainToken: "shhh",
+ }
+)
+
+func TestTokens_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.PersonalAccessToken{testToken})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Tokens.List(context.Background(), "meow")
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testToken, ret[0])
+ })
+}
+
+func TestTokens_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Tokens.List(context.Background(), "meow")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestTokens_Get_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(testToken)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Tokens.Get(context.Background(), "meow", "Test")
+ require.NoError(t, err)
+ assert.Equal(t, testToken, *ret)
+ })
+}
+
+func TestTokens_Get_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Tokens.Get(context.Background(), "meow", "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestTokens_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostApiUsersUserIdTokensJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, 5, req.ExpiresIn)
+ retBytes, _ := json.Marshal(testTokenGenerated)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Tokens.Create(context.Background(), "meow", api.PostApiUsersUserIdTokensJSONRequestBody{
+ ExpiresIn: 5,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testTokenGenerated, *ret)
+ })
+}
+
+func TestTokens_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Tokens.Create(context.Background(), "meow", api.PostApiUsersUserIdTokensJSONRequestBody{
+ ExpiresIn: 5,
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestTokens_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Tokens.Delete(context.Background(), "meow", "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestTokens_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Tokens.Delete(context.Background(), "meow", "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestTokens_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ tokenClear, err := c.Tokens.Create(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", api.PersonalAccessTokenRequest{
+ Name: "Test",
+ ExpiresIn: 365,
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, "Test", tokenClear.PersonalAccessToken.Name)
+
+ tokens, err := c.Tokens.List(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003")
+ require.NoError(t, err)
+ assert.Len(t, tokens, 2)
+
+ token, err := c.Tokens.Get(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", tokenClear.PersonalAccessToken.Id)
+ require.NoError(t, err)
+ assert.Equal(t, "Test", token.Name)
+
+ err = c.Tokens.Delete(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", token.Id)
+ require.NoError(t, err)
+ })
+}
diff --git a/management/client/rest/users.go b/management/client/rest/users.go
new file mode 100644
index 000000000..372bcee45
--- /dev/null
+++ b/management/client/rest/users.go
@@ -0,0 +1,82 @@
+package rest
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+
+ "github.com/netbirdio/netbird/management/server/http/api"
+)
+
+// UsersAPI APIs for users, do not use directly
+type UsersAPI struct {
+ c *Client
+}
+
+// List list all users, only returns one user always
+// See more: https://docs.netbird.io/api/resources/users#list-all-users
+func (a *UsersAPI) List(ctx context.Context) ([]api.User, error) {
+ resp, err := a.c.newRequest(ctx, "GET", "/api/users", nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[[]api.User](resp)
+ return ret, err
+}
+
+// Create create user
+// See more: https://docs.netbird.io/api/resources/users#create-a-user
+func (a *UsersAPI) Create(ctx context.Context, request api.PostApiUsersJSONRequestBody) (*api.User, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.User](resp)
+ return &ret, err
+}
+
+// Update update user settings
+// See more: https://docs.netbird.io/api/resources/users#update-a-user
+func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApiUsersUserIdJSONRequestBody) (*api.User, error) {
+ requestBytes, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := a.c.newRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ret, err := parseResponse[api.User](resp)
+ return &ret, err
+}
+
+// Delete delete user
+// See more: https://docs.netbird.io/api/resources/users#delete-a-user
+func (a *UsersAPI) Delete(ctx context.Context, userID string) error {
+ resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
+
+// ResendInvitation resend user invitation
+// See more: https://docs.netbird.io/api/resources/users#resend-user-invitation
+func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error {
+ resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/management/client/rest/users_test.go b/management/client/rest/users_test.go
new file mode 100644
index 000000000..2ff8a0327
--- /dev/null
+++ b/management/client/rest/users_test.go
@@ -0,0 +1,227 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/client/rest"
+ "github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/http/util"
+)
+
+var (
+ testUser = api.User{
+ Id: "Test",
+ AutoGroups: []string{"test-group"},
+ Email: "test@test.com",
+ IsBlocked: false,
+ IsCurrent: ptr(false),
+ IsServiceUser: ptr(false),
+ Issued: ptr("api"),
+ LastLogin: &time.Time{},
+ Name: "M. Essam",
+ Permissions: &api.UserPermissions{
+ DashboardView: ptr(api.UserPermissionsDashboardViewFull),
+ },
+ Role: "user",
+ Status: api.UserStatusActive,
+ }
+)
+
+func TestUsers_List_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal([]api.User{testUser})
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Users.List(context.Background())
+ require.NoError(t, err)
+ assert.Len(t, ret, 1)
+ assert.Equal(t, testUser, ret[0])
+ })
+}
+
+func TestUsers_List_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Users.List(context.Background())
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Empty(t, ret)
+ })
+}
+
+func TestUsers_Create_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PostApiUsersJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, []string{"meow"}, req.AutoGroups)
+ retBytes, _ := json.Marshal(testUser)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Users.Create(context.Background(), api.PostApiUsersJSONRequestBody{
+ AutoGroups: []string{"meow"},
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testUser, *ret)
+ })
+}
+
+func TestUsers_Create_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Users.Create(context.Background(), api.PostApiUsersJSONRequestBody{
+ AutoGroups: []string{"meow"},
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestUsers_Update_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ reqBytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var req api.PutApiUsersUserIdJSONRequestBody
+ err = json.Unmarshal(reqBytes, &req)
+ require.NoError(t, err)
+ assert.Equal(t, true, req.IsBlocked)
+ retBytes, _ := json.Marshal(testUser)
+ _, err = w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Users.Update(context.Background(), "Test", api.PutApiUsersUserIdJSONRequestBody{
+ IsBlocked: true,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, testUser, *ret)
+ })
+
+}
+
+func TestUsers_Update_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
+ w.WriteHeader(400)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ ret, err := c.Users.Update(context.Background(), "Test", api.PutApiUsersUserIdJSONRequestBody{
+ IsBlocked: true,
+ })
+ assert.Error(t, err)
+ assert.Equal(t, "No", err.Error())
+ assert.Nil(t, ret)
+ })
+}
+
+func TestUsers_Delete_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "DELETE", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Users.Delete(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestUsers_Delete_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Users.Delete(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestUsers_ResendInvitation_200(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "POST", r.Method)
+ w.WriteHeader(200)
+ })
+ err := c.Users.ResendInvitation(context.Background(), "Test")
+ require.NoError(t, err)
+ })
+}
+
+func TestUsers_ResendInvitation_Err(t *testing.T) {
+ withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+ mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) {
+ retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
+ w.WriteHeader(404)
+ _, err := w.Write(retBytes)
+ require.NoError(t, err)
+ })
+ err := c.Users.ResendInvitation(context.Background(), "Test")
+ assert.Error(t, err)
+ assert.Equal(t, "Not found", err.Error())
+ })
+}
+
+func TestUsers_Integration(t *testing.T) {
+ withBlackBoxServer(t, func(c *rest.Client) {
+ user, err := c.Users.Create(context.Background(), api.UserCreateRequest{
+ AutoGroups: []string{},
+ Email: ptr("test@example.com"),
+ IsServiceUser: true,
+ Name: ptr("Nobody"),
+ Role: "user",
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, "Nobody", user.Name)
+
+ users, err := c.Users.List(context.Background())
+ require.NoError(t, err)
+ assert.NotEmpty(t, users)
+
+ user, err = c.Users.Update(context.Background(), user.Id, api.UserRequest{
+ AutoGroups: []string{},
+ Role: "admin",
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, "admin", user.Role)
+
+ err = c.Users.Delete(context.Background(), user.Id)
+ require.NoError(t, err)
+ })
+}
diff --git a/management/cmd/management.go b/management/cmd/management.go
index 1c8fca8dc..9712f04aa 100644
--- a/management/cmd/management.go
+++ b/management/cmd/management.go
@@ -39,13 +39,12 @@ import (
"github.com/netbirdio/netbird/formatter"
mgmtProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/management/server"
+ "github.com/netbirdio/netbird/management/server/auth"
nbContext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/groups"
nbhttp "github.com/netbirdio/netbird/management/server/http"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/idp"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/metrics"
"github.com/netbirdio/netbird/management/server/networks"
"github.com/netbirdio/netbird/management/server/networks/resources"
@@ -255,24 +254,13 @@ var (
tlsEnabled = true
}
- jwtValidator, err := jwtclaims.NewJWTValidator(
- ctx,
+ authManager := auth.NewManager(store,
config.HttpConfig.AuthIssuer,
- config.GetAuthAudiences(),
+ config.HttpConfig.AuthAudience,
config.HttpConfig.AuthKeysLocation,
- config.HttpConfig.IdpSignKeyRefreshEnabled,
- )
- if err != nil {
- return fmt.Errorf("failed creating JWT validator: %v", err)
- }
-
- httpAPIAuthCfg := configs.AuthCfg{
- Issuer: config.HttpConfig.AuthIssuer,
- Audience: config.HttpConfig.AuthAudience,
- UserIDClaim: config.HttpConfig.AuthUserIDClaim,
- KeysLocation: config.HttpConfig.AuthKeysLocation,
- }
-
+ config.HttpConfig.AuthUserIDClaim,
+ config.GetAuthAudiences(),
+ config.HttpConfig.IdpSignKeyRefreshEnabled)
userManager := users.NewManager(store)
settingsManager := settings.NewManager(store)
permissionsManager := permissions.NewManager(userManager, settingsManager)
@@ -281,7 +269,7 @@ var (
routersManager := routers.NewManager(store, permissionsManager, accountManager)
networksManager := networks.NewManager(store, permissionsManager, resourcesManager, routersManager, accountManager)
- httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, jwtValidator, appMetrics, httpAPIAuthCfg, integratedPeerValidator)
+ httpAPIHandler, err := nbhttp.NewAPIHandler(ctx, accountManager, networksManager, resourcesManager, routersManager, groupsManager, geo, authManager, appMetrics, config, integratedPeerValidator)
if err != nil {
return fmt.Errorf("failed creating HTTP API handler: %v", err)
}
@@ -290,7 +278,7 @@ var (
ephemeralManager.LoadInitialPeers(ctx)
gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
- srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager)
+ srv, err := server.NewServer(ctx, config, accountManager, settingsManager, peersUpdateManager, secretsManager, appMetrics, ephemeralManager, authManager)
if err != nil {
return fmt.Errorf("failed creating gRPC API handler: %v", err)
}
diff --git a/management/domain/validate.go b/management/domain/validate.go
new file mode 100644
index 000000000..bcbf26e05
--- /dev/null
+++ b/management/domain/validate.go
@@ -0,0 +1,65 @@
+package domain
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+const maxDomains = 32
+
+// ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
+func ValidateDomains(domains []string) (List, error) {
+ if len(domains) == 0 {
+ return nil, fmt.Errorf("domains list is empty")
+ }
+ if len(domains) > maxDomains {
+ return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
+ }
+
+ domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
+
+ var domainList List
+
+ for _, d := range domains {
+ d := strings.ToLower(d)
+
+ // handles length and idna conversion
+ punycode, err := FromString(d)
+ if err != nil {
+ return domainList, fmt.Errorf("convert domain to punycode: %s: %w", d, err)
+ }
+
+ if !domainRegex.MatchString(string(punycode)) {
+ return domainList, fmt.Errorf("invalid domain format: %s", d)
+ }
+
+ domainList = append(domainList, punycode)
+ }
+ return domainList, nil
+}
+
+// ValidateDomainsStrSlice checks if each domain in the list is valid
+func ValidateDomainsStrSlice(domains []string) ([]string, error) {
+ if len(domains) == 0 {
+ return nil, nil
+ }
+ if len(domains) > maxDomains {
+ return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
+ }
+
+ domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
+
+ var domainList []string
+
+ for _, d := range domains {
+ d := strings.ToLower(d)
+
+ if !domainRegex.MatchString(d) {
+ return domainList, fmt.Errorf("invalid domain format: %s", d)
+ }
+
+ domainList = append(domainList, d)
+ }
+ return domainList, nil
+}
diff --git a/management/domain/validate_test.go b/management/domain/validate_test.go
new file mode 100644
index 000000000..c9c042d9d
--- /dev/null
+++ b/management/domain/validate_test.go
@@ -0,0 +1,206 @@
+package domain
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestValidateDomains(t *testing.T) {
+ tests := []struct {
+ name string
+ domains []string
+ expected List
+ wantErr bool
+ }{
+ {
+ name: "Empty list",
+ domains: nil,
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Valid ASCII domain",
+ domains: []string{"sub.ex-ample.com"},
+ expected: List{"sub.ex-ample.com"},
+ wantErr: false,
+ },
+ {
+ name: "Valid Unicode domain",
+ domains: []string{"münchen.de"},
+ expected: List{"xn--mnchen-3ya.de"},
+ wantErr: false,
+ },
+ {
+ name: "Valid Unicode, all labels",
+ domains: []string{"中国.中国.中国"},
+ expected: List{"xn--fiqs8s.xn--fiqs8s.xn--fiqs8s"},
+ wantErr: false,
+ },
+ {
+ name: "With underscores",
+ domains: []string{"_jabber._tcp.gmail.com"},
+ expected: List{"_jabber._tcp.gmail.com"},
+ wantErr: false,
+ },
+ {
+ name: "Invalid domain format",
+ domains: []string{"-example.com"},
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Invalid domain format 2",
+ domains: []string{"example.com-"},
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Multiple domains valid and invalid",
+ domains: []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
+ expected: List{"google.com"},
+ wantErr: true,
+ },
+ {
+ name: "Valid wildcard domain",
+ domains: []string{"*.example.com"},
+ expected: List{"*.example.com"},
+ wantErr: false,
+ },
+ {
+ name: "Wildcard with dot domain",
+ domains: []string{".*.example.com"},
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Wildcard with dot domain",
+ domains: []string{".*.example.com"},
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Invalid wildcard domain",
+ domains: []string{"a.*.example.com"},
+ expected: nil,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ValidateDomains(tt.domains)
+ assert.Equal(t, tt.wantErr, err != nil)
+ assert.Equal(t, got, tt.expected)
+ })
+ }
+}
+
+// TestValidateDomainsStrSlice tests the ValidateDomainsStrSlice function.
+func TestValidateDomainsStrSlice(t *testing.T) {
+ // Generate a slice of valid domains up to maxDomains
+ validDomains := make([]string, maxDomains)
+ for i := 0; i < maxDomains; i++ {
+ validDomains[i] = fmt.Sprintf("example%d.com", i)
+ }
+
+ tests := []struct {
+ name string
+ domains []string
+ expected []string
+ wantErr bool
+ }{
+ {
+ name: "Empty list",
+ domains: nil,
+ expected: nil,
+ wantErr: false,
+ },
+ {
+ name: "Single valid ASCII domain",
+ domains: []string{"sub.ex-ample.com"},
+ expected: []string{"sub.ex-ample.com"},
+ wantErr: false,
+ },
+ {
+ name: "Underscores in labels",
+ domains: []string{"_jabber._tcp.gmail.com"},
+ expected: []string{"_jabber._tcp.gmail.com"},
+ wantErr: false,
+ },
+ {
+ // Unlike ValidateDomains (which converts to punycode),
+ // ValidateDomainsStrSlice will fail on non-ASCII domain chars.
+ name: "Unicode domain fails (no punycode conversion)",
+ domains: []string{"münchen.de"},
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Invalid domain format - leading dash",
+ domains: []string{"-example.com"},
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Invalid domain format - trailing dash",
+ domains: []string{"example-.com"},
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ // The function stops on the first invalid domain and returns an error,
+ // so only the first domain is definitely valid, but the second is invalid.
+ name: "Multiple domains with a valid one, then invalid",
+ domains: []string{"google.com", "invalid_domain.com-"},
+ expected: []string{"google.com"},
+ wantErr: true,
+ },
+ {
+ name: "Valid wildcard domain",
+ domains: []string{"*.example.com"},
+ expected: []string{"*.example.com"},
+ wantErr: false,
+ },
+ {
+ name: "Wildcard with leading dot - invalid",
+ domains: []string{".*.example.com"},
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Invalid wildcard with multiple asterisks",
+ domains: []string{"a.*.example.com"},
+ expected: nil,
+ wantErr: true,
+ },
+ {
+ name: "Exactly maxDomains items (valid)",
+ domains: validDomains,
+ expected: validDomains,
+ wantErr: false,
+ },
+ {
+ name: "Exceeds maxDomains items",
+ domains: append(validDomains, "extra.com"),
+ expected: nil,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ValidateDomainsStrSlice(tt.domains)
+ // Check if we got an error where expected
+ if tt.wantErr {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+
+ // Compare the returned domains to what we expect
+ assert.Equal(t, tt.expected, got)
+ })
+ }
+}
diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go
index b4ff16e6d..2cd00783e 100644
--- a/management/proto/management.pb.go
+++ b/management/proto/management.pb.go
@@ -223,7 +223,7 @@ func (x HostConfig_Protocol) Number() protoreflect.EnumNumber {
// Deprecated: Use HostConfig_Protocol.Descriptor instead.
func (HostConfig_Protocol) EnumDescriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{13, 0}
+ return file_management_proto_rawDescGZIP(), []int{14, 0}
}
type DeviceAuthorizationFlowProvider int32
@@ -266,7 +266,7 @@ func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber {
// Deprecated: Use DeviceAuthorizationFlowProvider.Descriptor instead.
func (DeviceAuthorizationFlowProvider) EnumDescriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{21, 0}
+ return file_management_proto_rawDescGZIP(), []int{22, 0}
}
type EncryptedMessage struct {
@@ -278,7 +278,7 @@ type EncryptedMessage struct {
WgPubKey string `protobuf:"bytes,1,opt,name=wgPubKey,proto3" json:"wgPubKey,omitempty"`
// encrypted message Body
Body []byte `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"`
- // Version of the Wiretrustee Management Service protocol
+ // Version of the Netbird Management Service protocol
Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"`
}
@@ -383,14 +383,14 @@ func (x *SyncRequest) GetMeta() *PeerSystemMeta {
return nil
}
-// SyncResponse represents a state that should be applied to the local peer (e.g. Wiretrustee servers config as well as local peer and remote peers configs)
+// SyncResponse represents a state that should be applied to the local peer (e.g. Netbird servers config as well as local peer and remote peers configs)
type SyncResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Global config
- WiretrusteeConfig *WiretrusteeConfig `protobuf:"bytes,1,opt,name=wiretrusteeConfig,proto3" json:"wiretrusteeConfig,omitempty"`
+ NetbirdConfig *NetbirdConfig `protobuf:"bytes,1,opt,name=netbirdConfig,proto3" json:"netbirdConfig,omitempty"`
// Deprecated. Use NetworkMap.PeerConfig
PeerConfig *PeerConfig `protobuf:"bytes,2,opt,name=peerConfig,proto3" json:"peerConfig,omitempty"`
// Deprecated. Use NetworkMap.RemotePeerConfig
@@ -435,9 +435,9 @@ func (*SyncResponse) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{2}
}
-func (x *SyncResponse) GetWiretrusteeConfig() *WiretrusteeConfig {
+func (x *SyncResponse) GetNetbirdConfig() *NetbirdConfig {
if x != nil {
- return x.WiretrusteeConfig
+ return x.NetbirdConfig
}
return nil
}
@@ -537,7 +537,8 @@ type LoginRequest struct {
// SSO token (can be empty)
JwtToken string `protobuf:"bytes,3,opt,name=jwtToken,proto3" json:"jwtToken,omitempty"`
// Can be absent for now.
- PeerKeys *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"`
+ PeerKeys *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"`
+ DnsLabels []string `protobuf:"bytes,5,rep,name=dnsLabels,proto3" json:"dnsLabels,omitempty"`
}
func (x *LoginRequest) Reset() {
@@ -600,6 +601,13 @@ func (x *LoginRequest) GetPeerKeys() *PeerKeys {
return nil
}
+func (x *LoginRequest) GetDnsLabels() []string {
+ if x != nil {
+ return x.DnsLabels
+ }
+ return nil
+}
+
// PeerKeys is additional peer info like SSH pub key and WireGuard public key.
// This message is sent on Login or register requests, or when a key rotation has to happen.
type PeerKeys struct {
@@ -784,34 +792,130 @@ func (x *File) GetProcessIsRunning() bool {
return false
}
+type Flags struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ RosenpassEnabled bool `protobuf:"varint,1,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"`
+ RosenpassPermissive bool `protobuf:"varint,2,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"`
+ ServerSSHAllowed bool `protobuf:"varint,3,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"`
+ DisableClientRoutes bool `protobuf:"varint,4,opt,name=disableClientRoutes,proto3" json:"disableClientRoutes,omitempty"`
+ DisableServerRoutes bool `protobuf:"varint,5,opt,name=disableServerRoutes,proto3" json:"disableServerRoutes,omitempty"`
+ DisableDNS bool `protobuf:"varint,6,opt,name=disableDNS,proto3" json:"disableDNS,omitempty"`
+ DisableFirewall bool `protobuf:"varint,7,opt,name=disableFirewall,proto3" json:"disableFirewall,omitempty"`
+}
+
+func (x *Flags) Reset() {
+ *x = Flags{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_management_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Flags) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Flags) ProtoMessage() {}
+
+func (x *Flags) ProtoReflect() protoreflect.Message {
+ mi := &file_management_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Flags.ProtoReflect.Descriptor instead.
+func (*Flags) Descriptor() ([]byte, []int) {
+ return file_management_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *Flags) GetRosenpassEnabled() bool {
+ if x != nil {
+ return x.RosenpassEnabled
+ }
+ return false
+}
+
+func (x *Flags) GetRosenpassPermissive() bool {
+ if x != nil {
+ return x.RosenpassPermissive
+ }
+ return false
+}
+
+func (x *Flags) GetServerSSHAllowed() bool {
+ if x != nil {
+ return x.ServerSSHAllowed
+ }
+ return false
+}
+
+func (x *Flags) GetDisableClientRoutes() bool {
+ if x != nil {
+ return x.DisableClientRoutes
+ }
+ return false
+}
+
+func (x *Flags) GetDisableServerRoutes() bool {
+ if x != nil {
+ return x.DisableServerRoutes
+ }
+ return false
+}
+
+func (x *Flags) GetDisableDNS() bool {
+ if x != nil {
+ return x.DisableDNS
+ }
+ return false
+}
+
+func (x *Flags) GetDisableFirewall() bool {
+ if x != nil {
+ return x.DisableFirewall
+ }
+ return false
+}
+
// PeerSystemMeta is machine meta data like OS and version.
type PeerSystemMeta struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"`
- GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"`
- Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"`
- Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"`
- Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"`
- OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"`
- WiretrusteeVersion string `protobuf:"bytes,7,opt,name=wiretrusteeVersion,proto3" json:"wiretrusteeVersion,omitempty"`
- UiVersion string `protobuf:"bytes,8,opt,name=uiVersion,proto3" json:"uiVersion,omitempty"`
- KernelVersion string `protobuf:"bytes,9,opt,name=kernelVersion,proto3" json:"kernelVersion,omitempty"`
- OSVersion string `protobuf:"bytes,10,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"`
- NetworkAddresses []*NetworkAddress `protobuf:"bytes,11,rep,name=networkAddresses,proto3" json:"networkAddresses,omitempty"`
- SysSerialNumber string `protobuf:"bytes,12,opt,name=sysSerialNumber,proto3" json:"sysSerialNumber,omitempty"`
- SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"`
- SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"`
- Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"`
- Files []*File `protobuf:"bytes,16,rep,name=files,proto3" json:"files,omitempty"`
+ Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"`
+ GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"`
+ Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"`
+ Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"`
+ Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"`
+ OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"`
+ NetbirdVersion string `protobuf:"bytes,7,opt,name=netbirdVersion,proto3" json:"netbirdVersion,omitempty"`
+ UiVersion string `protobuf:"bytes,8,opt,name=uiVersion,proto3" json:"uiVersion,omitempty"`
+ KernelVersion string `protobuf:"bytes,9,opt,name=kernelVersion,proto3" json:"kernelVersion,omitempty"`
+ OSVersion string `protobuf:"bytes,10,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"`
+ NetworkAddresses []*NetworkAddress `protobuf:"bytes,11,rep,name=networkAddresses,proto3" json:"networkAddresses,omitempty"`
+ SysSerialNumber string `protobuf:"bytes,12,opt,name=sysSerialNumber,proto3" json:"sysSerialNumber,omitempty"`
+ SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"`
+ SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"`
+ Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"`
+ Files []*File `protobuf:"bytes,16,rep,name=files,proto3" json:"files,omitempty"`
+ Flags *Flags `protobuf:"bytes,17,opt,name=flags,proto3" json:"flags,omitempty"`
}
func (x *PeerSystemMeta) Reset() {
*x = PeerSystemMeta{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[8]
+ mi := &file_management_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -824,7 +928,7 @@ func (x *PeerSystemMeta) String() string {
func (*PeerSystemMeta) ProtoMessage() {}
func (x *PeerSystemMeta) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[8]
+ mi := &file_management_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -837,7 +941,7 @@ func (x *PeerSystemMeta) ProtoReflect() protoreflect.Message {
// Deprecated: Use PeerSystemMeta.ProtoReflect.Descriptor instead.
func (*PeerSystemMeta) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{8}
+ return file_management_proto_rawDescGZIP(), []int{9}
}
func (x *PeerSystemMeta) GetHostname() string {
@@ -882,9 +986,9 @@ func (x *PeerSystemMeta) GetOS() string {
return ""
}
-func (x *PeerSystemMeta) GetWiretrusteeVersion() string {
+func (x *PeerSystemMeta) GetNetbirdVersion() string {
if x != nil {
- return x.WiretrusteeVersion
+ return x.NetbirdVersion
}
return ""
}
@@ -952,13 +1056,20 @@ func (x *PeerSystemMeta) GetFiles() []*File {
return nil
}
+func (x *PeerSystemMeta) GetFlags() *Flags {
+ if x != nil {
+ return x.Flags
+ }
+ return nil
+}
+
type LoginResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Global config
- WiretrusteeConfig *WiretrusteeConfig `protobuf:"bytes,1,opt,name=wiretrusteeConfig,proto3" json:"wiretrusteeConfig,omitempty"`
+ NetbirdConfig *NetbirdConfig `protobuf:"bytes,1,opt,name=netbirdConfig,proto3" json:"netbirdConfig,omitempty"`
// Peer local config
PeerConfig *PeerConfig `protobuf:"bytes,2,opt,name=peerConfig,proto3" json:"peerConfig,omitempty"`
// Posture checks to be evaluated by client
@@ -968,7 +1079,7 @@ type LoginResponse struct {
func (x *LoginResponse) Reset() {
*x = LoginResponse{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[9]
+ mi := &file_management_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -981,7 +1092,7 @@ func (x *LoginResponse) String() string {
func (*LoginResponse) ProtoMessage() {}
func (x *LoginResponse) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[9]
+ mi := &file_management_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -994,12 +1105,12 @@ func (x *LoginResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead.
func (*LoginResponse) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{9}
+ return file_management_proto_rawDescGZIP(), []int{10}
}
-func (x *LoginResponse) GetWiretrusteeConfig() *WiretrusteeConfig {
+func (x *LoginResponse) GetNetbirdConfig() *NetbirdConfig {
if x != nil {
- return x.WiretrusteeConfig
+ return x.NetbirdConfig
}
return nil
}
@@ -1027,14 +1138,14 @@ type ServerKeyResponse struct {
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// Key expiration timestamp after which the key should be fetched again by the client
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"`
- // Version of the Wiretrustee Management Service protocol
+ // Version of the Netbird Management Service protocol
Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"`
}
func (x *ServerKeyResponse) Reset() {
*x = ServerKeyResponse{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[10]
+ mi := &file_management_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1047,7 +1158,7 @@ func (x *ServerKeyResponse) String() string {
func (*ServerKeyResponse) ProtoMessage() {}
func (x *ServerKeyResponse) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[10]
+ mi := &file_management_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1060,7 +1171,7 @@ func (x *ServerKeyResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServerKeyResponse.ProtoReflect.Descriptor instead.
func (*ServerKeyResponse) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{10}
+ return file_management_proto_rawDescGZIP(), []int{11}
}
func (x *ServerKeyResponse) GetKey() string {
@@ -1093,7 +1204,7 @@ type Empty struct {
func (x *Empty) Reset() {
*x = Empty{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[11]
+ mi := &file_management_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1106,7 +1217,7 @@ func (x *Empty) String() string {
func (*Empty) ProtoMessage() {}
func (x *Empty) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[11]
+ mi := &file_management_proto_msgTypes[12]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1119,11 +1230,11 @@ func (x *Empty) ProtoReflect() protoreflect.Message {
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
func (*Empty) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{11}
+ return file_management_proto_rawDescGZIP(), []int{12}
}
-// WiretrusteeConfig is a common configuration of any Wiretrustee peer. It contains STUN, TURN, Signal and Management servers configurations
-type WiretrusteeConfig struct {
+// NetbirdConfig is a common configuration of any Netbird peer. It contains STUN, TURN, Signal and Management servers configurations
+type NetbirdConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
@@ -1137,23 +1248,23 @@ type WiretrusteeConfig struct {
Relay *RelayConfig `protobuf:"bytes,4,opt,name=relay,proto3" json:"relay,omitempty"`
}
-func (x *WiretrusteeConfig) Reset() {
- *x = WiretrusteeConfig{}
+func (x *NetbirdConfig) Reset() {
+ *x = NetbirdConfig{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[12]
+ mi := &file_management_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *WiretrusteeConfig) String() string {
+func (x *NetbirdConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*WiretrusteeConfig) ProtoMessage() {}
+func (*NetbirdConfig) ProtoMessage() {}
-func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[12]
+func (x *NetbirdConfig) ProtoReflect() protoreflect.Message {
+ mi := &file_management_proto_msgTypes[13]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1164,33 +1275,33 @@ func (x *WiretrusteeConfig) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use WiretrusteeConfig.ProtoReflect.Descriptor instead.
-func (*WiretrusteeConfig) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{12}
+// Deprecated: Use NetbirdConfig.ProtoReflect.Descriptor instead.
+func (*NetbirdConfig) Descriptor() ([]byte, []int) {
+ return file_management_proto_rawDescGZIP(), []int{13}
}
-func (x *WiretrusteeConfig) GetStuns() []*HostConfig {
+func (x *NetbirdConfig) GetStuns() []*HostConfig {
if x != nil {
return x.Stuns
}
return nil
}
-func (x *WiretrusteeConfig) GetTurns() []*ProtectedHostConfig {
+func (x *NetbirdConfig) GetTurns() []*ProtectedHostConfig {
if x != nil {
return x.Turns
}
return nil
}
-func (x *WiretrusteeConfig) GetSignal() *HostConfig {
+func (x *NetbirdConfig) GetSignal() *HostConfig {
if x != nil {
return x.Signal
}
return nil
}
-func (x *WiretrusteeConfig) GetRelay() *RelayConfig {
+func (x *NetbirdConfig) GetRelay() *RelayConfig {
if x != nil {
return x.Relay
}
@@ -1203,7 +1314,7 @@ type HostConfig struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- // URI of the resource e.g. turns://stun.wiretrustee.com:4430 or signal.wiretrustee.com:10000
+ // URI of the resource e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000
Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"`
Protocol HostConfig_Protocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=management.HostConfig_Protocol" json:"protocol,omitempty"`
}
@@ -1211,7 +1322,7 @@ type HostConfig struct {
func (x *HostConfig) Reset() {
*x = HostConfig{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[13]
+ mi := &file_management_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1224,7 +1335,7 @@ func (x *HostConfig) String() string {
func (*HostConfig) ProtoMessage() {}
func (x *HostConfig) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[13]
+ mi := &file_management_proto_msgTypes[14]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1237,7 +1348,7 @@ func (x *HostConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use HostConfig.ProtoReflect.Descriptor instead.
func (*HostConfig) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{13}
+ return file_management_proto_rawDescGZIP(), []int{14}
}
func (x *HostConfig) GetUri() string {
@@ -1267,7 +1378,7 @@ type RelayConfig struct {
func (x *RelayConfig) Reset() {
*x = RelayConfig{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[14]
+ mi := &file_management_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1280,7 +1391,7 @@ func (x *RelayConfig) String() string {
func (*RelayConfig) ProtoMessage() {}
func (x *RelayConfig) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[14]
+ mi := &file_management_proto_msgTypes[15]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1293,7 +1404,7 @@ func (x *RelayConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use RelayConfig.ProtoReflect.Descriptor instead.
func (*RelayConfig) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{14}
+ return file_management_proto_rawDescGZIP(), []int{15}
}
func (x *RelayConfig) GetUrls() []string {
@@ -1332,7 +1443,7 @@ type ProtectedHostConfig struct {
func (x *ProtectedHostConfig) Reset() {
*x = ProtectedHostConfig{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[15]
+ mi := &file_management_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1345,7 +1456,7 @@ func (x *ProtectedHostConfig) String() string {
func (*ProtectedHostConfig) ProtoMessage() {}
func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[15]
+ mi := &file_management_proto_msgTypes[16]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1358,7 +1469,7 @@ func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProtectedHostConfig.ProtoReflect.Descriptor instead.
func (*ProtectedHostConfig) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{15}
+ return file_management_proto_rawDescGZIP(), []int{16}
}
func (x *ProtectedHostConfig) GetHostConfig() *HostConfig {
@@ -1389,9 +1500,9 @@ type PeerConfig struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- // Peer's virtual IP address within the Wiretrustee VPN (a Wireguard address config)
+ // Peer's virtual IP address within the Netbird VPN (a Wireguard address config)
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
- // Wiretrustee DNS server (a Wireguard DNS config)
+ // Netbird DNS server (a Wireguard DNS config)
Dns string `protobuf:"bytes,2,opt,name=dns,proto3" json:"dns,omitempty"`
// SSHConfig of the peer.
SshConfig *SSHConfig `protobuf:"bytes,3,opt,name=sshConfig,proto3" json:"sshConfig,omitempty"`
@@ -1403,7 +1514,7 @@ type PeerConfig struct {
func (x *PeerConfig) Reset() {
*x = PeerConfig{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[16]
+ mi := &file_management_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1416,7 +1527,7 @@ func (x *PeerConfig) String() string {
func (*PeerConfig) ProtoMessage() {}
func (x *PeerConfig) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[16]
+ mi := &file_management_proto_msgTypes[17]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1429,7 +1540,7 @@ func (x *PeerConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use PeerConfig.ProtoReflect.Descriptor instead.
func (*PeerConfig) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{16}
+ return file_management_proto_rawDescGZIP(), []int{17}
}
func (x *PeerConfig) GetAddress() string {
@@ -1502,7 +1613,7 @@ type NetworkMap struct {
func (x *NetworkMap) Reset() {
*x = NetworkMap{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[17]
+ mi := &file_management_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1515,7 +1626,7 @@ func (x *NetworkMap) String() string {
func (*NetworkMap) ProtoMessage() {}
func (x *NetworkMap) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[17]
+ mi := &file_management_proto_msgTypes[18]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1528,7 +1639,7 @@ func (x *NetworkMap) ProtoReflect() protoreflect.Message {
// Deprecated: Use NetworkMap.ProtoReflect.Descriptor instead.
func (*NetworkMap) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{17}
+ return file_management_proto_rawDescGZIP(), []int{18}
}
func (x *NetworkMap) GetSerial() uint64 {
@@ -1628,7 +1739,7 @@ type RemotePeerConfig struct {
func (x *RemotePeerConfig) Reset() {
*x = RemotePeerConfig{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[18]
+ mi := &file_management_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1641,7 +1752,7 @@ func (x *RemotePeerConfig) String() string {
func (*RemotePeerConfig) ProtoMessage() {}
func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[18]
+ mi := &file_management_proto_msgTypes[19]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1654,7 +1765,7 @@ func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use RemotePeerConfig.ProtoReflect.Descriptor instead.
func (*RemotePeerConfig) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{18}
+ return file_management_proto_rawDescGZIP(), []int{19}
}
func (x *RemotePeerConfig) GetWgPubKey() string {
@@ -1701,7 +1812,7 @@ type SSHConfig struct {
func (x *SSHConfig) Reset() {
*x = SSHConfig{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[19]
+ mi := &file_management_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1714,7 +1825,7 @@ func (x *SSHConfig) String() string {
func (*SSHConfig) ProtoMessage() {}
func (x *SSHConfig) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[19]
+ mi := &file_management_proto_msgTypes[20]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1727,7 +1838,7 @@ func (x *SSHConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use SSHConfig.ProtoReflect.Descriptor instead.
func (*SSHConfig) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{19}
+ return file_management_proto_rawDescGZIP(), []int{20}
}
func (x *SSHConfig) GetSshEnabled() bool {
@@ -1754,7 +1865,7 @@ type DeviceAuthorizationFlowRequest struct {
func (x *DeviceAuthorizationFlowRequest) Reset() {
*x = DeviceAuthorizationFlowRequest{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[20]
+ mi := &file_management_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1767,7 +1878,7 @@ func (x *DeviceAuthorizationFlowRequest) String() string {
func (*DeviceAuthorizationFlowRequest) ProtoMessage() {}
func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[20]
+ mi := &file_management_proto_msgTypes[21]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1780,7 +1891,7 @@ func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeviceAuthorizationFlowRequest.ProtoReflect.Descriptor instead.
func (*DeviceAuthorizationFlowRequest) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{20}
+ return file_management_proto_rawDescGZIP(), []int{21}
}
// DeviceAuthorizationFlow represents Device Authorization Flow information
@@ -1799,7 +1910,7 @@ type DeviceAuthorizationFlow struct {
func (x *DeviceAuthorizationFlow) Reset() {
*x = DeviceAuthorizationFlow{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[21]
+ mi := &file_management_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1812,7 +1923,7 @@ func (x *DeviceAuthorizationFlow) String() string {
func (*DeviceAuthorizationFlow) ProtoMessage() {}
func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[21]
+ mi := &file_management_proto_msgTypes[22]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1825,7 +1936,7 @@ func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeviceAuthorizationFlow.ProtoReflect.Descriptor instead.
func (*DeviceAuthorizationFlow) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{21}
+ return file_management_proto_rawDescGZIP(), []int{22}
}
func (x *DeviceAuthorizationFlow) GetProvider() DeviceAuthorizationFlowProvider {
@@ -1852,7 +1963,7 @@ type PKCEAuthorizationFlowRequest struct {
func (x *PKCEAuthorizationFlowRequest) Reset() {
*x = PKCEAuthorizationFlowRequest{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[22]
+ mi := &file_management_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1865,7 +1976,7 @@ func (x *PKCEAuthorizationFlowRequest) String() string {
func (*PKCEAuthorizationFlowRequest) ProtoMessage() {}
func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[22]
+ mi := &file_management_proto_msgTypes[23]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1878,7 +1989,7 @@ func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PKCEAuthorizationFlowRequest.ProtoReflect.Descriptor instead.
func (*PKCEAuthorizationFlowRequest) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{22}
+ return file_management_proto_rawDescGZIP(), []int{23}
}
// PKCEAuthorizationFlow represents Authorization Code Flow information
@@ -1895,7 +2006,7 @@ type PKCEAuthorizationFlow struct {
func (x *PKCEAuthorizationFlow) Reset() {
*x = PKCEAuthorizationFlow{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[23]
+ mi := &file_management_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1908,7 +2019,7 @@ func (x *PKCEAuthorizationFlow) String() string {
func (*PKCEAuthorizationFlow) ProtoMessage() {}
func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[23]
+ mi := &file_management_proto_msgTypes[24]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1921,7 +2032,7 @@ func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message {
// Deprecated: Use PKCEAuthorizationFlow.ProtoReflect.Descriptor instead.
func (*PKCEAuthorizationFlow) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{23}
+ return file_management_proto_rawDescGZIP(), []int{24}
}
func (x *PKCEAuthorizationFlow) GetProviderConfig() *ProviderConfig {
@@ -1963,7 +2074,7 @@ type ProviderConfig struct {
func (x *ProviderConfig) Reset() {
*x = ProviderConfig{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[24]
+ mi := &file_management_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1976,7 +2087,7 @@ func (x *ProviderConfig) String() string {
func (*ProviderConfig) ProtoMessage() {}
func (x *ProviderConfig) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[24]
+ mi := &file_management_proto_msgTypes[25]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1989,7 +2100,7 @@ func (x *ProviderConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProviderConfig.ProtoReflect.Descriptor instead.
func (*ProviderConfig) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{24}
+ return file_management_proto_rawDescGZIP(), []int{25}
}
func (x *ProviderConfig) GetClientID() string {
@@ -2082,7 +2193,7 @@ type Route struct {
func (x *Route) Reset() {
*x = Route{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[25]
+ mi := &file_management_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2095,7 +2206,7 @@ func (x *Route) String() string {
func (*Route) ProtoMessage() {}
func (x *Route) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[25]
+ mi := &file_management_proto_msgTypes[26]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2108,7 +2219,7 @@ func (x *Route) ProtoReflect() protoreflect.Message {
// Deprecated: Use Route.ProtoReflect.Descriptor instead.
func (*Route) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{25}
+ return file_management_proto_rawDescGZIP(), []int{26}
}
func (x *Route) GetID() string {
@@ -2188,7 +2299,7 @@ type DNSConfig struct {
func (x *DNSConfig) Reset() {
*x = DNSConfig{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[26]
+ mi := &file_management_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2201,7 +2312,7 @@ func (x *DNSConfig) String() string {
func (*DNSConfig) ProtoMessage() {}
func (x *DNSConfig) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[26]
+ mi := &file_management_proto_msgTypes[27]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2214,7 +2325,7 @@ func (x *DNSConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use DNSConfig.ProtoReflect.Descriptor instead.
func (*DNSConfig) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{26}
+ return file_management_proto_rawDescGZIP(), []int{27}
}
func (x *DNSConfig) GetServiceEnable() bool {
@@ -2251,7 +2362,7 @@ type CustomZone struct {
func (x *CustomZone) Reset() {
*x = CustomZone{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[27]
+ mi := &file_management_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2264,7 +2375,7 @@ func (x *CustomZone) String() string {
func (*CustomZone) ProtoMessage() {}
func (x *CustomZone) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[27]
+ mi := &file_management_proto_msgTypes[28]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2277,7 +2388,7 @@ func (x *CustomZone) ProtoReflect() protoreflect.Message {
// Deprecated: Use CustomZone.ProtoReflect.Descriptor instead.
func (*CustomZone) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{27}
+ return file_management_proto_rawDescGZIP(), []int{28}
}
func (x *CustomZone) GetDomain() string {
@@ -2310,7 +2421,7 @@ type SimpleRecord struct {
func (x *SimpleRecord) Reset() {
*x = SimpleRecord{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[28]
+ mi := &file_management_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2323,7 +2434,7 @@ func (x *SimpleRecord) String() string {
func (*SimpleRecord) ProtoMessage() {}
func (x *SimpleRecord) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[28]
+ mi := &file_management_proto_msgTypes[29]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2336,7 +2447,7 @@ func (x *SimpleRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use SimpleRecord.ProtoReflect.Descriptor instead.
func (*SimpleRecord) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{28}
+ return file_management_proto_rawDescGZIP(), []int{29}
}
func (x *SimpleRecord) GetName() string {
@@ -2389,7 +2500,7 @@ type NameServerGroup struct {
func (x *NameServerGroup) Reset() {
*x = NameServerGroup{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[29]
+ mi := &file_management_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2402,7 +2513,7 @@ func (x *NameServerGroup) String() string {
func (*NameServerGroup) ProtoMessage() {}
func (x *NameServerGroup) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[29]
+ mi := &file_management_proto_msgTypes[30]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2415,7 +2526,7 @@ func (x *NameServerGroup) ProtoReflect() protoreflect.Message {
// Deprecated: Use NameServerGroup.ProtoReflect.Descriptor instead.
func (*NameServerGroup) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{29}
+ return file_management_proto_rawDescGZIP(), []int{30}
}
func (x *NameServerGroup) GetNameServers() []*NameServer {
@@ -2460,7 +2571,7 @@ type NameServer struct {
func (x *NameServer) Reset() {
*x = NameServer{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[30]
+ mi := &file_management_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2473,7 +2584,7 @@ func (x *NameServer) String() string {
func (*NameServer) ProtoMessage() {}
func (x *NameServer) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[30]
+ mi := &file_management_proto_msgTypes[31]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2486,7 +2597,7 @@ func (x *NameServer) ProtoReflect() protoreflect.Message {
// Deprecated: Use NameServer.ProtoReflect.Descriptor instead.
func (*NameServer) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{30}
+ return file_management_proto_rawDescGZIP(), []int{31}
}
func (x *NameServer) GetIP() string {
@@ -2521,12 +2632,13 @@ type FirewallRule struct {
Action RuleAction `protobuf:"varint,3,opt,name=Action,proto3,enum=management.RuleAction" json:"Action,omitempty"`
Protocol RuleProtocol `protobuf:"varint,4,opt,name=Protocol,proto3,enum=management.RuleProtocol" json:"Protocol,omitempty"`
Port string `protobuf:"bytes,5,opt,name=Port,proto3" json:"Port,omitempty"`
+ PortInfo *PortInfo `protobuf:"bytes,6,opt,name=PortInfo,proto3" json:"PortInfo,omitempty"`
}
func (x *FirewallRule) Reset() {
*x = FirewallRule{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[31]
+ mi := &file_management_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2539,7 +2651,7 @@ func (x *FirewallRule) String() string {
func (*FirewallRule) ProtoMessage() {}
func (x *FirewallRule) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[31]
+ mi := &file_management_proto_msgTypes[32]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2552,7 +2664,7 @@ func (x *FirewallRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use FirewallRule.ProtoReflect.Descriptor instead.
func (*FirewallRule) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{31}
+ return file_management_proto_rawDescGZIP(), []int{32}
}
func (x *FirewallRule) GetPeerIP() string {
@@ -2590,6 +2702,13 @@ func (x *FirewallRule) GetPort() string {
return ""
}
+func (x *FirewallRule) GetPortInfo() *PortInfo {
+ if x != nil {
+ return x.PortInfo
+ }
+ return nil
+}
+
type NetworkAddress struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -2602,7 +2721,7 @@ type NetworkAddress struct {
func (x *NetworkAddress) Reset() {
*x = NetworkAddress{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[32]
+ mi := &file_management_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2615,7 +2734,7 @@ func (x *NetworkAddress) String() string {
func (*NetworkAddress) ProtoMessage() {}
func (x *NetworkAddress) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[32]
+ mi := &file_management_proto_msgTypes[33]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2628,7 +2747,7 @@ func (x *NetworkAddress) ProtoReflect() protoreflect.Message {
// Deprecated: Use NetworkAddress.ProtoReflect.Descriptor instead.
func (*NetworkAddress) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{32}
+ return file_management_proto_rawDescGZIP(), []int{33}
}
func (x *NetworkAddress) GetNetIP() string {
@@ -2656,7 +2775,7 @@ type Checks struct {
func (x *Checks) Reset() {
*x = Checks{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[33]
+ mi := &file_management_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2669,7 +2788,7 @@ func (x *Checks) String() string {
func (*Checks) ProtoMessage() {}
func (x *Checks) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[33]
+ mi := &file_management_proto_msgTypes[34]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2682,7 +2801,7 @@ func (x *Checks) ProtoReflect() protoreflect.Message {
// Deprecated: Use Checks.ProtoReflect.Descriptor instead.
func (*Checks) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{33}
+ return file_management_proto_rawDescGZIP(), []int{34}
}
func (x *Checks) GetFiles() []string {
@@ -2707,7 +2826,7 @@ type PortInfo struct {
func (x *PortInfo) Reset() {
*x = PortInfo{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[34]
+ mi := &file_management_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2720,7 +2839,7 @@ func (x *PortInfo) String() string {
func (*PortInfo) ProtoMessage() {}
func (x *PortInfo) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[34]
+ mi := &file_management_proto_msgTypes[35]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2733,7 +2852,7 @@ func (x *PortInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use PortInfo.ProtoReflect.Descriptor instead.
func (*PortInfo) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{34}
+ return file_management_proto_rawDescGZIP(), []int{35}
}
func (m *PortInfo) GetPortSelection() isPortInfo_PortSelection {
@@ -2800,7 +2919,7 @@ type RouteFirewallRule struct {
func (x *RouteFirewallRule) Reset() {
*x = RouteFirewallRule{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[35]
+ mi := &file_management_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2813,7 +2932,7 @@ func (x *RouteFirewallRule) String() string {
func (*RouteFirewallRule) ProtoMessage() {}
func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[35]
+ mi := &file_management_proto_msgTypes[36]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2826,7 +2945,7 @@ func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use RouteFirewallRule.ProtoReflect.Descriptor instead.
func (*RouteFirewallRule) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{35}
+ return file_management_proto_rawDescGZIP(), []int{36}
}
func (x *RouteFirewallRule) GetSourceRanges() []string {
@@ -2897,7 +3016,7 @@ type PortInfo_Range struct {
func (x *PortInfo_Range) Reset() {
*x = PortInfo_Range{}
if protoimpl.UnsafeEnabled {
- mi := &file_management_proto_msgTypes[36]
+ mi := &file_management_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2910,7 +3029,7 @@ func (x *PortInfo_Range) String() string {
func (*PortInfo_Range) ProtoMessage() {}
func (x *PortInfo_Range) ProtoReflect() protoreflect.Message {
- mi := &file_management_proto_msgTypes[36]
+ mi := &file_management_proto_msgTypes[37]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2923,7 +3042,7 @@ func (x *PortInfo_Range) ProtoReflect() protoreflect.Message {
// Deprecated: Use PortInfo_Range.ProtoReflect.Descriptor instead.
func (*PortInfo_Range) Descriptor() ([]byte, []int) {
- return file_management_proto_rawDescGZIP(), []int{34, 0}
+ return file_management_proto_rawDescGZIP(), []int{35, 0}
}
func (x *PortInfo_Range) GetStart() uint32 {
@@ -2956,416 +3075,441 @@ var file_management_proto_rawDesc = []byte{
0x0b, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04,
0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74,
- 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xe7, 0x02, 0x0a,
- 0x0c, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a,
- 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66,
- 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
- 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65,
- 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75,
- 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65,
- 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16,
- 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72,
- 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66,
- 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72,
- 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
- 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43,
- 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65,
- 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72,
- 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12,
- 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70,
- 0x74, 0x79, 0x12, 0x36, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70,
- 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
- 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x52, 0x0a,
- 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x2a, 0x0a, 0x06, 0x43, 0x68,
- 0x65, 0x63, 0x6b, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x61, 0x6e,
- 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x52, 0x06,
- 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x41, 0x0a, 0x0f, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65,
- 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74,
- 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
- 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d,
- 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xa8, 0x01, 0x0a, 0x0c, 0x4c, 0x6f,
- 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65,
- 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65,
- 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
- 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61,
- 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x77, 0x74, 0x54, 0x6f, 0x6b,
- 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x77, 0x74, 0x54, 0x6f, 0x6b,
- 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x18, 0x04,
- 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
- 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72,
- 0x4b, 0x65, 0x79, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73,
- 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1a,
- 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
- 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x0b, 0x45, 0x6e,
- 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6c, 0x6f,
- 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x12,
- 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5c, 0x0a, 0x04, 0x46,
- 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12, 0x2a, 0x0a,
- 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e,
- 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73,
- 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xd1, 0x04, 0x0a, 0x0e, 0x50, 0x65,
- 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08,
- 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
- 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06,
- 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x65,
- 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74,
- 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74,
- 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x02, 0x4f, 0x53, 0x12, 0x2e, 0x0a, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73,
- 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, 0x65, 0x72,
- 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
- 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69,
- 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73,
- 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65,
- 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65,
- 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56,
- 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
- 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b,
- 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65,
- 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65,
- 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28,
- 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65,
- 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69,
- 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50,
- 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65,
- 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75,
- 0x72, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61,
- 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e,
- 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32,
- 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76,
- 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f,
- 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10,
- 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
- 0x74, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0xc0, 0x01,
- 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
- 0x4b, 0x0a, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f,
- 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e,
- 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73,
- 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74,
- 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a,
- 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
- 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65,
- 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f,
- 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03,
- 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
- 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73,
- 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72,
- 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
- 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
- 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41,
- 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01,
- 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45,
- 0x6d, 0x70, 0x74, 0x79, 0x22, 0xd7, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75,
- 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74,
- 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
- 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
- 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e,
- 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
- 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f,
- 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12,
- 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
- 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73,
- 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x12,
- 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17,
- 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6c, 0x61,
- 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x98,
- 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a,
- 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12,
- 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28,
- 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48,
- 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
- 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08,
- 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10,
- 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54,
- 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12,
- 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, 0x6c,
- 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73,
- 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, 0x0c,
- 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
- 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75,
- 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53,
- 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74,
- 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
- 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
- 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73,
- 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70,
- 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70,
- 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72,
- 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73,
- 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
- 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64,
- 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18,
- 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
- 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73,
- 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18,
- 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52,
- 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73,
- 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05,
- 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65,
- 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e,
- 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
- 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a,
- 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
- 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65,
- 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f,
- 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65,
- 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
- 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65,
- 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50,
- 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65,
- 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08,
- 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45,
- 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05,
- 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
- 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12,
- 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
- 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f,
- 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50,
- 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e,
- 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65,
- 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e,
- 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61,
- 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e,
- 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77,
- 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c,
- 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61,
- 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09,
- 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75,
- 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f,
- 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65,
- 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
- 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61,
- 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69,
- 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72,
- 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c,
- 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52,
- 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52,
- 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10,
- 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
- 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a,
- 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
- 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09,
- 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
- 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48,
- 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69,
- 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66,
- 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c,
- 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79,
- 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
- 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74,
- 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48,
- 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
- 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65,
- 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
- 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08,
- 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76,
- 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
- 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72,
- 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72,
- 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08,
- 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54,
- 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68,
- 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68,
- 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a,
+ 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xdb, 0x02, 0x0a,
+ 0x0c, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a,
+ 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
+ 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
+ 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36,
+ 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
+ 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72,
+ 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65,
+ 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61,
+ 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50,
+ 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74,
+ 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65,
+ 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49,
+ 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
+ 0x6b, 0x4d, 0x61, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e,
+ 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d,
+ 0x61, 0x70, 0x52, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x2a,
+ 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12,
+ 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63,
+ 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x41, 0x0a, 0x0f, 0x53, 0x79,
+ 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a,
+ 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61,
+ 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73,
+ 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xc6, 0x01,
+ 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a,
+ 0x0a, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65,
+ 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d,
+ 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x77,
+ 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x77,
+ 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65,
+ 0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x08,
+ 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x4c,
+ 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73,
+ 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65,
+ 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79,
+ 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x0b,
+ 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63,
+ 0x6c, 0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75,
+ 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5c, 0x0a,
+ 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78, 0x69,
+ 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12,
+ 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e,
+ 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65,
+ 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x02, 0x0a, 0x05,
+ 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61,
+ 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
+ 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65,
+ 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65,
+ 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13,
+ 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
+ 0x69, 0x76, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48,
+ 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73,
+ 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12,
+ 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
+ 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69,
+ 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65,
+ 0x73, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76,
+ 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13,
+ 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75,
+ 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e,
+ 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
+ 0x44, 0x4e, 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69,
+ 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69,
+ 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x22, 0xf2, 0x04,
+ 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61,
+ 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04,
+ 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53,
+ 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65,
+ 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08,
+ 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
+ 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x62,
+ 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
+ 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24,
+ 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
+ 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72,
+ 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
+ 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69,
+ 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64,
+ 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d,
+ 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
+ 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
+ 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79,
+ 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75,
+ 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75,
+ 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79,
+ 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f,
+ 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18,
+ 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61,
+ 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f,
+ 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61,
+ 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e,
+ 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e,
+ 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b,
+ 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69,
+ 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61,
+ 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61,
+ 0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43,
+ 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61,
+ 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64,
+ 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43,
+ 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e,
+ 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
+ 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
+ 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a,
+ 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
+ 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b,
+ 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72,
+ 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10,
+ 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
+ 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
+ 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65,
+ 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72,
+ 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xd3, 0x01,
+ 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
+ 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
+ 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74,
+ 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a,
+ 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d,
+ 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63,
+ 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74,
+ 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
+ 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69,
+ 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
+ 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65,
+ 0x6c, 0x61, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
+ 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
+ 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50,
+ 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
+ 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a,
+ 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12,
+ 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54,
+ 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d,
+ 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a,
+ 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c,
+ 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61,
+ 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61,
+ 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69,
+ 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74,
+ 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x7d, 0x0a,
+ 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f,
+ 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
+ 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+ 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04,
+ 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72,
+ 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a,
+ 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61,
+ 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64,
+ 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f,
+ 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e,
+ 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69,
+ 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04,
+ 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e,
+ 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44,
+ 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62,
+ 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69,
+ 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74,
+ 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xf3, 0x04, 0x0a, 0x0a, 0x4e,
+ 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72,
+ 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61,
+ 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
+ 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70,
+ 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d,
+ 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c,
+ 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f,
+ 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65,
+ 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d,
+ 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18,
+ 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65,
+ 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75,
+ 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
+ 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f,
+ 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69,
+ 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
+ 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09,
+ 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66,
+ 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32,
+ 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d,
+ 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f,
+ 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46,
+ 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03,
+ 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
+ 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69,
+ 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66,
+ 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d,
+ 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77,
+ 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
+ 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c,
+ 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d,
+ 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46,
+ 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75,
+ 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73,
+ 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61,
+ 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b,
+ 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65,
+ 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79,
+ 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43,
+ 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65,
+ 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65,
+ 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18,
+ 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70,
+ 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
+ 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68,
+ 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53,
+ 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e,
+ 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68,
+ 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75,
+ 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50,
+ 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41,
+ 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69,
+ 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46,
+ 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
+ 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
+ 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
+ 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a,
0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
+ 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
- 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f,
- 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44,
- 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65,
- 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08,
- 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
- 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69,
- 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68,
- 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65,
- 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14,
- 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53,
- 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b,
- 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54,
- 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
- 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69,
- 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65,
- 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09,
- 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xed,
- 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77,
- 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f,
- 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70,
- 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
- 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72,
- 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63,
- 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06,
- 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65,
- 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
- 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73,
- 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20,
- 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4,
- 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d,
- 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62,
- 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
- 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d,
- 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65,
- 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53,
- 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43,
- 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
- 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75,
- 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d,
- 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a,
- 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52,
- 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d,
- 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65,
- 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22,
- 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12,
- 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e,
- 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
- 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73,
- 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a,
- 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12,
- 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
- 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65,
- 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d,
- 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
- 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65,
- 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76,
- 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a,
- 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
- 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63,
- 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18,
- 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d,
- 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e,
- 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54,
- 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70,
- 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
- 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xd9, 0x01, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61,
- 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37,
- 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28,
- 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52,
- 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69,
- 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f,
- 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
- 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
- 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f,
+ 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a,
+ 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43,
+ 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
+ 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43,
+ 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
+ 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f,
+ 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e,
+ 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
+ 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
+ 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69,
+ 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69,
+ 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69,
+ 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53,
+ 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69,
+ 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d,
+ 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69,
+ 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a,
+ 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f,
+ 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63,
+ 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a,
+ 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f,
+ 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65,
+ 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55,
+ 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74,
+ 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69,
+ 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
+ 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
+ 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18,
+ 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55,
+ 0x52, 0x4c, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a,
+ 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a,
+ 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
+ 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f,
+ 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65,
+ 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65,
+ 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a,
+ 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d,
+ 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72,
+ 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75,
+ 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44,
+ 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f,
+ 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75,
+ 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f,
+ 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69,
+ 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62,
+ 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+ 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53,
+ 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
+ 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e,
+ 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10,
+ 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73,
+ 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18,
+ 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
+ 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43,
+ 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75,
+ 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61,
+ 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
+ 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
+ 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53,
+ 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63,
+ 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65,
+ 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05,
+ 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61,
+ 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
+ 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e,
+ 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38,
+ 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20,
+ 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
+ 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d,
+ 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d,
+ 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61,
+ 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20,
+ 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14,
+ 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61,
+ 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72,
+ 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
+ 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e,
+ 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16,
+ 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
+ 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x02, 0x0a, 0x0c, 0x46,
+ 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50,
+ 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65,
+ 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
+ 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06,
+ 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d,
+ 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63,
+ 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08,
+ 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18,
+ 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65,
+ 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
+ 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e,
+ 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08,
+ 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77,
+ 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65,
+ 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50,
+ 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d,
+ 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05,
+ 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c,
+ 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12,
+ 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52,
+ 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
+ 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65,
+ 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e,
+ 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f,
+ 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11,
+ 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c,
+ 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65,
+ 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52,
+ 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
+ 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
+ 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74,
+ 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f,
- 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a,
- 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72,
- 0x74, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72,
- 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43,
- 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01,
- 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08,
- 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32,
- 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
- 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49,
- 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e,
- 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73,
- 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72,
- 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03,
- 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63,
- 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xd1, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69,
- 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09,
- 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e,
- 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16,
- 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65,
- 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20,
- 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e,
- 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
- 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72,
- 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e,
- 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
- 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08,
- 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79,
- 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44,
- 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
- 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73,
- 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
- 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
- 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65,
- 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e,
- 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07,
- 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03,
- 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55,
- 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69,
- 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12,
- 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65,
- 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54,
- 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a,
- 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
- 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61,
- 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
- 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
- 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
- 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e,
- 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
- 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a,
- 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63,
- 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30,
- 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65,
- 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
- 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
- 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74,
- 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
- 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
- 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65,
- 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
- 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
- 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d,
- 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
- 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73,
- 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43,
- 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
- 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
+ 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a,
+ 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72,
+ 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12,
+ 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01,
+ 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a,
+ 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
+ 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f,
+ 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52,
+ 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a,
+ 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
+ 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03,
+ 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07,
+ 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10,
+ 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a,
+ 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06,
+ 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a,
+ 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a,
+ 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f,
+ 0x50, 0x10, 0x01, 0x32, 0x90, 0x04, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
+ 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67,
+ 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e,
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00,
- 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d,
- 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
- 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e,
- 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42,
- 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
- 0x33,
+ 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d,
+ 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
+ 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73,
+ 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53,
+ 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+ 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61,
+ 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b,
+ 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09,
+ 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
+ 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d,
+ 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
+ 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75,
+ 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12,
+ 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63,
+ 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e,
+ 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79,
+ 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a,
+ 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
+ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
+ 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
+ 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
+ 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65,
+ 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d,
+ 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
+ 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
+ 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
+ 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -3381,7 +3525,7 @@ func file_management_proto_rawDescGZIP() []byte {
}
var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
-var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 37)
+var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 38)
var file_management_proto_goTypes = []interface{}{
(RuleProtocol)(0), // 0: management.RuleProtocol
(RuleDirection)(0), // 1: management.RuleDirection
@@ -3396,102 +3540,105 @@ var file_management_proto_goTypes = []interface{}{
(*PeerKeys)(nil), // 10: management.PeerKeys
(*Environment)(nil), // 11: management.Environment
(*File)(nil), // 12: management.File
- (*PeerSystemMeta)(nil), // 13: management.PeerSystemMeta
- (*LoginResponse)(nil), // 14: management.LoginResponse
- (*ServerKeyResponse)(nil), // 15: management.ServerKeyResponse
- (*Empty)(nil), // 16: management.Empty
- (*WiretrusteeConfig)(nil), // 17: management.WiretrusteeConfig
- (*HostConfig)(nil), // 18: management.HostConfig
- (*RelayConfig)(nil), // 19: management.RelayConfig
- (*ProtectedHostConfig)(nil), // 20: management.ProtectedHostConfig
- (*PeerConfig)(nil), // 21: management.PeerConfig
- (*NetworkMap)(nil), // 22: management.NetworkMap
- (*RemotePeerConfig)(nil), // 23: management.RemotePeerConfig
- (*SSHConfig)(nil), // 24: management.SSHConfig
- (*DeviceAuthorizationFlowRequest)(nil), // 25: management.DeviceAuthorizationFlowRequest
- (*DeviceAuthorizationFlow)(nil), // 26: management.DeviceAuthorizationFlow
- (*PKCEAuthorizationFlowRequest)(nil), // 27: management.PKCEAuthorizationFlowRequest
- (*PKCEAuthorizationFlow)(nil), // 28: management.PKCEAuthorizationFlow
- (*ProviderConfig)(nil), // 29: management.ProviderConfig
- (*Route)(nil), // 30: management.Route
- (*DNSConfig)(nil), // 31: management.DNSConfig
- (*CustomZone)(nil), // 32: management.CustomZone
- (*SimpleRecord)(nil), // 33: management.SimpleRecord
- (*NameServerGroup)(nil), // 34: management.NameServerGroup
- (*NameServer)(nil), // 35: management.NameServer
- (*FirewallRule)(nil), // 36: management.FirewallRule
- (*NetworkAddress)(nil), // 37: management.NetworkAddress
- (*Checks)(nil), // 38: management.Checks
- (*PortInfo)(nil), // 39: management.PortInfo
- (*RouteFirewallRule)(nil), // 40: management.RouteFirewallRule
- (*PortInfo_Range)(nil), // 41: management.PortInfo.Range
- (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp
+ (*Flags)(nil), // 13: management.Flags
+ (*PeerSystemMeta)(nil), // 14: management.PeerSystemMeta
+ (*LoginResponse)(nil), // 15: management.LoginResponse
+ (*ServerKeyResponse)(nil), // 16: management.ServerKeyResponse
+ (*Empty)(nil), // 17: management.Empty
+ (*NetbirdConfig)(nil), // 18: management.NetbirdConfig
+ (*HostConfig)(nil), // 19: management.HostConfig
+ (*RelayConfig)(nil), // 20: management.RelayConfig
+ (*ProtectedHostConfig)(nil), // 21: management.ProtectedHostConfig
+ (*PeerConfig)(nil), // 22: management.PeerConfig
+ (*NetworkMap)(nil), // 23: management.NetworkMap
+ (*RemotePeerConfig)(nil), // 24: management.RemotePeerConfig
+ (*SSHConfig)(nil), // 25: management.SSHConfig
+ (*DeviceAuthorizationFlowRequest)(nil), // 26: management.DeviceAuthorizationFlowRequest
+ (*DeviceAuthorizationFlow)(nil), // 27: management.DeviceAuthorizationFlow
+ (*PKCEAuthorizationFlowRequest)(nil), // 28: management.PKCEAuthorizationFlowRequest
+ (*PKCEAuthorizationFlow)(nil), // 29: management.PKCEAuthorizationFlow
+ (*ProviderConfig)(nil), // 30: management.ProviderConfig
+ (*Route)(nil), // 31: management.Route
+ (*DNSConfig)(nil), // 32: management.DNSConfig
+ (*CustomZone)(nil), // 33: management.CustomZone
+ (*SimpleRecord)(nil), // 34: management.SimpleRecord
+ (*NameServerGroup)(nil), // 35: management.NameServerGroup
+ (*NameServer)(nil), // 36: management.NameServer
+ (*FirewallRule)(nil), // 37: management.FirewallRule
+ (*NetworkAddress)(nil), // 38: management.NetworkAddress
+ (*Checks)(nil), // 39: management.Checks
+ (*PortInfo)(nil), // 40: management.PortInfo
+ (*RouteFirewallRule)(nil), // 41: management.RouteFirewallRule
+ (*PortInfo_Range)(nil), // 42: management.PortInfo.Range
+ (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp
}
var file_management_proto_depIdxs = []int32{
- 13, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta
- 17, // 1: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig
- 21, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig
- 23, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig
- 22, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap
- 38, // 5: management.SyncResponse.Checks:type_name -> management.Checks
- 13, // 6: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta
- 13, // 7: management.LoginRequest.meta:type_name -> management.PeerSystemMeta
+ 14, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta
+ 18, // 1: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig
+ 22, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig
+ 24, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig
+ 23, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap
+ 39, // 5: management.SyncResponse.Checks:type_name -> management.Checks
+ 14, // 6: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta
+ 14, // 7: management.LoginRequest.meta:type_name -> management.PeerSystemMeta
10, // 8: management.LoginRequest.peerKeys:type_name -> management.PeerKeys
- 37, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress
+ 38, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress
11, // 10: management.PeerSystemMeta.environment:type_name -> management.Environment
12, // 11: management.PeerSystemMeta.files:type_name -> management.File
- 17, // 12: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig
- 21, // 13: management.LoginResponse.peerConfig:type_name -> management.PeerConfig
- 38, // 14: management.LoginResponse.Checks:type_name -> management.Checks
- 42, // 15: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp
- 18, // 16: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig
- 20, // 17: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig
- 18, // 18: management.WiretrusteeConfig.signal:type_name -> management.HostConfig
- 19, // 19: management.WiretrusteeConfig.relay:type_name -> management.RelayConfig
- 3, // 20: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol
- 18, // 21: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig
- 24, // 22: management.PeerConfig.sshConfig:type_name -> management.SSHConfig
- 21, // 23: management.NetworkMap.peerConfig:type_name -> management.PeerConfig
- 23, // 24: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig
- 30, // 25: management.NetworkMap.Routes:type_name -> management.Route
- 31, // 26: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig
- 23, // 27: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig
- 36, // 28: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule
- 40, // 29: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule
- 24, // 30: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig
- 4, // 31: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider
- 29, // 32: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig
- 29, // 33: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig
- 34, // 34: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup
- 32, // 35: management.DNSConfig.CustomZones:type_name -> management.CustomZone
- 33, // 36: management.CustomZone.Records:type_name -> management.SimpleRecord
- 35, // 37: management.NameServerGroup.NameServers:type_name -> management.NameServer
- 1, // 38: management.FirewallRule.Direction:type_name -> management.RuleDirection
- 2, // 39: management.FirewallRule.Action:type_name -> management.RuleAction
- 0, // 40: management.FirewallRule.Protocol:type_name -> management.RuleProtocol
- 41, // 41: management.PortInfo.range:type_name -> management.PortInfo.Range
- 2, // 42: management.RouteFirewallRule.action:type_name -> management.RuleAction
- 0, // 43: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol
- 39, // 44: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo
- 5, // 45: management.ManagementService.Login:input_type -> management.EncryptedMessage
- 5, // 46: management.ManagementService.Sync:input_type -> management.EncryptedMessage
- 16, // 47: management.ManagementService.GetServerKey:input_type -> management.Empty
- 16, // 48: management.ManagementService.isHealthy:input_type -> management.Empty
- 5, // 49: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage
- 5, // 50: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage
- 5, // 51: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage
- 5, // 52: management.ManagementService.Login:output_type -> management.EncryptedMessage
- 5, // 53: management.ManagementService.Sync:output_type -> management.EncryptedMessage
- 15, // 54: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse
- 16, // 55: management.ManagementService.isHealthy:output_type -> management.Empty
- 5, // 56: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage
- 5, // 57: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage
- 16, // 58: management.ManagementService.SyncMeta:output_type -> management.Empty
- 52, // [52:59] is the sub-list for method output_type
- 45, // [45:52] is the sub-list for method input_type
- 45, // [45:45] is the sub-list for extension type_name
- 45, // [45:45] is the sub-list for extension extendee
- 0, // [0:45] is the sub-list for field type_name
+ 13, // 12: management.PeerSystemMeta.flags:type_name -> management.Flags
+ 18, // 13: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig
+ 22, // 14: management.LoginResponse.peerConfig:type_name -> management.PeerConfig
+ 39, // 15: management.LoginResponse.Checks:type_name -> management.Checks
+ 43, // 16: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp
+ 19, // 17: management.NetbirdConfig.stuns:type_name -> management.HostConfig
+ 21, // 18: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig
+ 19, // 19: management.NetbirdConfig.signal:type_name -> management.HostConfig
+ 20, // 20: management.NetbirdConfig.relay:type_name -> management.RelayConfig
+ 3, // 21: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol
+ 19, // 22: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig
+ 25, // 23: management.PeerConfig.sshConfig:type_name -> management.SSHConfig
+ 22, // 24: management.NetworkMap.peerConfig:type_name -> management.PeerConfig
+ 24, // 25: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig
+ 31, // 26: management.NetworkMap.Routes:type_name -> management.Route
+ 32, // 27: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig
+ 24, // 28: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig
+ 37, // 29: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule
+ 41, // 30: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule
+ 25, // 31: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig
+ 4, // 32: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider
+ 30, // 33: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig
+ 30, // 34: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig
+ 35, // 35: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup
+ 33, // 36: management.DNSConfig.CustomZones:type_name -> management.CustomZone
+ 34, // 37: management.CustomZone.Records:type_name -> management.SimpleRecord
+ 36, // 38: management.NameServerGroup.NameServers:type_name -> management.NameServer
+ 1, // 39: management.FirewallRule.Direction:type_name -> management.RuleDirection
+ 2, // 40: management.FirewallRule.Action:type_name -> management.RuleAction
+ 0, // 41: management.FirewallRule.Protocol:type_name -> management.RuleProtocol
+ 40, // 42: management.FirewallRule.PortInfo:type_name -> management.PortInfo
+ 42, // 43: management.PortInfo.range:type_name -> management.PortInfo.Range
+ 2, // 44: management.RouteFirewallRule.action:type_name -> management.RuleAction
+ 0, // 45: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol
+ 40, // 46: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo
+ 5, // 47: management.ManagementService.Login:input_type -> management.EncryptedMessage
+ 5, // 48: management.ManagementService.Sync:input_type -> management.EncryptedMessage
+ 17, // 49: management.ManagementService.GetServerKey:input_type -> management.Empty
+ 17, // 50: management.ManagementService.isHealthy:input_type -> management.Empty
+ 5, // 51: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage
+ 5, // 52: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage
+ 5, // 53: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage
+ 5, // 54: management.ManagementService.Login:output_type -> management.EncryptedMessage
+ 5, // 55: management.ManagementService.Sync:output_type -> management.EncryptedMessage
+ 16, // 56: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse
+ 17, // 57: management.ManagementService.isHealthy:output_type -> management.Empty
+ 5, // 58: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage
+ 5, // 59: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage
+ 17, // 60: management.ManagementService.SyncMeta:output_type -> management.Empty
+ 54, // [54:61] is the sub-list for method output_type
+ 47, // [47:54] is the sub-list for method input_type
+ 47, // [47:47] is the sub-list for extension type_name
+ 47, // [47:47] is the sub-list for extension extendee
+ 0, // [0:47] is the sub-list for field type_name
}
func init() { file_management_proto_init() }
@@ -3597,7 +3744,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*PeerSystemMeta); i {
+ switch v := v.(*Flags); i {
case 0:
return &v.state
case 1:
@@ -3609,7 +3756,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*LoginResponse); i {
+ switch v := v.(*PeerSystemMeta); i {
case 0:
return &v.state
case 1:
@@ -3621,7 +3768,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*ServerKeyResponse); i {
+ switch v := v.(*LoginResponse); i {
case 0:
return &v.state
case 1:
@@ -3633,7 +3780,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Empty); i {
+ switch v := v.(*ServerKeyResponse); i {
case 0:
return &v.state
case 1:
@@ -3645,7 +3792,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*WiretrusteeConfig); i {
+ switch v := v.(*Empty); i {
case 0:
return &v.state
case 1:
@@ -3657,7 +3804,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*HostConfig); i {
+ switch v := v.(*NetbirdConfig); i {
case 0:
return &v.state
case 1:
@@ -3669,7 +3816,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*RelayConfig); i {
+ switch v := v.(*HostConfig); i {
case 0:
return &v.state
case 1:
@@ -3681,7 +3828,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*ProtectedHostConfig); i {
+ switch v := v.(*RelayConfig); i {
case 0:
return &v.state
case 1:
@@ -3693,7 +3840,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*PeerConfig); i {
+ switch v := v.(*ProtectedHostConfig); i {
case 0:
return &v.state
case 1:
@@ -3705,7 +3852,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*NetworkMap); i {
+ switch v := v.(*PeerConfig); i {
case 0:
return &v.state
case 1:
@@ -3717,7 +3864,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*RemotePeerConfig); i {
+ switch v := v.(*NetworkMap); i {
case 0:
return &v.state
case 1:
@@ -3729,7 +3876,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SSHConfig); i {
+ switch v := v.(*RemotePeerConfig); i {
case 0:
return &v.state
case 1:
@@ -3741,7 +3888,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*DeviceAuthorizationFlowRequest); i {
+ switch v := v.(*SSHConfig); i {
case 0:
return &v.state
case 1:
@@ -3753,7 +3900,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*DeviceAuthorizationFlow); i {
+ switch v := v.(*DeviceAuthorizationFlowRequest); i {
case 0:
return &v.state
case 1:
@@ -3765,7 +3912,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*PKCEAuthorizationFlowRequest); i {
+ switch v := v.(*DeviceAuthorizationFlow); i {
case 0:
return &v.state
case 1:
@@ -3777,7 +3924,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*PKCEAuthorizationFlow); i {
+ switch v := v.(*PKCEAuthorizationFlowRequest); i {
case 0:
return &v.state
case 1:
@@ -3789,7 +3936,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*ProviderConfig); i {
+ switch v := v.(*PKCEAuthorizationFlow); i {
case 0:
return &v.state
case 1:
@@ -3801,7 +3948,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Route); i {
+ switch v := v.(*ProviderConfig); i {
case 0:
return &v.state
case 1:
@@ -3813,7 +3960,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*DNSConfig); i {
+ switch v := v.(*Route); i {
case 0:
return &v.state
case 1:
@@ -3825,7 +3972,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*CustomZone); i {
+ switch v := v.(*DNSConfig); i {
case 0:
return &v.state
case 1:
@@ -3837,7 +3984,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*SimpleRecord); i {
+ switch v := v.(*CustomZone); i {
case 0:
return &v.state
case 1:
@@ -3849,7 +3996,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*NameServerGroup); i {
+ switch v := v.(*SimpleRecord); i {
case 0:
return &v.state
case 1:
@@ -3861,7 +4008,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*NameServer); i {
+ switch v := v.(*NameServerGroup); i {
case 0:
return &v.state
case 1:
@@ -3873,7 +4020,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*FirewallRule); i {
+ switch v := v.(*NameServer); i {
case 0:
return &v.state
case 1:
@@ -3885,7 +4032,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*NetworkAddress); i {
+ switch v := v.(*FirewallRule); i {
case 0:
return &v.state
case 1:
@@ -3897,7 +4044,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Checks); i {
+ switch v := v.(*NetworkAddress); i {
case 0:
return &v.state
case 1:
@@ -3909,7 +4056,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*PortInfo); i {
+ switch v := v.(*Checks); i {
case 0:
return &v.state
case 1:
@@ -3921,7 +4068,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*RouteFirewallRule); i {
+ switch v := v.(*PortInfo); i {
case 0:
return &v.state
case 1:
@@ -3933,6 +4080,18 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RouteFirewallRule); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_management_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PortInfo_Range); i {
case 0:
return &v.state
@@ -3945,7 +4104,7 @@ func file_management_proto_init() {
}
}
}
- file_management_proto_msgTypes[34].OneofWrappers = []interface{}{
+ file_management_proto_msgTypes[35].OneofWrappers = []interface{}{
(*PortInfo_Port)(nil),
(*PortInfo_Range_)(nil),
}
@@ -3955,7 +4114,7 @@ func file_management_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_management_proto_rawDesc,
NumEnums: 5,
- NumMessages: 37,
+ NumMessages: 38,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/management/proto/management.proto b/management/proto/management.proto
index 5f4e0df46..cd207136f 100644
--- a/management/proto/management.proto
+++ b/management/proto/management.proto
@@ -52,7 +52,7 @@ message EncryptedMessage {
// encrypted message Body
bytes body = 2;
- // Version of the Wiretrustee Management Service protocol
+ // Version of the Netbird Management Service protocol
int32 version = 3;
}
@@ -61,11 +61,11 @@ message SyncRequest {
PeerSystemMeta meta = 1;
}
-// SyncResponse represents a state that should be applied to the local peer (e.g. Wiretrustee servers config as well as local peer and remote peers configs)
+// SyncResponse represents a state that should be applied to the local peer (e.g. Netbird servers config as well as local peer and remote peers configs)
message SyncResponse {
// Global config
- WiretrusteeConfig wiretrusteeConfig = 1;
+ NetbirdConfig netbirdConfig = 1;
// Deprecated. Use NetworkMap.PeerConfig
PeerConfig peerConfig = 2;
@@ -97,7 +97,8 @@ message LoginRequest {
string jwtToken = 3;
// Can be absent for now.
PeerKeys peerKeys = 4;
-
+
+ repeated string dnsLabels = 5;
}
// PeerKeys is additional peer info like SSH pub key and WireGuard public key.
@@ -128,6 +129,16 @@ message File {
bool processIsRunning = 3;
}
+message Flags {
+ bool rosenpassEnabled = 1;
+ bool rosenpassPermissive = 2;
+ bool serverSSHAllowed = 3;
+ bool disableClientRoutes = 4;
+ bool disableServerRoutes = 5;
+ bool disableDNS = 6;
+ bool disableFirewall = 7;
+}
+
// PeerSystemMeta is machine meta data like OS and version.
message PeerSystemMeta {
string hostname = 1;
@@ -136,7 +147,7 @@ message PeerSystemMeta {
string core = 4;
string platform = 5;
string OS = 6;
- string wiretrusteeVersion = 7;
+ string netbirdVersion = 7;
string uiVersion = 8;
string kernelVersion = 9;
string OSVersion = 10;
@@ -146,11 +157,12 @@ message PeerSystemMeta {
string sysManufacturer = 14;
Environment environment = 15;
repeated File files = 16;
+ Flags flags = 17;
}
message LoginResponse {
// Global config
- WiretrusteeConfig wiretrusteeConfig = 1;
+ NetbirdConfig netbirdConfig = 1;
// Peer local config
PeerConfig peerConfig = 2;
// Posture checks to be evaluated by client
@@ -162,14 +174,14 @@ message ServerKeyResponse {
string key = 1;
// Key expiration timestamp after which the key should be fetched again by the client
google.protobuf.Timestamp expiresAt = 2;
- // Version of the Wiretrustee Management Service protocol
+ // Version of the Netbird Management Service protocol
int32 version = 3;
}
message Empty {}
-// WiretrusteeConfig is a common configuration of any Wiretrustee peer. It contains STUN, TURN, Signal and Management servers configurations
-message WiretrusteeConfig {
+// NetbirdConfig is a common configuration of any Netbird peer. It contains STUN, TURN, Signal and Management servers configurations
+message NetbirdConfig {
// a list of STUN servers
repeated HostConfig stuns = 1;
// a list of TURN servers
@@ -183,7 +195,7 @@ message WiretrusteeConfig {
// HostConfig describes connection properties of some server (e.g. STUN, Signal, Management)
message HostConfig {
- // URI of the resource e.g. turns://stun.wiretrustee.com:4430 or signal.wiretrustee.com:10000
+ // URI of the resource e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000
string uri = 1;
Protocol protocol = 2;
@@ -213,9 +225,9 @@ message ProtectedHostConfig {
// PeerConfig represents a configuration of a "our" peer.
// The properties are used to configure local Wireguard
message PeerConfig {
- // Peer's virtual IP address within the Wiretrustee VPN (a Wireguard address config)
+ // Peer's virtual IP address within the Netbird VPN (a Wireguard address config)
string address = 1;
- // Wiretrustee DNS server (a Wireguard DNS config)
+ // Netbird DNS server (a Wireguard DNS config)
string dns = 2;
// SSHConfig of the peer.
@@ -419,6 +431,7 @@ message FirewallRule {
RuleAction Action = 3;
RuleProtocol Protocol = 4;
string Port = 5;
+ PortInfo PortInfo = 6;
}
message NetworkAddress {
diff --git a/management/server/account.go b/management/server/account.go
index 17fee541e..332d356e2 100644
--- a/management/server/account.go
+++ b/management/server/account.go
@@ -2,11 +2,8 @@ package server
import (
"context"
- "crypto/sha256"
- b64 "encoding/base64"
"errors"
"fmt"
- "hash/crc32"
"math/rand"
"net"
"net/netip"
@@ -24,14 +21,13 @@ import (
log "github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
- "github.com/netbirdio/netbird/base62"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/domain"
"github.com/netbirdio/netbird/management/server/activity"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/integrated_validator"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/status"
@@ -63,11 +59,11 @@ type AccountManager interface {
GetOrCreateAccountByUser(ctx context.Context, userId, domain string) (*types.Account, error)
GetAccount(ctx context.Context, accountID string) (*types.Account, error)
CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType, expiresIn time.Duration,
- autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error)
+ autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error)
SaveSetupKey(ctx context.Context, accountID string, key *types.SetupKey, userID string) (*types.SetupKey, error)
CreateUser(ctx context.Context, accountID, initiatorUserID string, key *types.UserInfo) (*types.UserInfo, error)
DeleteUser(ctx context.Context, accountID, initiatorUserID string, targetUserID string) error
- DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) 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
ListSetupKeys(ctx context.Context, accountID, userID string) ([]*types.SetupKey, error)
SaveUser(ctx context.Context, accountID, initiatorUserID string, update *types.User) (*types.UserInfo, error)
@@ -77,13 +73,10 @@ type AccountManager interface {
GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error)
AccountExists(ctx context.Context, accountID string) (bool, error)
GetAccountIDByUserID(ctx context.Context, userID, domain string) (string, error)
- GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
- CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error
- GetAccountInfoFromPAT(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error)
+ GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
DeleteAccount(ctx context.Context, accountID, userID string) error
- MarkPATUsed(ctx context.Context, tokenID string) error
GetUserByID(ctx context.Context, id string) (*types.User, error)
- GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error)
+ GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
ListUsers(ctx context.Context, accountID string) ([]*types.User, error)
GetPeers(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error)
MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error
@@ -96,7 +89,7 @@ type AccountManager interface {
DeletePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error
GetPAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) (*types.PersonalAccessToken, error)
GetAllPATs(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) ([]*types.PersonalAccessToken, error)
- GetUsersFromAccount(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error)
+ GetUsersFromAccount(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error)
GetGroup(ctx context.Context, accountId, groupID, userID string) (*types.Group, error)
GetAllGroups(ctx context.Context, accountID, userID string) ([]*types.Group, error)
GetGroupByName(ctx context.Context, groupName, accountID string) (*types.Group, error)
@@ -149,6 +142,8 @@ type AccountManager interface {
GetAccountSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error)
DeleteSetupKey(ctx context.Context, accountID, userID, keyID string) error
UpdateAccountPeers(ctx context.Context, accountID string)
+ BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error)
+ SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error
}
type DefaultAccountManager struct {
@@ -251,6 +246,11 @@ func BuildManager(
integratedPeerValidator integrated_validator.IntegratedValidator,
metrics telemetry.AppMetrics,
) (*DefaultAccountManager, error) {
+ start := time.Now()
+ defer func() {
+ log.WithContext(ctx).Debugf("took %v to instantiate account manager", time.Since(start))
+ }()
+
am := &DefaultAccountManager{
Store: store,
geo: geo,
@@ -268,39 +268,21 @@ func BuildManager(
metrics: metrics,
requestBuffer: NewAccountRequestBuffer(ctx, store),
}
- allAccounts := store.GetAllAccounts(ctx)
+ accountsCounter, err := store.GetAccountsCounter(ctx)
+ if err != nil {
+ log.WithContext(ctx).Error(err)
+ }
+
// enable single account mode only if configured by user and number of existing accounts is not grater than 1
- am.singleAccountMode = singleAccountModeDomain != "" && len(allAccounts) <= 1
+ am.singleAccountMode = singleAccountModeDomain != "" && accountsCounter <= 1
if am.singleAccountMode {
if !isDomainValid(singleAccountModeDomain) {
return nil, status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for a single account mode. Please review your input for --single-account-mode-domain", singleAccountModeDomain)
}
am.singleAccountModeDomain = singleAccountModeDomain
- log.WithContext(ctx).Infof("single account mode enabled, accounts number %d", len(allAccounts))
+ log.WithContext(ctx).Infof("single account mode enabled, accounts number %d", accountsCounter)
} else {
- log.WithContext(ctx).Infof("single account mode disabled, accounts number %d", len(allAccounts))
- }
-
- // if account doesn't have a default group
- // we create 'all' group and add all peers into it
- // also we create default rule with source as destination
- for _, account := range allAccounts {
- shouldSave := false
-
- _, err := account.GetGroupAll()
- if err != nil {
- if err := addAllGroup(account); err != nil {
- return nil, err
- }
- shouldSave = true
- }
-
- if shouldSave {
- err = store.SaveAccount(ctx, account)
- if err != nil {
- return nil, err
- }
- }
+ log.WithContext(ctx).Infof("single account mode disabled, accounts number %d", accountsCounter)
}
goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute)
@@ -617,6 +599,12 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u
if user.Role != types.UserRoleOwner {
return status.Errorf(status.PermissionDenied, "user is not allowed to delete account. Only account owner can delete account")
}
+
+ userInfosMap, err := am.BuildUserInfosForAccount(ctx, accountID, userID, maps.Values(account.Users))
+ if err != nil {
+ return status.Errorf(status.Internal, "failed to build user infos for account %s: %v", accountID, err)
+ }
+
for _, otherUser := range account.Users {
if otherUser.IsServiceUser {
continue
@@ -626,13 +614,23 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u
continue
}
- _, deleteUserErr := am.deleteRegularUser(ctx, accountID, userID, otherUser.Id)
+ userInfo, ok := userInfosMap[otherUser.Id]
+ if !ok {
+ return status.Errorf(status.NotFound, "user info not found for user %s", otherUser.Id)
+ }
+
+ _, deleteUserErr := am.deleteRegularUser(ctx, accountID, userID, userInfo)
if deleteUserErr != nil {
return deleteUserErr
}
}
- _, err = am.deleteRegularUser(ctx, accountID, userID, userID)
+ userInfo, ok := userInfosMap[userID]
+ if !ok {
+ return status.Errorf(status.NotFound, "user info not found for user %s", userID)
+ }
+
+ _, err = am.deleteRegularUser(ctx, accountID, userID, userInfo)
if err != nil {
log.WithContext(ctx).Errorf("failed deleting user %s. error: %s", userID, err)
return err
@@ -690,7 +688,7 @@ func isNil(i idp.Manager) bool {
func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error {
if !isNil(am.idpManager) {
// user can be nil if it wasn't found (e.g., just created)
- user, err := am.lookupUserInCache(ctx, am.Store, userID, accountID)
+ user, err := am.lookupUserInCache(ctx, userID, accountID)
if err != nil {
return err
}
@@ -766,8 +764,8 @@ func (am *DefaultAccountManager) lookupUserInCacheByEmail(ctx context.Context, e
}
// lookupUserInCache looks up user in the IdP cache and returns it. If the user wasn't found, the function returns nil
-func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, transaction store.Store, userID string, accountID string) (*idp.UserData, error) {
- accountUsers, err := transaction.GetAccountUsers(ctx, store.LockingStrengthShare, accountID)
+func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, userID string, accountID string) (*idp.UserData, error) {
+ accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
@@ -797,7 +795,7 @@ func (am *DefaultAccountManager) lookupUserInCache(ctx context.Context, transact
// add extra check on external cache manager. We may get to this point when the user is not yet findable in IDP,
// or it didn't have its metadata updated with am.addAccountIDToIDPAppMeta
- user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
+ user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
if err != nil {
log.WithContext(ctx).Errorf("failed finding user %s in account %s", userID, accountID)
return nil, err
@@ -937,11 +935,11 @@ func (am *DefaultAccountManager) removeUserFromCache(ctx context.Context, accoun
}
// updateAccountDomainAttributesIfNotUpToDate updates the account domain attributes if they are not up to date and then, saves the account changes
-func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx context.Context, accountID string, claims jwtclaims.AuthorizationClaims,
+func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx context.Context, accountID string, userAuth nbcontext.UserAuth,
primaryDomain bool,
) error {
- if claims.Domain == "" {
- log.WithContext(ctx).Errorf("claims don't contain a valid domain, skipping domain attributes update. Received claims: %v", claims)
+ if userAuth.Domain == "" {
+ log.WithContext(ctx).Errorf("claims don't contain a valid domain, skipping domain attributes update. Received claims: %v", userAuth)
return nil
}
@@ -954,11 +952,11 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx
return err
}
- if domainIsUpToDate(accountDomain, domainCategory, claims) {
+ if domainIsUpToDate(accountDomain, domainCategory, userAuth) {
return nil
}
- user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId)
+ user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
if err != nil {
log.WithContext(ctx).Errorf("error getting user: %v", err)
return err
@@ -967,13 +965,13 @@ func (am *DefaultAccountManager) updateAccountDomainAttributesIfNotUpToDate(ctx
newDomain := accountDomain
newCategoty := domainCategory
- lowerDomain := strings.ToLower(claims.Domain)
+ lowerDomain := strings.ToLower(userAuth.Domain)
if accountDomain != lowerDomain && user.HasAdminPower() {
newDomain = lowerDomain
}
if accountDomain == lowerDomain {
- newCategoty = claims.DomainCategory
+ newCategoty = userAuth.DomainCategory
}
return am.Store.UpdateAccountDomainAttributes(ctx, accountID, newDomain, newCategoty, primaryDomain)
@@ -989,16 +987,16 @@ func (am *DefaultAccountManager) handleExistingUserAccount(
ctx context.Context,
userAccountID string,
domainAccountID string,
- claims jwtclaims.AuthorizationClaims,
+ userAuth nbcontext.UserAuth,
) error {
primaryDomain := domainAccountID == "" || userAccountID == domainAccountID
- err := am.updateAccountDomainAttributesIfNotUpToDate(ctx, userAccountID, claims, primaryDomain)
+ err := am.updateAccountDomainAttributesIfNotUpToDate(ctx, userAccountID, userAuth, primaryDomain)
if err != nil {
return err
}
// we should register the account ID to this user's metadata in our IDP manager
- err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, userAccountID)
+ err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, userAccountID)
if err != nil {
return err
}
@@ -1008,20 +1006,20 @@ func (am *DefaultAccountManager) handleExistingUserAccount(
// addNewPrivateAccount validates if there is an existing primary account for the domain, if so it adds the new user to that account,
// otherwise it will create a new account and make it primary account for the domain.
-func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domainAccountID string, claims jwtclaims.AuthorizationClaims) (string, error) {
- if claims.UserId == "" {
+func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) {
+ if userAuth.UserId == "" {
return "", fmt.Errorf("user ID is empty")
}
- lowerDomain := strings.ToLower(claims.Domain)
+ lowerDomain := strings.ToLower(userAuth.Domain)
- newAccount, err := am.newAccount(ctx, claims.UserId, lowerDomain)
+ newAccount, err := am.newAccount(ctx, userAuth.UserId, lowerDomain)
if err != nil {
return "", err
}
newAccount.Domain = lowerDomain
- newAccount.DomainCategory = claims.DomainCategory
+ newAccount.DomainCategory = userAuth.DomainCategory
newAccount.IsDomainPrimaryAccount = true
err = am.Store.SaveAccount(ctx, newAccount)
@@ -1029,33 +1027,33 @@ func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domai
return "", err
}
- err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, newAccount.Id)
+ err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, newAccount.Id)
if err != nil {
return "", err
}
- am.StoreEvent(ctx, claims.UserId, claims.UserId, newAccount.Id, activity.UserJoined, nil)
+ am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, newAccount.Id, activity.UserJoined, nil)
return newAccount.Id, nil
}
-func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, claims jwtclaims.AuthorizationClaims) (string, error) {
+func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, userAuth nbcontext.UserAuth) (string, error) {
unlockAccount := am.Store.AcquireWriteLockByUID(ctx, domainAccountID)
defer unlockAccount()
- newUser := types.NewRegularUser(claims.UserId)
+ newUser := types.NewRegularUser(userAuth.UserId)
newUser.AccountID = domainAccountID
err := am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser)
if err != nil {
return "", err
}
- err = am.addAccountIDToIDPAppMeta(ctx, claims.UserId, domainAccountID)
+ err = am.addAccountIDToIDPAppMeta(ctx, userAuth.UserId, domainAccountID)
if err != nil {
return "", err
}
- am.StoreEvent(ctx, claims.UserId, claims.UserId, domainAccountID, activity.UserJoined, nil)
+ am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, domainAccountID, activity.UserJoined, nil)
return domainAccountID, nil
}
@@ -1068,7 +1066,7 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str
return nil
}
- user, err := am.lookupUserInCache(ctx, am.Store, userID, accountID)
+ user, err := am.lookupUserInCache(ctx, userID, accountID)
if err != nil {
return err
}
@@ -1095,76 +1093,11 @@ func (am *DefaultAccountManager) redeemInvite(ctx context.Context, accountID str
return nil
}
-// MarkPATUsed marks a personal access token as used
-func (am *DefaultAccountManager) MarkPATUsed(ctx context.Context, tokenID string) error {
- return am.Store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID)
-}
-
// GetAccount returns an account associated with this account ID.
func (am *DefaultAccountManager) GetAccount(ctx context.Context, accountID string) (*types.Account, error) {
return am.Store.GetAccount(ctx, accountID)
}
-// GetAccountInfoFromPAT retrieves user, personal access token, domain, and category details from a personal access token.
-func (am *DefaultAccountManager) GetAccountInfoFromPAT(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
- user, pat, err = am.extractPATFromToken(ctx, token)
- if err != nil {
- return nil, nil, "", "", err
- }
-
- domain, category, err = am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID)
- if err != nil {
- return nil, nil, "", "", err
- }
-
- return user, pat, domain, category, nil
-}
-
-// extractPATFromToken validates the token structure and retrieves associated User and PAT.
-func (am *DefaultAccountManager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) {
- if len(token) != types.PATLength {
- return nil, nil, fmt.Errorf("token has incorrect length")
- }
-
- prefix := token[:len(types.PATPrefix)]
- if prefix != types.PATPrefix {
- return nil, nil, fmt.Errorf("token has wrong prefix")
- }
- secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength]
- encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength]
-
- verificationChecksum, err := base62.Decode(encodedChecksum)
- if err != nil {
- return nil, nil, fmt.Errorf("token checksum decoding failed: %w", err)
- }
-
- secretChecksum := crc32.ChecksumIEEE([]byte(secret))
- if secretChecksum != verificationChecksum {
- return nil, nil, fmt.Errorf("token checksum does not match")
- }
-
- hashedToken := sha256.Sum256([]byte(token))
- encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:])
-
- var user *types.User
- var pat *types.PersonalAccessToken
-
- err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
- pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken)
- if err != nil {
- return err
- }
-
- user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID)
- return err
- })
- if err != nil {
- return nil, nil, err
- }
-
- return user, pat, nil
-}
-
// GetAccountByID returns an account associated with this account ID.
func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID string, userID string) (*types.Account, error) {
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
@@ -1179,58 +1112,56 @@ func (am *DefaultAccountManager) GetAccountByID(ctx context.Context, accountID s
return am.Store.GetAccount(ctx, accountID)
}
-// GetAccountIDFromToken returns an account ID associated with this token.
-func (am *DefaultAccountManager) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- if claims.UserId == "" {
+func (am *DefaultAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
+ if userAuth.UserId == "" {
return "", "", errors.New(emptyUserID)
}
if am.singleAccountMode && am.singleAccountModeDomain != "" {
// This section is mostly related to self-hosted installations.
// We override incoming domain claims to group users under a single account.
- claims.Domain = am.singleAccountModeDomain
- claims.DomainCategory = types.PrivateCategory
+ userAuth.Domain = am.singleAccountModeDomain
+ userAuth.DomainCategory = types.PrivateCategory
log.WithContext(ctx).Debugf("overriding JWT Domain and DomainCategory claims since single account mode is enabled")
}
- accountID, err := am.getAccountIDWithAuthorizationClaims(ctx, claims)
+ accountID, err := am.getAccountIDWithAuthorizationClaims(ctx, userAuth)
if err != nil {
return "", "", err
}
- user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId)
+ user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
if err != nil {
// this is not really possible because we got an account by user ID
- return "", "", status.Errorf(status.NotFound, "user %s not found", claims.UserId)
+ return "", "", status.Errorf(status.NotFound, "user %s not found", userAuth.UserId)
+ }
+
+ if userAuth.IsChild {
+ return accountID, user.Id, nil
}
if user.AccountID != accountID {
- return "", "", status.Errorf(status.PermissionDenied, "user %s is not part of the account %s", claims.UserId, accountID)
+ return "", "", status.Errorf(status.PermissionDenied, "user %s is not part of the account %s", userAuth.UserId, accountID)
}
- if !user.IsServiceUser && claims.Invited {
+ if !user.IsServiceUser && userAuth.Invited {
err = am.redeemInvite(ctx, accountID, user.Id)
if err != nil {
return "", "", err
}
}
- if err = am.syncJWTGroups(ctx, accountID, claims); err != nil {
- return "", "", err
- }
-
return accountID, user.Id, nil
}
// syncJWTGroups processes the JWT groups for a user, updates the account based on the groups,
// and propagates changes to peers if group propagation is enabled.
-func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID string, claims jwtclaims.AuthorizationClaims) error {
- if claim, exists := claims.Raw[jwtclaims.IsToken]; exists {
- if isToken, ok := claim.(bool); ok && isToken {
- return nil
- }
+// requires userAuth to have been ValidateAndParseToken and EnsureUserAccessByJWTGroups by the AuthManager
+func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error {
+ if userAuth.IsChild || userAuth.IsPAT {
+ return nil
}
- settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+ settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId)
if err != nil {
return err
}
@@ -1244,9 +1175,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
return nil
}
- jwtGroupsNames := extractJWTGroups(ctx, settings.JWTGroupsClaimName, claims)
-
- unlockAccount := am.Store.AcquireWriteLockByUID(ctx, accountID)
+ unlockAccount := am.Store.AcquireWriteLockByUID(ctx, userAuth.AccountId)
defer func() {
if unlockAccount != nil {
unlockAccount()
@@ -1258,17 +1187,17 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
var hasChanges bool
var user *types.User
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
- user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, claims.UserId)
+ user, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
if err != nil {
return fmt.Errorf("error getting user: %w", err)
}
- groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID)
+ groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId)
if err != nil {
return fmt.Errorf("error getting account groups: %w", err)
}
- changed, updatedAutoGroups, newGroupsToCreate, err := am.getJWTGroupsChanges(user, groups, jwtGroupsNames)
+ changed, updatedAutoGroups, newGroupsToCreate, err := am.getJWTGroupsChanges(user, groups, userAuth.Groups)
if err != nil {
return fmt.Errorf("error getting JWT groups changes: %w", err)
}
@@ -1293,7 +1222,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
// Propagate changes to peers if group propagation is enabled
if settings.GroupsPropagationEnabled {
- groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, accountID)
+ groups, err = transaction.GetAccountGroups(ctx, store.LockingStrengthShare, userAuth.AccountId)
if err != nil {
return fmt.Errorf("error getting account groups: %w", err)
}
@@ -1303,7 +1232,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
groupsMap[group.ID] = group
}
- peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, claims.UserId)
+ peers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, userAuth.AccountId, userAuth.UserId)
if err != nil {
return fmt.Errorf("error getting user peers: %w", err)
}
@@ -1317,7 +1246,7 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
return fmt.Errorf("error saving groups: %w", err)
}
- if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil {
+ if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, userAuth.AccountId); err != nil {
return fmt.Errorf("error incrementing network serial: %w", err)
}
}
@@ -1335,45 +1264,45 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
}
for _, g := range addNewGroups {
- group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, accountID, g)
+ group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g)
if err != nil {
- log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, accountID)
+ log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId)
} else {
meta := map[string]any{
"group": group.Name, "group_id": group.ID,
"is_service_user": user.IsServiceUser, "user_name": user.ServiceUserName,
}
- am.StoreEvent(ctx, user.Id, user.Id, accountID, activity.GroupAddedToUser, meta)
+ am.StoreEvent(ctx, user.Id, user.Id, userAuth.AccountId, activity.GroupAddedToUser, meta)
}
}
for _, g := range removeOldGroups {
- group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, accountID, g)
+ group, err := am.Store.GetGroupByID(ctx, store.LockingStrengthShare, userAuth.AccountId, g)
if err != nil {
- log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, accountID)
+ log.WithContext(ctx).Debugf("group %s not found while saving user activity event of account %s", g, userAuth.AccountId)
} else {
meta := map[string]any{
"group": group.Name, "group_id": group.ID,
"is_service_user": user.IsServiceUser, "user_name": user.ServiceUserName,
}
- am.StoreEvent(ctx, user.Id, user.Id, accountID, activity.GroupRemovedFromUser, meta)
+ am.StoreEvent(ctx, user.Id, user.Id, userAuth.AccountId, activity.GroupRemovedFromUser, meta)
}
}
if settings.GroupsPropagationEnabled {
- removedGroupAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, accountID, removeOldGroups)
+ removedGroupAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, userAuth.AccountId, removeOldGroups)
if err != nil {
return err
}
- newGroupsAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, accountID, addNewGroups)
+ newGroupsAffectsPeers, err := areGroupChangesAffectPeers(ctx, am.Store, userAuth.AccountId, addNewGroups)
if err != nil {
return err
}
if removedGroupAffectsPeers || newGroupsAffectsPeers {
- log.WithContext(ctx).Tracef("user %s: JWT group membership changed, updating account peers", claims.UserId)
- am.UpdateAccountPeers(ctx, accountID)
+ log.WithContext(ctx).Tracef("user %s: JWT group membership changed, updating account peers", userAuth.UserId)
+ am.UpdateAccountPeers(ctx, userAuth.AccountId)
}
}
@@ -1398,24 +1327,34 @@ func (am *DefaultAccountManager) syncJWTGroups(ctx context.Context, accountID st
// Existing user + Existing account + Existing Indexed Domain -> Nothing changes
//
// Existing user + Existing account + Existing domain reclassified Domain as private -> Nothing changes (index domain)
-func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, error) {
+//
+// UserAuth IsChild -> checks that account exists
+func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context.Context, userAuth nbcontext.UserAuth) (string, error) {
log.WithContext(ctx).Tracef("getting account with authorization claims. User ID: \"%s\", Account ID: \"%s\", Domain: \"%s\", Domain Category: \"%s\"",
- claims.UserId, claims.AccountId, claims.Domain, claims.DomainCategory)
+ userAuth.UserId, userAuth.AccountId, userAuth.Domain, userAuth.DomainCategory)
- if claims.UserId == "" {
+ if userAuth.UserId == "" {
return "", errors.New(emptyUserID)
}
- if claims.DomainCategory != types.PrivateCategory || !isDomainValid(claims.Domain) {
- return am.GetAccountIDByUserID(ctx, claims.UserId, claims.Domain)
+ if userAuth.IsChild {
+ exists, err := am.Store.AccountExists(ctx, store.LockingStrengthShare, userAuth.AccountId)
+ if err != nil || !exists {
+ return "", err
+ }
+ return userAuth.AccountId, nil
}
- if claims.AccountId != "" {
- return am.handlePrivateAccountWithIDFromClaim(ctx, claims)
+ if userAuth.DomainCategory != types.PrivateCategory || !isDomainValid(userAuth.Domain) {
+ return am.GetAccountIDByUserID(ctx, userAuth.UserId, userAuth.Domain)
+ }
+
+ if userAuth.AccountId != "" {
+ return am.handlePrivateAccountWithIDFromClaim(ctx, userAuth)
}
// We checked if the domain has a primary account already
- domainAccountID, cancel, err := am.getPrivateDomainWithGlobalLock(ctx, claims.Domain)
+ domainAccountID, cancel, err := am.getPrivateDomainWithGlobalLock(ctx, userAuth.Domain)
if cancel != nil {
defer cancel()
}
@@ -1423,14 +1362,14 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context
return "", err
}
- userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId)
+ userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
if handleNotFound(err) != nil {
log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err)
return "", err
}
if userAccountID != "" {
- if err = am.handleExistingUserAccount(ctx, userAccountID, domainAccountID, claims); err != nil {
+ if err = am.handleExistingUserAccount(ctx, userAccountID, domainAccountID, userAuth); err != nil {
return "", err
}
@@ -1438,10 +1377,10 @@ func (am *DefaultAccountManager) getAccountIDWithAuthorizationClaims(ctx context
}
if domainAccountID != "" {
- return am.addNewUserToDomainAccount(ctx, domainAccountID, claims)
+ return am.addNewUserToDomainAccount(ctx, domainAccountID, userAuth)
}
- return am.addNewPrivateAccount(ctx, domainAccountID, claims)
+ return am.addNewPrivateAccount(ctx, domainAccountID, userAuth)
}
func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Context, domain string) (string, context.CancelFunc, error) {
domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, domain)
@@ -1469,40 +1408,40 @@ func (am *DefaultAccountManager) getPrivateDomainWithGlobalLock(ctx context.Cont
return domainAccountID, cancel, nil
}
-func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, error) {
- userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, claims.UserId)
+func (am *DefaultAccountManager) handlePrivateAccountWithIDFromClaim(ctx context.Context, userAuth nbcontext.UserAuth) (string, error) {
+ userAccountID, err := am.Store.GetAccountIDByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
if err != nil {
log.WithContext(ctx).Errorf("error getting account ID by user ID: %v", err)
return "", err
}
- if userAccountID != claims.AccountId {
- return "", fmt.Errorf("user %s is not part of the account id %s", claims.UserId, claims.AccountId)
+ if userAccountID != userAuth.AccountId {
+ return "", fmt.Errorf("user %s is not part of the account id %s", userAuth.UserId, userAuth.AccountId)
}
- accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, claims.AccountId)
+ accountDomain, domainCategory, err := am.Store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, userAuth.AccountId)
if handleNotFound(err) != nil {
log.WithContext(ctx).Errorf("error getting account domain and category: %v", err)
return "", err
}
- if domainIsUpToDate(accountDomain, domainCategory, claims) {
- return claims.AccountId, nil
+ if domainIsUpToDate(accountDomain, domainCategory, userAuth) {
+ return userAuth.AccountId, nil
}
// We checked if the domain has a primary account already
- domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, claims.Domain)
+ domainAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, userAuth.Domain)
if handleNotFound(err) != nil {
log.WithContext(ctx).Errorf(errorGettingDomainAccIDFmt, err)
return "", err
}
- err = am.handleExistingUserAccount(ctx, claims.AccountId, domainAccountID, claims)
+ err = am.handleExistingUserAccount(ctx, userAuth.AccountId, domainAccountID, userAuth)
if err != nil {
return "", err
}
- return claims.AccountId, nil
+ return userAuth.AccountId, nil
}
func handleNotFound(err error) error {
@@ -1517,11 +1456,16 @@ func handleNotFound(err error) error {
return nil
}
-func domainIsUpToDate(domain string, domainCategory string, claims jwtclaims.AuthorizationClaims) bool {
- return domainCategory == types.PrivateCategory || claims.DomainCategory != types.PrivateCategory || domain != claims.Domain
+func domainIsUpToDate(domain string, domainCategory string, userAuth nbcontext.UserAuth) bool {
+ return domainCategory == types.PrivateCategory || userAuth.DomainCategory != types.PrivateCategory || domain != userAuth.Domain
}
func (am *DefaultAccountManager) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) {
+ start := time.Now()
+ defer func() {
+ log.WithContext(ctx).Debugf("SyncAndMarkPeer: took %v", time.Since(start))
+ }()
+
accountUnlock := am.Store.AcquireReadLockByUID(ctx, accountID)
defer accountUnlock()
peerUnlock := am.Store.AcquireWriteLockByUID(ctx, peerPubKey)
@@ -1595,34 +1539,6 @@ func (am *DefaultAccountManager) GetDNSDomain() string {
return am.dnsDomain
}
-// CheckUserAccessByJWTGroups checks if the user has access, particularly in cases where the admin enabled JWT
-// group propagation and set the list of groups with access permissions.
-func (am *DefaultAccountManager) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error {
- accountID, _, err := am.GetAccountIDFromToken(ctx, claims)
- if err != nil {
- return err
- }
-
- settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
- if err != nil {
- return err
- }
-
- // Ensures JWT group synchronization to the management is enabled before,
- // filtering access based on the allowed groups.
- if settings != nil && settings.JWTGroupsEnabled {
- if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 {
- userJWTGroups := extractJWTGroups(ctx, settings.JWTGroupsClaimName, claims)
-
- if !userHasAllowedGroup(allowedGroups, userJWTGroups) {
- return fmt.Errorf("user does not belong to any of the allowed JWT groups")
- }
- }
- }
-
- return nil
-}
-
func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string) {
log.WithContext(ctx).Debugf("validated peers has been invalidated for account %s", accountID)
am.UpdateAccountPeers(ctx, accountID)
@@ -1690,46 +1606,6 @@ func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, account
return am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
}
-// addAllGroup to account object if it doesn't exist
-func addAllGroup(account *types.Account) error {
- if len(account.Groups) == 0 {
- allGroup := &types.Group{
- ID: xid.New().String(),
- Name: "All",
- Issued: types.GroupIssuedAPI,
- }
- for _, peer := range account.Peers {
- allGroup.Peers = append(allGroup.Peers, peer.ID)
- }
- account.Groups = map[string]*types.Group{allGroup.ID: allGroup}
-
- id := xid.New().String()
-
- defaultPolicy := &types.Policy{
- ID: id,
- Name: types.DefaultRuleName,
- Description: types.DefaultRuleDescription,
- Enabled: true,
- Rules: []*types.PolicyRule{
- {
- ID: id,
- Name: types.DefaultRuleName,
- Description: types.DefaultRuleDescription,
- Enabled: true,
- Sources: []string{allGroup.ID},
- Destinations: []string{allGroup.ID},
- Bidirectional: true,
- Protocol: types.PolicyRuleProtocolALL,
- Action: types.PolicyTrafficActionAccept,
- },
- },
- }
-
- account.Policies = []*types.Policy{defaultPolicy}
- }
- return nil
-}
-
// newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id
func newAccountWithId(ctx context.Context, accountID, userID, domain string) *types.Account {
log.WithContext(ctx).Debugf("creating new account")
@@ -1774,45 +1650,12 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty
},
}
- if err := addAllGroup(acc); err != nil {
+ if err := acc.AddAllGroup(); err != nil {
log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err)
}
return acc
}
-// extractJWTGroups extracts the group names from a JWT token's claims.
-func extractJWTGroups(ctx context.Context, claimName string, claims jwtclaims.AuthorizationClaims) []string {
- userJWTGroups := make([]string, 0)
-
- if claim, ok := claims.Raw[claimName]; ok {
- if claimGroups, ok := claim.([]interface{}); ok {
- for _, g := range claimGroups {
- if group, ok := g.(string); ok {
- userJWTGroups = append(userJWTGroups, group)
- } else {
- log.WithContext(ctx).Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g)
- }
- }
- }
- } else {
- log.WithContext(ctx).Debugf("JWT claim %q is not a string array", claimName)
- }
-
- return userJWTGroups
-}
-
-// userHasAllowedGroup checks if a user belongs to any of the allowed groups.
-func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
- for _, userGroup := range userGroups {
- for _, allowedGroup := range allowedGroups {
- if userGroup == allowedGroup {
- return true
- }
- }
- }
- return false
-}
-
// separateGroups separates user's auto groups into non-JWT and JWT groups.
// Returns the list of standard auto groups and a map of JWT auto groups,
// where the keys are the group names and the values are the group IDs.
diff --git a/management/server/account_test.go b/management/server/account_test.go
index 08bdc8821..f203e2066 100644
--- a/management/server/account_test.go
+++ b/management/server/account_test.go
@@ -2,8 +2,6 @@ package server
import (
"context"
- "crypto/sha256"
- b64 "encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -15,8 +13,6 @@ import (
"testing"
"time"
- "github.com/golang-jwt/jwt"
-
"github.com/netbirdio/netbird/management/server/util"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
@@ -30,7 +26,7 @@ import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server/activity"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/store"
@@ -437,7 +433,7 @@ func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) {
}
func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
- type initUserParams jwtclaims.AuthorizationClaims
+ type initUserParams nbcontext.UserAuth
var (
publicDomain = "public.com"
@@ -460,7 +456,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
testCases := []struct {
name string
- inputClaims jwtclaims.AuthorizationClaims
+ inputClaims nbcontext.UserAuth
inputInitUserParams initUserParams
inputUpdateAttrs bool
inputUpdateClaimAccount bool
@@ -475,7 +471,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
}{
{
name: "New User With Public Domain",
- inputClaims: jwtclaims.AuthorizationClaims{
+ inputClaims: nbcontext.UserAuth{
Domain: publicDomain,
UserId: "pub-domain-user",
DomainCategory: types.PublicCategory,
@@ -492,7 +488,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
},
{
name: "New User With Unknown Domain",
- inputClaims: jwtclaims.AuthorizationClaims{
+ inputClaims: nbcontext.UserAuth{
Domain: unknownDomain,
UserId: "unknown-domain-user",
DomainCategory: types.UnknownCategory,
@@ -509,7 +505,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
},
{
name: "New User With Private Domain",
- inputClaims: jwtclaims.AuthorizationClaims{
+ inputClaims: nbcontext.UserAuth{
Domain: privateDomain,
UserId: "pvt-domain-user",
DomainCategory: types.PrivateCategory,
@@ -526,7 +522,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
},
{
name: "New Regular User With Existing Private Domain",
- inputClaims: jwtclaims.AuthorizationClaims{
+ inputClaims: nbcontext.UserAuth{
Domain: privateDomain,
UserId: "new-pvt-domain-user",
DomainCategory: types.PrivateCategory,
@@ -544,7 +540,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
},
{
name: "Existing User With Existing Reclassified Private Domain",
- inputClaims: jwtclaims.AuthorizationClaims{
+ inputClaims: nbcontext.UserAuth{
Domain: defaultInitAccount.Domain,
UserId: defaultInitAccount.UserId,
DomainCategory: types.PrivateCategory,
@@ -561,7 +557,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
},
{
name: "Existing Account Id With Existing Reclassified Private Domain",
- inputClaims: jwtclaims.AuthorizationClaims{
+ inputClaims: nbcontext.UserAuth{
Domain: defaultInitAccount.Domain,
UserId: defaultInitAccount.UserId,
DomainCategory: types.PrivateCategory,
@@ -579,7 +575,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
},
{
name: "User With Private Category And Empty Domain",
- inputClaims: jwtclaims.AuthorizationClaims{
+ inputClaims: nbcontext.UserAuth{
Domain: "",
UserId: "pvt-domain-user",
DomainCategory: types.PrivateCategory,
@@ -608,7 +604,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
require.NoError(t, err, "get init account failed")
if testCase.inputUpdateAttrs {
- err = manager.updateAccountDomainAttributesIfNotUpToDate(context.Background(), initAccount.Id, jwtclaims.AuthorizationClaims{UserId: testCase.inputInitUserParams.UserId, Domain: testCase.inputInitUserParams.Domain, DomainCategory: testCase.inputInitUserParams.DomainCategory}, true)
+ err = manager.updateAccountDomainAttributesIfNotUpToDate(context.Background(), initAccount.Id, nbcontext.UserAuth{UserId: testCase.inputInitUserParams.UserId, Domain: testCase.inputInitUserParams.Domain, DomainCategory: testCase.inputInitUserParams.DomainCategory}, true)
require.NoError(t, err, "update init user failed")
}
@@ -616,7 +612,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
testCase.inputClaims.AccountId = initAccount.Id
}
- accountID, _, err = manager.GetAccountIDFromToken(context.Background(), testCase.inputClaims)
+ accountID, _, err = manager.GetAccountIDFromUserAuth(context.Background(), testCase.inputClaims)
require.NoError(t, err, "support function failed")
account, err := manager.Store.GetAccount(context.Background(), accountID)
@@ -635,14 +631,12 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
}
}
-func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
+func TestDefaultAccountManager_SyncUserJWTGroups(t *testing.T) {
userId := "user-id"
domain := "test.domain"
-
_ = newAccountWithId(context.Background(), "", userId, domain)
manager, err := createManager(t)
require.NoError(t, err, "unable to create account manager")
-
accountID, err := manager.GetAccountIDByUserID(context.Background(), userId, domain)
require.NoError(t, err, "create init user failed")
// as initAccount was created without account id we have to take the id after account initialization
@@ -650,65 +644,50 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
// it is important to set the id as it help to avoid creating additional account with empty Id and re-pointing indices to it
initAccount, err := manager.Store.GetAccount(context.Background(), accountID)
require.NoError(t, err, "get init account failed")
-
- claims := jwtclaims.AuthorizationClaims{
+ claims := nbcontext.UserAuth{
AccountId: accountID, // is empty as it is based on accountID right after initialization of initAccount
Domain: domain,
UserId: userId,
DomainCategory: "test-category",
- Raw: jwt.MapClaims{"idp-groups": []interface{}{"group1", "group2"}},
+ Groups: []string{"group1", "group2"},
}
-
t.Run("JWT groups disabled", func(t *testing.T) {
- accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims)
- require.NoError(t, err, "get account by token failed")
-
+ err := manager.SyncUserJWTGroups(context.Background(), claims)
+ require.NoError(t, err, "synt user jwt groups failed")
account, err := manager.Store.GetAccount(context.Background(), accountID)
require.NoError(t, err, "get account failed")
-
require.Len(t, account.Groups, 1, "only ALL group should exists")
})
-
t.Run("JWT groups enabled without claim name", func(t *testing.T) {
initAccount.Settings.JWTGroupsEnabled = true
err := manager.Store.SaveAccount(context.Background(), initAccount)
require.NoError(t, err, "save account failed")
require.Len(t, manager.Store.GetAllAccounts(context.Background()), 1, "only one account should exist")
-
- accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims)
- require.NoError(t, err, "get account by token failed")
-
+ err = manager.SyncUserJWTGroups(context.Background(), claims)
+ require.NoError(t, err, "synt user jwt groups failed")
account, err := manager.Store.GetAccount(context.Background(), accountID)
require.NoError(t, err, "get account failed")
-
require.Len(t, account.Groups, 1, "if group claim is not set no group added from JWT")
})
-
t.Run("JWT groups enabled", func(t *testing.T) {
initAccount.Settings.JWTGroupsEnabled = true
initAccount.Settings.JWTGroupsClaimName = "idp-groups"
err := manager.Store.SaveAccount(context.Background(), initAccount)
require.NoError(t, err, "save account failed")
require.Len(t, manager.Store.GetAllAccounts(context.Background()), 1, "only one account should exist")
-
- accountID, _, err := manager.GetAccountIDFromToken(context.Background(), claims)
- require.NoError(t, err, "get account by token failed")
-
+ err = manager.SyncUserJWTGroups(context.Background(), claims)
+ require.NoError(t, err, "synt user jwt groups failed")
account, err := manager.Store.GetAccount(context.Background(), accountID)
require.NoError(t, err, "get account failed")
-
require.Len(t, account.Groups, 3, "groups should be added to the account")
-
groupsByNames := map[string]*types.Group{}
for _, g := range account.Groups {
groupsByNames[g.Name] = g
}
-
g1, ok := groupsByNames["group1"]
require.True(t, ok, "group1 should be added to the account")
require.Equal(t, g1.Name, "group1", "group1 name should match")
require.Equal(t, g1.Issued, types.GroupIssuedJWT, "group1 issued should match")
-
g2, ok := groupsByNames["group2"]
require.True(t, ok, "group2 should be added to the account")
require.Equal(t, g2.Name, "group2", "group2 name should match")
@@ -716,88 +695,6 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
})
}
-func TestAccountManager_GetAccountFromPAT(t *testing.T) {
- store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
- if err != nil {
- t.Fatalf("Error when creating store: %s", err)
- }
- t.Cleanup(cleanup)
- account := newAccountWithId(context.Background(), "account_id", "testuser", "")
-
- token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
- hashedToken := sha256.Sum256([]byte(token))
- encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:])
- account.Users["someUser"] = &types.User{
- Id: "someUser",
- PATs: map[string]*types.PersonalAccessToken{
- "tokenId": {
- ID: "tokenId",
- UserID: "someUser",
- HashedToken: encodedHashedToken,
- },
- },
- }
- err = store.SaveAccount(context.Background(), account)
- if err != nil {
- t.Fatalf("Error when saving account: %s", err)
- }
-
- am := DefaultAccountManager{
- Store: store,
- }
-
- user, pat, _, _, err := am.GetAccountInfoFromPAT(context.Background(), token)
- if err != nil {
- t.Fatalf("Error when getting Account from PAT: %s", err)
- }
-
- assert.Equal(t, "account_id", user.AccountID)
- assert.Equal(t, "someUser", user.Id)
- assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID)
-}
-
-func TestDefaultAccountManager_MarkPATUsed(t *testing.T) {
- store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
- if err != nil {
- t.Fatalf("Error when creating store: %s", err)
- }
- t.Cleanup(cleanup)
-
- account := newAccountWithId(context.Background(), "account_id", "testuser", "")
-
- token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
- hashedToken := sha256.Sum256([]byte(token))
- encodedHashedToken := b64.StdEncoding.EncodeToString(hashedToken[:])
- account.Users["someUser"] = &types.User{
- Id: "someUser",
- PATs: map[string]*types.PersonalAccessToken{
- "tokenId": {
- ID: "tokenId",
- HashedToken: encodedHashedToken,
- },
- },
- }
- err = store.SaveAccount(context.Background(), account)
- if err != nil {
- t.Fatalf("Error when saving account: %s", err)
- }
-
- am := DefaultAccountManager{
- Store: store,
- }
-
- err = am.MarkPATUsed(context.Background(), "tokenId")
- if err != nil {
- t.Fatalf("Error when marking PAT used: %s", err)
- }
-
- account, err = am.Store.GetAccount(context.Background(), "account_id")
- if err != nil {
- t.Fatalf("Error when getting account: %s", err)
- }
- assert.True(t, !account.Users["someUser"].PATs["tokenId"].GetLastUsed().IsZero())
-}
-
func TestAccountManager_PrivateAccount(t *testing.T) {
manager, err := createManager(t)
if err != nil {
@@ -962,13 +859,13 @@ func TestAccountManager_DeleteAccount(t *testing.T) {
}
func BenchmarkTest_GetAccountWithclaims(b *testing.B) {
- claims := jwtclaims.AuthorizationClaims{
+ claims := nbcontext.UserAuth{
Domain: "example.com",
UserId: "pvt-domain-user",
DomainCategory: types.PrivateCategory,
}
- publicClaims := jwtclaims.AuthorizationClaims{
+ publicClaims := nbcontext.UserAuth{
Domain: "test.com",
UserId: "public-domain-user",
DomainCategory: types.PublicCategory,
@@ -1080,7 +977,7 @@ func TestAccountManager_AddPeer(t *testing.T) {
serial := account.Network.CurrentSerial() // should be 0
- setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
+ setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
if err != nil {
t.Fatal("error creating setup key")
return
@@ -1456,7 +1353,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
t.Fatal(err)
}
- setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
+ setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
if err != nil {
t.Fatal("error creating setup key")
return
@@ -2683,11 +2580,13 @@ func TestAccount_SetJWTGroups(t *testing.T) {
assert.NoError(t, manager.Store.SaveAccount(context.Background(), account), "unable to save account")
t.Run("skip sync for token auth type", func(t *testing.T) {
- claims := jwtclaims.AuthorizationClaims{
- UserId: "user1",
- Raw: jwt.MapClaims{"groups": []interface{}{"group3"}, "is_token": true},
+ claims := nbcontext.UserAuth{
+ UserId: "user1",
+ AccountId: "accountID",
+ Groups: []string{"group3"},
+ IsPAT: true,
}
- err = manager.syncJWTGroups(context.Background(), "accountID", claims)
+ err = manager.SyncUserJWTGroups(context.Background(), claims)
assert.NoError(t, err, "unable to sync jwt groups")
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
@@ -2696,11 +2595,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
})
t.Run("empty jwt groups", func(t *testing.T) {
- claims := jwtclaims.AuthorizationClaims{
- UserId: "user1",
- Raw: jwt.MapClaims{"groups": []interface{}{}},
+ claims := nbcontext.UserAuth{
+ UserId: "user1",
+ AccountId: "accountID",
+ Groups: []string{},
}
- err := manager.syncJWTGroups(context.Background(), "accountID", claims)
+ err := manager.SyncUserJWTGroups(context.Background(), claims)
assert.NoError(t, err, "unable to sync jwt groups")
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
@@ -2709,11 +2609,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
})
t.Run("jwt match existing api group", func(t *testing.T) {
- claims := jwtclaims.AuthorizationClaims{
- UserId: "user1",
- Raw: jwt.MapClaims{"groups": []interface{}{"group1"}},
+ claims := nbcontext.UserAuth{
+ UserId: "user1",
+ AccountId: "accountID",
+ Groups: []string{"group1"},
}
- err := manager.syncJWTGroups(context.Background(), "accountID", claims)
+ err := manager.SyncUserJWTGroups(context.Background(), claims)
assert.NoError(t, err, "unable to sync jwt groups")
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
@@ -2729,11 +2630,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
account.Users["user1"].AutoGroups = []string{"group1"}
assert.NoError(t, manager.Store.SaveUser(context.Background(), store.LockingStrengthUpdate, account.Users["user1"]))
- claims := jwtclaims.AuthorizationClaims{
- UserId: "user1",
- Raw: jwt.MapClaims{"groups": []interface{}{"group1"}},
+ claims := nbcontext.UserAuth{
+ UserId: "user1",
+ AccountId: "accountID",
+ Groups: []string{"group1"},
}
- err = manager.syncJWTGroups(context.Background(), "accountID", claims)
+ err = manager.SyncUserJWTGroups(context.Background(), claims)
assert.NoError(t, err, "unable to sync jwt groups")
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
@@ -2746,11 +2648,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
})
t.Run("add jwt group", func(t *testing.T) {
- claims := jwtclaims.AuthorizationClaims{
- UserId: "user1",
- Raw: jwt.MapClaims{"groups": []interface{}{"group1", "group2"}},
+ claims := nbcontext.UserAuth{
+ UserId: "user1",
+ AccountId: "accountID",
+ Groups: []string{"group1", "group2"},
}
- err = manager.syncJWTGroups(context.Background(), "accountID", claims)
+ err = manager.SyncUserJWTGroups(context.Background(), claims)
assert.NoError(t, err, "unable to sync jwt groups")
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
@@ -2759,11 +2662,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
})
t.Run("existed group not update", func(t *testing.T) {
- claims := jwtclaims.AuthorizationClaims{
- UserId: "user1",
- Raw: jwt.MapClaims{"groups": []interface{}{"group2"}},
+ claims := nbcontext.UserAuth{
+ UserId: "user1",
+ AccountId: "accountID",
+ Groups: []string{"group2"},
}
- err = manager.syncJWTGroups(context.Background(), "accountID", claims)
+ err = manager.SyncUserJWTGroups(context.Background(), claims)
assert.NoError(t, err, "unable to sync jwt groups")
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
@@ -2772,11 +2676,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
})
t.Run("add new group", func(t *testing.T) {
- claims := jwtclaims.AuthorizationClaims{
- UserId: "user2",
- Raw: jwt.MapClaims{"groups": []interface{}{"group1", "group3"}},
+ claims := nbcontext.UserAuth{
+ UserId: "user2",
+ AccountId: "accountID",
+ Groups: []string{"group1", "group3"},
}
- err = manager.syncJWTGroups(context.Background(), "accountID", claims)
+ err = manager.SyncUserJWTGroups(context.Background(), claims)
assert.NoError(t, err, "unable to sync jwt groups")
groups, err := manager.Store.GetAccountGroups(context.Background(), store.LockingStrengthShare, "accountID")
@@ -2789,11 +2694,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
})
t.Run("remove all JWT groups when list is empty", func(t *testing.T) {
- claims := jwtclaims.AuthorizationClaims{
- UserId: "user1",
- Raw: jwt.MapClaims{"groups": []interface{}{}},
+ claims := nbcontext.UserAuth{
+ UserId: "user1",
+ AccountId: "accountID",
+ Groups: []string{},
}
- err = manager.syncJWTGroups(context.Background(), "accountID", claims)
+ err = manager.SyncUserJWTGroups(context.Background(), claims)
assert.NoError(t, err, "unable to sync jwt groups")
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user1")
@@ -2803,11 +2709,12 @@ func TestAccount_SetJWTGroups(t *testing.T) {
})
t.Run("remove all JWT groups when claim does not exist", func(t *testing.T) {
- claims := jwtclaims.AuthorizationClaims{
- UserId: "user2",
- Raw: jwt.MapClaims{},
+ claims := nbcontext.UserAuth{
+ UserId: "user2",
+ AccountId: "accountID",
+ Groups: []string{},
}
- err = manager.syncJWTGroups(context.Background(), "accountID", claims)
+ err = manager.SyncUserJWTGroups(context.Background(), claims)
assert.NoError(t, err, "unable to sync jwt groups")
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthShare, "user2")
@@ -2948,7 +2855,7 @@ func setupNetworkMapTest(t *testing.T) (*DefaultAccountManager, *types.Account,
t.Fatal(err)
}
- setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
+ setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
if err != nil {
t.Fatal("error creating setup key")
}
@@ -3006,6 +2913,8 @@ func peerShouldReceiveUpdate(t *testing.T, updateMessage <-chan *UpdateMessage)
}
func BenchmarkSyncAndMarkPeer(b *testing.B) {
+ b.Setenv("NB_GET_ACCOUNT_BUFFER_INTERVAL", "0")
+
benchCases := []struct {
name string
peers int
@@ -3016,11 +2925,11 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) {
minMsPerOpCICD float64
maxMsPerOpCICD float64
}{
- {"Small", 50, 5, 1, 3, 3, 19},
- {"Medium", 500, 100, 7, 13, 10, 90},
- {"Large", 5000, 200, 65, 80, 60, 240},
- {"Small single", 50, 10, 1, 3, 3, 80},
- {"Medium single", 500, 10, 7, 13, 10, 37},
+ {"Small", 50, 5, 1, 5, 3, 24},
+ {"Medium", 500, 100, 7, 22, 10, 135},
+ {"Large", 5000, 200, 65, 110, 60, 320},
+ {"Small single", 50, 10, 1, 4, 3, 80},
+ {"Medium single", 500, 10, 7, 13, 10, 43},
{"Large 5", 5000, 15, 65, 80, 60, 220},
}
@@ -3073,6 +2982,7 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) {
}
func BenchmarkLoginPeer_ExistingPeer(b *testing.B) {
+ b.Setenv("NB_GET_ACCOUNT_BUFFER_INTERVAL", "0")
benchCases := []struct {
name string
peers int
@@ -3083,12 +2993,12 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) {
minMsPerOpCICD float64
maxMsPerOpCICD float64
}{
- {"Small", 50, 5, 102, 110, 3, 20},
- {"Medium", 500, 100, 105, 140, 20, 110},
- {"Large", 5000, 200, 160, 200, 120, 260},
- {"Small single", 50, 10, 102, 110, 5, 40},
- {"Medium single", 500, 10, 105, 140, 10, 60},
- {"Large 5", 5000, 15, 160, 200, 60, 180},
+ {"Small", 50, 5, 2, 10, 3, 35},
+ {"Medium", 500, 100, 5, 40, 20, 140},
+ {"Large", 5000, 200, 60, 100, 120, 320},
+ {"Small single", 50, 10, 2, 10, 5, 40},
+ {"Medium single", 500, 10, 5, 40, 10, 60},
+ {"Large 5", 5000, 15, 60, 100, 60, 180},
}
log.SetOutput(io.Discard)
@@ -3147,6 +3057,7 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) {
}
func BenchmarkLoginPeer_NewPeer(b *testing.B) {
+ b.Setenv("NB_GET_ACCOUNT_BUFFER_INTERVAL", "0")
benchCases := []struct {
name string
peers int
@@ -3157,12 +3068,12 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) {
minMsPerOpCICD float64
maxMsPerOpCICD float64
}{
- {"Small", 50, 5, 107, 120, 10, 80},
- {"Medium", 500, 100, 105, 140, 30, 140},
- {"Large", 5000, 200, 180, 220, 140, 300},
- {"Small single", 50, 10, 107, 120, 10, 80},
- {"Medium single", 500, 10, 105, 140, 20, 60},
- {"Large 5", 5000, 15, 180, 220, 80, 200},
+ {"Small", 50, 5, 7, 20, 10, 80},
+ {"Medium", 500, 100, 5, 40, 30, 140},
+ {"Large", 5000, 200, 80, 120, 140, 390},
+ {"Small single", 50, 10, 7, 20, 10, 80},
+ {"Medium single", 500, 10, 5, 40, 20, 85},
+ {"Large 5", 5000, 15, 80, 120, 80, 200},
}
log.SetOutput(io.Discard)
diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go
index 823e0b4ac..ffb863de9 100644
--- a/management/server/activity/sqlite/sqlite.go
+++ b/management/server/activity/sqlite/sqlite.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"path/filepath"
+ "runtime"
"time"
_ "github.com/mattn/go-sqlite3"
@@ -95,6 +96,7 @@ func NewSQLiteStore(ctx context.Context, dataDir string, encryptionKey string) (
if err != nil {
return nil, err
}
+ db.SetMaxOpenConns(runtime.NumCPU())
crypt, err := NewFieldEncrypt(encryptionKey)
if err != nil {
diff --git a/management/server/jwtclaims/extractor.go b/management/server/auth/jwt/extractor.go
similarity index 51%
rename from management/server/jwtclaims/extractor.go
rename to management/server/auth/jwt/extractor.go
index 18214b434..fab429125 100644
--- a/management/server/jwtclaims/extractor.go
+++ b/management/server/auth/jwt/extractor.go
@@ -1,15 +1,17 @@
-package jwtclaims
+package jwt
import (
- "net/http"
+ "errors"
+ "net/url"
"time"
"github.com/golang-jwt/jwt"
+ log "github.com/sirupsen/logrus"
+
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
)
const (
- // TokenUserProperty key for the user property in the request context
- TokenUserProperty = "user"
// AccountIDSuffix suffix for the account id claim
AccountIDSuffix = "wt_account_id"
// DomainIDSuffix suffix for the domain id claim
@@ -22,19 +24,16 @@ const (
LastLoginSuffix = "nb_last_login"
// Invited claim indicates that an incoming JWT is from a user that just accepted an invitation
Invited = "nb_invited"
- // IsToken claim indicates that auth type from the user is a token
- IsToken = "is_token"
)
-// ExtractClaims Extract function type
-type ExtractClaims func(r *http.Request) AuthorizationClaims
+var (
+ errUserIDClaimEmpty = errors.New("user ID claim token value is empty")
+)
// ClaimsExtractor struct that holds the extract function
type ClaimsExtractor struct {
authAudience string
userIDClaim string
-
- FromRequestContext ExtractClaims
}
// ClaimsExtractorOption is a function that configures the ClaimsExtractor
@@ -54,13 +53,6 @@ func WithUserIDClaim(userIDClaim string) ClaimsExtractorOption {
}
}
-// WithFromRequestContext sets the function that extracts claims from the request context
-func WithFromRequestContext(ec ExtractClaims) ClaimsExtractorOption {
- return func(c *ClaimsExtractor) {
- c.FromRequestContext = ec
- }
-}
-
// NewClaimsExtractor returns an extractor, and if provided with a function with ExtractClaims signature,
// then it will use that logic. Uses ExtractClaimsFromRequestContext by default
func NewClaimsExtractor(options ...ClaimsExtractorOption) *ClaimsExtractor {
@@ -68,49 +60,13 @@ func NewClaimsExtractor(options ...ClaimsExtractorOption) *ClaimsExtractor {
for _, option := range options {
option(ce)
}
- if ce.FromRequestContext == nil {
- ce.FromRequestContext = ce.fromRequestContext
- }
+
if ce.userIDClaim == "" {
ce.userIDClaim = UserIDClaim
}
return ce
}
-// FromToken extracts claims from the token (after auth)
-func (c *ClaimsExtractor) FromToken(token *jwt.Token) AuthorizationClaims {
- claims := token.Claims.(jwt.MapClaims)
- jwtClaims := AuthorizationClaims{
- Raw: claims,
- }
- userID, ok := claims[c.userIDClaim].(string)
- if !ok {
- return jwtClaims
- }
- jwtClaims.UserId = userID
- accountIDClaim, ok := claims[c.authAudience+AccountIDSuffix]
- if ok {
- jwtClaims.AccountId = accountIDClaim.(string)
- }
- domainClaim, ok := claims[c.authAudience+DomainIDSuffix]
- if ok {
- jwtClaims.Domain = domainClaim.(string)
- }
- domainCategoryClaim, ok := claims[c.authAudience+DomainCategorySuffix]
- if ok {
- jwtClaims.DomainCategory = domainCategoryClaim.(string)
- }
- LastLoginClaimString, ok := claims[c.authAudience+LastLoginSuffix]
- if ok {
- jwtClaims.LastLogin = parseTime(LastLoginClaimString.(string))
- }
- invitedBool, ok := claims[c.authAudience+Invited]
- if ok {
- jwtClaims.Invited = invitedBool.(bool)
- }
- return jwtClaims
-}
-
func parseTime(timeString string) time.Time {
if timeString == "" {
return time.Time{}
@@ -122,11 +78,67 @@ func parseTime(timeString string) time.Time {
return parsedTime
}
-// fromRequestContext extracts claims from the request context previously filled by the JWT token (after auth)
-func (c *ClaimsExtractor) fromRequestContext(r *http.Request) AuthorizationClaims {
- if r.Context().Value(TokenUserProperty) == nil {
- return AuthorizationClaims{}
+func (c ClaimsExtractor) audienceClaim(claimName string) string {
+ url, err := url.JoinPath(c.authAudience, claimName)
+ if err != nil {
+ return c.authAudience + claimName // as it was previously
}
- token := r.Context().Value(TokenUserProperty).(*jwt.Token)
- return c.FromToken(token)
+
+ return url
+}
+
+func (c *ClaimsExtractor) ToUserAuth(token *jwt.Token) (nbcontext.UserAuth, error) {
+ claims := token.Claims.(jwt.MapClaims)
+ userAuth := nbcontext.UserAuth{}
+
+ userID, ok := claims[c.userIDClaim].(string)
+ if !ok {
+ return userAuth, errUserIDClaimEmpty
+ }
+ userAuth.UserId = userID
+
+ if accountIDClaim, ok := claims[c.audienceClaim(AccountIDSuffix)]; ok {
+ userAuth.AccountId = accountIDClaim.(string)
+ }
+
+ if domainClaim, ok := claims[c.audienceClaim(DomainIDSuffix)]; ok {
+ userAuth.Domain = domainClaim.(string)
+ }
+
+ if domainCategoryClaim, ok := claims[c.audienceClaim(DomainCategorySuffix)]; ok {
+ userAuth.DomainCategory = domainCategoryClaim.(string)
+ }
+
+ if lastLoginClaimString, ok := claims[c.audienceClaim(LastLoginSuffix)]; ok {
+ userAuth.LastLogin = parseTime(lastLoginClaimString.(string))
+ }
+
+ if invitedBool, ok := claims[c.audienceClaim(Invited)]; ok {
+ if value, ok := invitedBool.(bool); ok {
+ userAuth.Invited = value
+ }
+ }
+
+ return userAuth, nil
+}
+
+func (c *ClaimsExtractor) ToGroups(token *jwt.Token, claimName string) []string {
+ claims := token.Claims.(jwt.MapClaims)
+ userJWTGroups := make([]string, 0)
+
+ if claim, ok := claims[claimName]; ok {
+ if claimGroups, ok := claim.([]interface{}); ok {
+ for _, g := range claimGroups {
+ if group, ok := g.(string); ok {
+ userJWTGroups = append(userJWTGroups, group)
+ } else {
+ log.Debugf("JWT claim %q contains a non-string group (type: %T): %v", claimName, g, g)
+ }
+ }
+ }
+ } else {
+ log.Debugf("JWT claim %q is not a string array", claimName)
+ }
+
+ return userJWTGroups
}
diff --git a/management/server/auth/jwt/validator.go b/management/server/auth/jwt/validator.go
new file mode 100644
index 000000000..5b38ca786
--- /dev/null
+++ b/management/server/auth/jwt/validator.go
@@ -0,0 +1,302 @@
+package jwt
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rsa"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math/big"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/golang-jwt/jwt"
+
+ log "github.com/sirupsen/logrus"
+)
+
+// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation
+type Jwks struct {
+ Keys []JSONWebKey `json:"keys"`
+ expiresInTime time.Time
+}
+
+// The supported elliptic curves types
+const (
+ // p256 represents a cryptographic elliptical curve type.
+ p256 = "P-256"
+
+ // p384 represents a cryptographic elliptical curve type.
+ p384 = "P-384"
+
+ // p521 represents a cryptographic elliptical curve type.
+ p521 = "P-521"
+)
+
+// JSONWebKey is a representation of a Jason Web Key
+type JSONWebKey struct {
+ Kty string `json:"kty"`
+ Kid string `json:"kid"`
+ Use string `json:"use"`
+ N string `json:"n"`
+ E string `json:"e"`
+ Crv string `json:"crv"`
+ X string `json:"x"`
+ Y string `json:"y"`
+ X5c []string `json:"x5c"`
+}
+
+type Validator struct {
+ lock sync.Mutex
+ issuer string
+ audienceList []string
+ keysLocation string
+ idpSignkeyRefreshEnabled bool
+ keys *Jwks
+}
+
+var (
+ errKeyNotFound = errors.New("unable to find appropriate key")
+ errInvalidAudience = errors.New("invalid audience")
+ errInvalidIssuer = errors.New("invalid issuer")
+ errTokenEmpty = errors.New("required authorization token not found")
+ errTokenInvalid = errors.New("token is invalid")
+ errTokenParsing = errors.New("token could not be parsed")
+)
+
+func NewValidator(issuer string, audienceList []string, keysLocation string, idpSignkeyRefreshEnabled bool) *Validator {
+ keys, err := getPemKeys(keysLocation)
+ if err != nil {
+ log.WithField("keysLocation", keysLocation).Errorf("could not get keys from location: %s", err)
+ }
+
+ return &Validator{
+ keys: keys,
+ issuer: issuer,
+ audienceList: audienceList,
+ keysLocation: keysLocation,
+ idpSignkeyRefreshEnabled: idpSignkeyRefreshEnabled,
+ }
+}
+
+func (v *Validator) getKeyFunc(ctx context.Context) jwt.Keyfunc {
+ return func(token *jwt.Token) (interface{}, error) {
+ // Verify 'aud' claim
+ var checkAud bool
+ for _, audience := range v.audienceList {
+ checkAud = token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
+ if checkAud {
+ break
+ }
+ }
+ if !checkAud {
+ return token, errInvalidAudience
+ }
+
+ // Verify 'issuer' claim
+ checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(v.issuer, false)
+ if !checkIss {
+ return token, errInvalidIssuer
+ }
+
+ // If keys are rotated, verify the keys prior to token validation
+ if v.idpSignkeyRefreshEnabled {
+ // If the keys are invalid, retrieve new ones
+ // @todo propose a separate go routine to regularly check these to prevent blocking when actually
+ // validating the token
+ if !v.keys.stillValid() {
+ v.lock.Lock()
+ defer v.lock.Unlock()
+
+ refreshedKeys, err := getPemKeys(v.keysLocation)
+ if err != nil {
+ log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
+ refreshedKeys = v.keys
+ }
+
+ log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
+
+ v.keys = refreshedKeys
+ }
+ }
+
+ publicKey, err := getPublicKey(token, v.keys)
+ if err == nil {
+ return publicKey, nil
+ }
+
+ msg := fmt.Sprintf("getPublicKey error: %s", err)
+ if errors.Is(err, errKeyNotFound) && !v.idpSignkeyRefreshEnabled {
+ msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err)
+ }
+
+ log.WithContext(ctx).Error(msg)
+
+ return nil, err
+ }
+}
+
+// ValidateAndParse validates the token and returns the parsed token
+func (m *Validator) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
+ // If the token is empty...
+ if token == "" {
+ // If we get here, the required token is missing
+ log.WithContext(ctx).Debugf(" Error: No credentials found (CredentialsOptional=false)")
+ return nil, errTokenEmpty
+ }
+
+ // Now parse the token
+ parsedToken, err := jwt.Parse(token, m.getKeyFunc(ctx))
+
+ // Check if there was an error in parsing...
+ if err != nil {
+ err = fmt.Errorf("%w: %s", errTokenParsing, err)
+ log.WithContext(ctx).Error(err.Error())
+ return nil, err
+ }
+
+ // Check if the parsed token is valid...
+ if !parsedToken.Valid {
+ log.WithContext(ctx).Debug(errTokenInvalid.Error())
+ return nil, errTokenInvalid
+ }
+
+ return parsedToken, nil
+}
+
+// stillValid returns true if the JSONWebKey still valid and have enough time to be used
+func (jwks *Jwks) stillValid() bool {
+ return !jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime)
+}
+
+func getPemKeys(keysLocation string) (*Jwks, error) {
+ jwks := &Jwks{}
+
+ url, err := url.ParseRequestURI(keysLocation)
+ if err != nil {
+ return jwks, err
+ }
+
+ resp, err := http.Get(url.String())
+ if err != nil {
+ return jwks, err
+ }
+ defer resp.Body.Close()
+
+ err = json.NewDecoder(resp.Body).Decode(jwks)
+ if err != nil {
+ return jwks, err
+ }
+
+ cacheControlHeader := resp.Header.Get("Cache-Control")
+ expiresIn := getMaxAgeFromCacheHeader(cacheControlHeader)
+ jwks.expiresInTime = time.Now().Add(time.Duration(expiresIn) * time.Second)
+
+ return jwks, nil
+}
+
+func getPublicKey(token *jwt.Token, jwks *Jwks) (interface{}, error) {
+ // todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time
+ for k := range jwks.Keys {
+ if token.Header["kid"] != jwks.Keys[k].Kid {
+ continue
+ }
+
+ if len(jwks.Keys[k].X5c) != 0 {
+ cert := "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
+ return jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
+ }
+
+ if jwks.Keys[k].Kty == "RSA" {
+ return getPublicKeyFromRSA(jwks.Keys[k])
+ }
+ if jwks.Keys[k].Kty == "EC" {
+ return getPublicKeyFromECDSA(jwks.Keys[k])
+ }
+ }
+
+ return nil, errKeyNotFound
+}
+
+func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) {
+ if jwk.X == "" || jwk.Y == "" || jwk.Crv == "" {
+ return nil, fmt.Errorf("ecdsa key incomplete")
+ }
+
+ var xCoordinate []byte
+ if xCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.X); err != nil {
+ return nil, err
+ }
+
+ var yCoordinate []byte
+ if yCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.Y); err != nil {
+ return nil, err
+ }
+
+ publicKey = &ecdsa.PublicKey{}
+
+ var curve elliptic.Curve
+ switch jwk.Crv {
+ case p256:
+ curve = elliptic.P256()
+ case p384:
+ curve = elliptic.P384()
+ case p521:
+ curve = elliptic.P521()
+ }
+
+ publicKey.Curve = curve
+ publicKey.X = big.NewInt(0).SetBytes(xCoordinate)
+ publicKey.Y = big.NewInt(0).SetBytes(yCoordinate)
+
+ return publicKey, nil
+}
+
+func getPublicKeyFromRSA(jwk JSONWebKey) (*rsa.PublicKey, error) {
+ decodedE, err := base64.RawURLEncoding.DecodeString(jwk.E)
+ if err != nil {
+ return nil, err
+ }
+ decodedN, err := base64.RawURLEncoding.DecodeString(jwk.N)
+ if err != nil {
+ return nil, err
+ }
+
+ var n, e big.Int
+ e.SetBytes(decodedE)
+ n.SetBytes(decodedN)
+
+ return &rsa.PublicKey{
+ E: int(e.Int64()),
+ N: &n,
+ }, nil
+}
+
+// getMaxAgeFromCacheHeader extracts max-age directive from the Cache-Control header
+func getMaxAgeFromCacheHeader(cacheControl string) int {
+ // Split into individual directives
+ directives := strings.Split(cacheControl, ",")
+
+ for _, directive := range directives {
+ directive = strings.TrimSpace(directive)
+ if strings.HasPrefix(directive, "max-age=") {
+ // Extract the max-age value
+ maxAgeStr := strings.TrimPrefix(directive, "max-age=")
+ maxAge, err := strconv.Atoi(maxAgeStr)
+ if err != nil {
+ return 0
+ }
+
+ return maxAge
+ }
+ }
+
+ return 0
+}
diff --git a/management/server/auth/manager.go b/management/server/auth/manager.go
new file mode 100644
index 000000000..6835a3ced
--- /dev/null
+++ b/management/server/auth/manager.go
@@ -0,0 +1,170 @@
+package auth
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "hash/crc32"
+
+ "github.com/golang-jwt/jwt"
+
+ "github.com/netbirdio/netbird/base62"
+ nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
+ "github.com/netbirdio/netbird/management/server/store"
+ "github.com/netbirdio/netbird/management/server/types"
+)
+
+var _ Manager = (*manager)(nil)
+
+type Manager interface {
+ ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error)
+ EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error)
+ MarkPATUsed(ctx context.Context, tokenID string) error
+ GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
+}
+
+type manager struct {
+ store store.Store
+
+ validator *nbjwt.Validator
+ extractor *nbjwt.ClaimsExtractor
+}
+
+func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim string, allAudiences []string, idpRefreshKeys bool) Manager {
+ // @note if invalid/missing parameters are sent the validator will instantiate
+ // but it will fail when validating and parsing the token
+ jwtValidator := nbjwt.NewValidator(
+ issuer,
+ allAudiences,
+ keysLocation,
+ idpRefreshKeys,
+ )
+
+ claimsExtractor := nbjwt.NewClaimsExtractor(
+ nbjwt.WithAudience(audience),
+ nbjwt.WithUserIDClaim(userIdClaim),
+ )
+
+ return &manager{
+ store: store,
+
+ validator: jwtValidator,
+ extractor: claimsExtractor,
+ }
+}
+
+func (m *manager) ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) {
+ token, err := m.validator.ValidateAndParse(ctx, value)
+ if err != nil {
+ return nbcontext.UserAuth{}, nil, err
+ }
+
+ userAuth, err := m.extractor.ToUserAuth(token)
+ if err != nil {
+ return nbcontext.UserAuth{}, nil, err
+ }
+ return userAuth, token, err
+}
+
+func (m *manager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) {
+ if userAuth.IsChild || userAuth.IsPAT {
+ return userAuth, nil
+ }
+
+ settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthShare, userAuth.AccountId)
+ if err != nil {
+ return userAuth, err
+ }
+
+ // Ensures JWT group synchronization to the management is enabled before,
+ // filtering access based on the allowed groups.
+ if settings != nil && settings.JWTGroupsEnabled {
+ userAuth.Groups = m.extractor.ToGroups(token, settings.JWTGroupsClaimName)
+ if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 {
+ if !userHasAllowedGroup(allowedGroups, userAuth.Groups) {
+ return userAuth, fmt.Errorf("user does not belong to any of the allowed JWT groups")
+ }
+ }
+ }
+
+ return userAuth, nil
+}
+
+// MarkPATUsed marks a personal access token as used
+func (am *manager) MarkPATUsed(ctx context.Context, tokenID string) error {
+ return am.store.MarkPATUsed(ctx, store.LockingStrengthUpdate, tokenID)
+}
+
+// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token.
+func (am *manager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
+ user, pat, err = am.extractPATFromToken(ctx, token)
+ if err != nil {
+ return nil, nil, "", "", err
+ }
+
+ domain, category, err = am.store.GetAccountDomainAndCategory(ctx, store.LockingStrengthShare, user.AccountID)
+ if err != nil {
+ return nil, nil, "", "", err
+ }
+
+ return user, pat, domain, category, nil
+}
+
+// extractPATFromToken validates the token structure and retrieves associated User and PAT.
+func (am *manager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) {
+ if len(token) != types.PATLength {
+ return nil, nil, fmt.Errorf("PAT has incorrect length")
+ }
+
+ prefix := token[:len(types.PATPrefix)]
+ if prefix != types.PATPrefix {
+ return nil, nil, fmt.Errorf("PAT has wrong prefix")
+ }
+ secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength]
+ encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength]
+
+ verificationChecksum, err := base62.Decode(encodedChecksum)
+ if err != nil {
+ return nil, nil, fmt.Errorf("PAT checksum decoding failed: %w", err)
+ }
+
+ secretChecksum := crc32.ChecksumIEEE([]byte(secret))
+ if secretChecksum != verificationChecksum {
+ return nil, nil, fmt.Errorf("PAT checksum does not match")
+ }
+
+ hashedToken := sha256.Sum256([]byte(token))
+ encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
+
+ var user *types.User
+ var pat *types.PersonalAccessToken
+
+ err = am.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
+ pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthShare, encodedHashedToken)
+ if err != nil {
+ return err
+ }
+
+ user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthShare, pat.ID)
+ return err
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return user, pat, nil
+}
+
+// userHasAllowedGroup checks if a user belongs to any of the allowed groups.
+func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
+ for _, userGroup := range userGroups {
+ for _, allowedGroup := range allowedGroups {
+ if userGroup == allowedGroup {
+ return true
+ }
+ }
+ }
+ return false
+}
diff --git a/management/server/auth/manager_mock.go b/management/server/auth/manager_mock.go
new file mode 100644
index 000000000..bc7066548
--- /dev/null
+++ b/management/server/auth/manager_mock.go
@@ -0,0 +1,54 @@
+package auth
+
+import (
+ "context"
+
+ "github.com/golang-jwt/jwt"
+
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
+ "github.com/netbirdio/netbird/management/server/types"
+)
+
+var (
+ _ Manager = (*MockManager)(nil)
+)
+
+// @note really dislike this mocking approach but rather than have to do additional test refactoring.
+type MockManager struct {
+ ValidateAndParseTokenFunc func(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error)
+ EnsureUserAccessByJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error)
+ MarkPATUsedFunc func(ctx context.Context, tokenID string) error
+ GetPATInfoFunc func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
+}
+
+// EnsureUserAccessByJWTGroups implements Manager.
+func (m *MockManager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) {
+ if m.EnsureUserAccessByJWTGroupsFunc != nil {
+ return m.EnsureUserAccessByJWTGroupsFunc(ctx, userAuth, token)
+ }
+ return nbcontext.UserAuth{}, nil
+}
+
+// GetPATInfo implements Manager.
+func (m *MockManager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
+ if m.GetPATInfoFunc != nil {
+ return m.GetPATInfoFunc(ctx, token)
+ }
+ return &types.User{}, &types.PersonalAccessToken{}, "", "", nil
+}
+
+// MarkPATUsed implements Manager.
+func (m *MockManager) MarkPATUsed(ctx context.Context, tokenID string) error {
+ if m.MarkPATUsedFunc != nil {
+ return m.MarkPATUsedFunc(ctx, tokenID)
+ }
+ return nil
+}
+
+// ValidateAndParseToken implements Manager.
+func (m *MockManager) ValidateAndParseToken(ctx context.Context, value string) (nbcontext.UserAuth, *jwt.Token, error) {
+ if m.ValidateAndParseTokenFunc != nil {
+ return m.ValidateAndParseTokenFunc(ctx, value)
+ }
+ return nbcontext.UserAuth{}, &jwt.Token{}, nil
+}
diff --git a/management/server/auth/manager_test.go b/management/server/auth/manager_test.go
new file mode 100644
index 000000000..55fb1e31a
--- /dev/null
+++ b/management/server/auth/manager_test.go
@@ -0,0 +1,407 @@
+package auth_test
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/golang-jwt/jwt"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/netbirdio/netbird/management/server/auth"
+ nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
+ "github.com/netbirdio/netbird/management/server/store"
+ "github.com/netbirdio/netbird/management/server/types"
+)
+
+func TestAuthManager_GetAccountInfoFromPAT(t *testing.T) {
+ store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
+ if err != nil {
+ t.Fatalf("Error when creating store: %s", err)
+ }
+ t.Cleanup(cleanup)
+
+ token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
+ hashedToken := sha256.Sum256([]byte(token))
+ encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
+ account := &types.Account{
+ Id: "account_id",
+ Users: map[string]*types.User{"someUser": {
+ Id: "someUser",
+ PATs: map[string]*types.PersonalAccessToken{
+ "tokenId": {
+ ID: "tokenId",
+ UserID: "someUser",
+ HashedToken: encodedHashedToken,
+ },
+ },
+ }},
+ }
+
+ err = store.SaveAccount(context.Background(), account)
+ if err != nil {
+ t.Fatalf("Error when saving account: %s", err)
+ }
+
+ manager := auth.NewManager(store, "", "", "", "", []string{}, false)
+
+ user, pat, _, _, err := manager.GetPATInfo(context.Background(), token)
+ if err != nil {
+ t.Fatalf("Error when getting Account from PAT: %s", err)
+ }
+
+ assert.Equal(t, "account_id", user.AccountID)
+ assert.Equal(t, "someUser", user.Id)
+ assert.Equal(t, account.Users["someUser"].PATs["tokenId"].ID, pat.ID)
+}
+
+func TestAuthManager_MarkPATUsed(t *testing.T) {
+ store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
+ if err != nil {
+ t.Fatalf("Error when creating store: %s", err)
+ }
+ t.Cleanup(cleanup)
+
+ token := "nbp_9999EUDNdkeusjentDLSJEn1902u84390W6W"
+ hashedToken := sha256.Sum256([]byte(token))
+ encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
+ account := &types.Account{
+ Id: "account_id",
+ Users: map[string]*types.User{"someUser": {
+ Id: "someUser",
+ PATs: map[string]*types.PersonalAccessToken{
+ "tokenId": {
+ ID: "tokenId",
+ HashedToken: encodedHashedToken,
+ },
+ },
+ }},
+ }
+
+ err = store.SaveAccount(context.Background(), account)
+ if err != nil {
+ t.Fatalf("Error when saving account: %s", err)
+ }
+
+ manager := auth.NewManager(store, "", "", "", "", []string{}, false)
+
+ err = manager.MarkPATUsed(context.Background(), "tokenId")
+ if err != nil {
+ t.Fatalf("Error when marking PAT used: %s", err)
+ }
+
+ account, err = store.GetAccount(context.Background(), "account_id")
+ if err != nil {
+ t.Fatalf("Error when getting account: %s", err)
+ }
+ assert.True(t, !account.Users["someUser"].PATs["tokenId"].GetLastUsed().IsZero())
+}
+
+func TestAuthManager_EnsureUserAccessByJWTGroups(t *testing.T) {
+ store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
+ if err != nil {
+ t.Fatalf("Error when creating store: %s", err)
+ }
+ t.Cleanup(cleanup)
+
+ userId := "user-id"
+ domain := "test.domain"
+
+ account := &types.Account{
+ Id: "account_id",
+ Domain: domain,
+ Users: map[string]*types.User{"someUser": {
+ Id: "someUser",
+ }},
+ Settings: &types.Settings{},
+ }
+
+ err = store.SaveAccount(context.Background(), account)
+ if err != nil {
+ t.Fatalf("Error when saving account: %s", err)
+ }
+
+ // this has been validated and parsed by ValidateAndParseToken
+ userAuth := nbcontext.UserAuth{
+ AccountId: account.Id,
+ Domain: domain,
+ UserId: userId,
+ DomainCategory: "test-category",
+ // Groups: []string{"group1", "group2"},
+ }
+
+ // these tests only assert groups are parsed from token as per account settings
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"idp-groups": []interface{}{"group1", "group2"}})
+
+ manager := auth.NewManager(store, "", "", "", "", []string{}, false)
+
+ t.Run("JWT groups disabled", func(t *testing.T) {
+ userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
+ require.NoError(t, err, "ensure user access by JWT groups failed")
+ require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups")
+ })
+
+ t.Run("User impersonated", func(t *testing.T) {
+ userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
+ require.NoError(t, err, "ensure user access by JWT groups failed")
+ require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups")
+ })
+
+ t.Run("User PAT", func(t *testing.T) {
+ userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
+ require.NoError(t, err, "ensure user access by JWT groups failed")
+ require.Len(t, userAuth.Groups, 0, "account not enabled to ensure access by groups")
+ })
+
+ t.Run("JWT groups enabled without claim name", func(t *testing.T) {
+ account.Settings.JWTGroupsEnabled = true
+ err := store.SaveAccount(context.Background(), account)
+ require.NoError(t, err, "save account failed")
+
+ userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
+ require.NoError(t, err, "ensure user access by JWT groups failed")
+ require.Len(t, userAuth.Groups, 0, "account missing groups claim name")
+ })
+
+ t.Run("JWT groups enabled without allowed groups", func(t *testing.T) {
+ account.Settings.JWTGroupsEnabled = true
+ account.Settings.JWTGroupsClaimName = "idp-groups"
+ err := store.SaveAccount(context.Background(), account)
+ require.NoError(t, err, "save account failed")
+
+ userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
+ require.NoError(t, err, "ensure user access by JWT groups failed")
+ require.Equal(t, []string{"group1", "group2"}, userAuth.Groups, "group parsed do not match")
+ })
+
+ t.Run("User in allowed JWT groups", func(t *testing.T) {
+ account.Settings.JWTGroupsEnabled = true
+ account.Settings.JWTGroupsClaimName = "idp-groups"
+ account.Settings.JWTAllowGroups = []string{"group1"}
+ err := store.SaveAccount(context.Background(), account)
+ require.NoError(t, err, "save account failed")
+
+ userAuth, err := manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
+ require.NoError(t, err, "ensure user access by JWT groups failed")
+
+ require.Equal(t, []string{"group1", "group2"}, userAuth.Groups, "group parsed do not match")
+ })
+
+ t.Run("User not in allowed JWT groups", func(t *testing.T) {
+ account.Settings.JWTGroupsEnabled = true
+ account.Settings.JWTGroupsClaimName = "idp-groups"
+ account.Settings.JWTAllowGroups = []string{"not-a-group"}
+ err := store.SaveAccount(context.Background(), account)
+ require.NoError(t, err, "save account failed")
+
+ _, err = manager.EnsureUserAccessByJWTGroups(context.Background(), userAuth, token)
+ require.Error(t, err, "ensure user access is not in allowed groups")
+ })
+}
+
+func TestAuthManager_ValidateAndParseToken(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Cache-Control", "max-age=30") // set a 30s expiry to these keys
+ http.ServeFile(w, r, "test_data/jwks.json")
+ }))
+ defer server.Close()
+
+ issuer := "http://issuer.local"
+ audience := "http://audience.local"
+ userIdClaim := "" // defaults to "sub"
+
+ // we're only testing with RSA256
+ keyData, _ := os.ReadFile("test_data/sample_key")
+ key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData)
+ keyId := "test-key"
+
+ // note, we can use a nil store because ValidateAndParseToken does not use it in it's flow
+ manager := auth.NewManager(nil, issuer, audience, server.URL, userIdClaim, []string{audience}, false)
+
+ customClaim := func(name string) string {
+ return fmt.Sprintf("%s/%s", audience, name)
+ }
+
+ lastLogin := time.Date(2025, 2, 12, 14, 25, 26, 0, time.UTC) //"2025-02-12T14:25:26.186Z"
+
+ tests := []struct {
+ name string
+ tokenFunc func() string
+ expected *nbcontext.UserAuth // nil indicates expected error
+ }{
+ {
+ name: "Valid with custom claims",
+ tokenFunc: func() string {
+ token := jwt.New(jwt.SigningMethodRS256)
+ token.Header["kid"] = keyId
+ token.Claims = jwt.MapClaims{
+ "iss": issuer,
+ "aud": []string{audience},
+ "iat": time.Now().Unix(),
+ "exp": time.Now().Add(time.Hour * 1).Unix(),
+ "sub": "user-id|123",
+ customClaim(nbjwt.AccountIDSuffix): "account-id|567",
+ customClaim(nbjwt.DomainIDSuffix): "http://localhost",
+ customClaim(nbjwt.DomainCategorySuffix): "private",
+ customClaim(nbjwt.LastLoginSuffix): lastLogin.Format(time.RFC3339),
+ customClaim(nbjwt.Invited): false,
+ }
+ tokenString, _ := token.SignedString(key)
+ return tokenString
+ },
+ expected: &nbcontext.UserAuth{
+ UserId: "user-id|123",
+ AccountId: "account-id|567",
+ Domain: "http://localhost",
+ DomainCategory: "private",
+ LastLogin: lastLogin,
+ Invited: false,
+ },
+ },
+ {
+ name: "Valid without custom claims",
+ tokenFunc: func() string {
+ token := jwt.New(jwt.SigningMethodRS256)
+ token.Header["kid"] = keyId
+ token.Claims = jwt.MapClaims{
+ "iss": issuer,
+ "aud": []string{audience},
+ "iat": time.Now().Unix(),
+ "exp": time.Now().Add(time.Hour).Unix(),
+ "sub": "user-id|123",
+ }
+ tokenString, _ := token.SignedString(key)
+ return tokenString
+ },
+ expected: &nbcontext.UserAuth{
+ UserId: "user-id|123",
+ },
+ },
+ {
+ name: "Expired token",
+ tokenFunc: func() string {
+ token := jwt.New(jwt.SigningMethodRS256)
+ token.Header["kid"] = keyId
+ token.Claims = jwt.MapClaims{
+ "iss": issuer,
+ "aud": []string{audience},
+ "iat": time.Now().Add(time.Hour * -2).Unix(),
+ "exp": time.Now().Add(time.Hour * -1).Unix(),
+ "sub": "user-id|123",
+ }
+ tokenString, _ := token.SignedString(key)
+ return tokenString
+ },
+ },
+ {
+ name: "Not yet valid",
+ tokenFunc: func() string {
+ token := jwt.New(jwt.SigningMethodRS256)
+ token.Header["kid"] = keyId
+ token.Claims = jwt.MapClaims{
+ "iss": issuer,
+ "aud": []string{audience},
+ "iat": time.Now().Add(time.Hour).Unix(),
+ "exp": time.Now().Add(time.Hour * 2).Unix(),
+ "sub": "user-id|123",
+ }
+ tokenString, _ := token.SignedString(key)
+ return tokenString
+ },
+ },
+ {
+ name: "Invalid signature",
+ tokenFunc: func() string {
+ token := jwt.New(jwt.SigningMethodRS256)
+ token.Header["kid"] = keyId
+ token.Claims = jwt.MapClaims{
+ "iss": issuer,
+ "aud": []string{audience},
+ "iat": time.Now().Unix(),
+ "exp": time.Now().Add(time.Hour).Unix(),
+ "sub": "user-id|123",
+ }
+ tokenString, _ := token.SignedString(key)
+ parts := strings.Split(tokenString, ".")
+ parts[2] = "invalid-signature"
+ return strings.Join(parts, ".")
+ },
+ },
+ {
+ name: "Invalid issuer",
+ tokenFunc: func() string {
+ token := jwt.New(jwt.SigningMethodRS256)
+ token.Header["kid"] = keyId
+ token.Claims = jwt.MapClaims{
+ "iss": "not-the-issuer",
+ "aud": []string{audience},
+ "iat": time.Now().Unix(),
+ "exp": time.Now().Add(time.Hour).Unix(),
+ "sub": "user-id|123",
+ }
+ tokenString, _ := token.SignedString(key)
+ return tokenString
+ },
+ },
+ {
+ name: "Invalid audience",
+ tokenFunc: func() string {
+ token := jwt.New(jwt.SigningMethodRS256)
+ token.Header["kid"] = keyId
+ token.Claims = jwt.MapClaims{
+ "iss": issuer,
+ "aud": []string{"not-the-audience"},
+ "iat": time.Now().Unix(),
+ "exp": time.Now().Add(time.Hour).Unix(),
+ "sub": "user-id|123",
+ }
+ tokenString, _ := token.SignedString(key)
+ return tokenString
+ },
+ },
+ {
+ name: "Invalid user claim",
+ tokenFunc: func() string {
+ token := jwt.New(jwt.SigningMethodRS256)
+ token.Header["kid"] = keyId
+ token.Claims = jwt.MapClaims{
+ "iss": issuer,
+ "aud": []string{audience},
+ "iat": time.Now().Unix(),
+ "exp": time.Now().Add(time.Hour).Unix(),
+ "not-sub": "user-id|123",
+ }
+ tokenString, _ := token.SignedString(key)
+ return tokenString
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tokenString := tt.tokenFunc()
+
+ userAuth, token, err := manager.ValidateAndParseToken(context.Background(), tokenString)
+
+ if tt.expected != nil {
+ assert.NoError(t, err)
+ assert.True(t, token.Valid)
+ assert.Equal(t, *tt.expected, userAuth)
+ } else {
+ assert.Error(t, err)
+ assert.Nil(t, token)
+ assert.Empty(t, userAuth)
+ }
+ })
+ }
+
+}
diff --git a/management/server/auth/test_data/jwks.json b/management/server/auth/test_data/jwks.json
new file mode 100644
index 000000000..8080f5599
--- /dev/null
+++ b/management/server/auth/test_data/jwks.json
@@ -0,0 +1,11 @@
+{
+ "keys": [
+ {
+ "kty": "RSA",
+ "kid": "test-key",
+ "use": "sig",
+ "n": "4f5wg5l2hKsTeNem_V41fGnJm6gOdrj8ym3rFkEU_wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn_MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR-1DcKJzQBSTAGnpYVaqpsARap-nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7w",
+ "e": "AQAB"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/management/server/auth/test_data/sample_key b/management/server/auth/test_data/sample_key
new file mode 100644
index 000000000..e69284a3f
--- /dev/null
+++ b/management/server/auth/test_data/sample_key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA4f5wg5l2hKsTeNem/V41fGnJm6gOdrj8ym3rFkEU/wT8RDtn
+SgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0i
+cqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhC
+PUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR+1DcKJzQBSTAGnpYVaqpsAR
+ap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKA
+Rdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7wIDAQABAoIBAQCwia1k7+2oZ2d3
+n6agCAbqIE1QXfCmh41ZqJHbOY3oRQG3X1wpcGH4Gk+O+zDVTV2JszdcOt7E5dAy
+MaomETAhRxB7hlIOnEN7WKm+dGNrKRvV0wDU5ReFMRHg31/Lnu8c+5BvGjZX+ky9
+POIhFFYJqwCRlopGSUIxmVj5rSgtzk3iWOQXr+ah1bjEXvlxDOWkHN6YfpV5ThdE
+KdBIPGEVqa63r9n2h+qazKrtiRqJqGnOrHzOECYbRFYhexsNFz7YT02xdfSHn7gM
+IvabDDP/Qp0PjE1jdouiMaFHYnLBbgvlnZW9yuVf/rpXTUq/njxIXMmvmEyyvSDn
+FcFikB8pAoGBAPF77hK4m3/rdGT7X8a/gwvZ2R121aBcdPwEaUhvj/36dx596zvY
+mEOjrWfZhF083/nYWE2kVquj2wjs+otCLfifEEgXcVPTnEOPO9Zg3uNSL0nNQghj
+FuD3iGLTUBCtM66oTe0jLSslHe8gLGEQqyMzHOzYxNqibxcOZIe8Qt0NAoGBAO+U
+I5+XWjWEgDmvyC3TrOSf/KCGjtu0TSv30ipv27bDLMrpvPmD/5lpptTFwcxvVhCs
+2b+chCjlghFSWFbBULBrfci2FtliClOVMYrlNBdUSJhf3aYSG2Doe6Bgt1n2CpNn
+/iu37Y3NfemZBJA7hNl4dYe+f+uzM87cdQ214+jrAoGAXA0XxX8ll2+ToOLJsaNT
+OvNB9h9Uc5qK5X5w+7G7O998BN2PC/MWp8H+2fVqpXgNENpNXttkRm1hk1dych86
+EunfdPuqsX+as44oCyJGFHVBnWpm33eWQw9YqANRI+pCJzP08I5WK3osnPiwshd+
+hR54yjgfYhBFNI7B95PmEQkCgYBzFSz7h1+s34Ycr8SvxsOBWxymG5zaCsUbPsL0
+4aCgLScCHb9J+E86aVbbVFdglYa5Id7DPTL61ixhl7WZjujspeXZGSbmq0Kcnckb
+mDgqkLECiOJW2NHP/j0McAkDLL4tysF8TLDO8gvuvzNC+WQ6drO2ThrypLVZQ+ry
+eBIPmwKBgEZxhqa0gVvHQG/7Od69KWj4eJP28kq13RhKay8JOoN0vPmspXJo1HY3
+CKuHRG+AP579dncdUnOMvfXOtkdM4vk0+hWASBQzM9xzVcztCa+koAugjVaLS9A+
+9uQoqEeVNTckxx0S2bYevRy7hGQmUJTyQm3j1zEUR5jpdbL83Fbq
+-----END RSA PRIVATE KEY-----
\ No newline at end of file
diff --git a/management/server/auth/test_data/sample_key.pub b/management/server/auth/test_data/sample_key.pub
new file mode 100644
index 000000000..d5b7f7102
--- /dev/null
+++ b/management/server/auth/test_data/sample_key.pub
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4f5wg5l2hKsTeNem/V41
+fGnJm6gOdrj8ym3rFkEU/wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7
+mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBp
+HssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2
+XrHhR+1DcKJzQBSTAGnpYVaqpsARap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3b
+ODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy
+7wIDAQAB
+-----END PUBLIC KEY-----
\ No newline at end of file
diff --git a/management/server/config.go b/management/server/config.go
index f3555b92b..ce2ff4d16 100644
--- a/management/server/config.go
+++ b/management/server/config.go
@@ -2,7 +2,6 @@ package server
import (
"net/netip"
- "net/url"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/store"
@@ -106,10 +105,10 @@ type HttpServerConfig struct {
ExtraAuthAudience string
}
-// Host represents a Wiretrustee host (e.g. STUN, TURN, Signal)
+// Host represents a Netbird host (e.g. STUN, TURN, Signal)
type Host struct {
Proto Protocol
- // URI e.g. turns://stun.wiretrustee.com:4430 or signal.wiretrustee.com:10000
+ // URI e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000
URI string
Username string
Password string
@@ -180,9 +179,3 @@ type ReverseProxy struct {
// trusted IP prefixes.
TrustedPeers []netip.Prefix
}
-
-// validateURL validates input http url
-func validateURL(httpURL string) bool {
- _, err := url.ParseRequestURI(httpURL)
- return err == nil
-}
diff --git a/management/server/context/auth.go b/management/server/context/auth.go
new file mode 100644
index 000000000..5cb28ddb7
--- /dev/null
+++ b/management/server/context/auth.go
@@ -0,0 +1,60 @@
+package context
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+type key int
+
+const (
+ UserAuthContextKey key = iota
+)
+
+type UserAuth struct {
+ // The account id the user is accessing
+ AccountId string
+ // The account domain
+ Domain string
+ // The account domain category, TBC values
+ DomainCategory string
+ // Indicates whether this user was invited, TBC logic
+ Invited bool
+ // Indicates whether this is a child account
+ IsChild bool
+
+ // The user id
+ UserId string
+ // Last login time for this user
+ LastLogin time.Time
+ // The Groups the user belongs to on this account
+ Groups []string
+
+ // Indicates whether this user has authenticated with a Personal Access Token
+ IsPAT bool
+}
+
+func GetUserAuthFromRequest(r *http.Request) (UserAuth, error) {
+ return GetUserAuthFromContext(r.Context())
+}
+
+func SetUserAuthInRequest(r *http.Request, userAuth UserAuth) *http.Request {
+ return r.WithContext(SetUserAuthInContext(r.Context(), userAuth))
+}
+
+func GetUserAuthFromContext(ctx context.Context) (UserAuth, error) {
+ if userAuth, ok := ctx.Value(UserAuthContextKey).(UserAuth); ok {
+ return userAuth, nil
+ }
+ return UserAuth{}, fmt.Errorf("user auth not in context")
+}
+
+func SetUserAuthInContext(ctx context.Context, userAuth UserAuth) context.Context {
+ //nolint
+ ctx = context.WithValue(ctx, UserIDKey, userAuth.UserId)
+ //nolint
+ ctx = context.WithValue(ctx, AccountIDKey, userAuth.AccountId)
+ return context.WithValue(ctx, UserAuthContextKey, userAuth)
+}
diff --git a/management/server/dns_test.go b/management/server/dns_test.go
index 6fb9f6a29..c40f62324 100644
--- a/management/server/dns_test.go
+++ b/management/server/dns_test.go
@@ -42,7 +42,7 @@ func TestGetDNSSettings(t *testing.T) {
account, err := initTestDNSAccount(t, am)
if err != nil {
- t.Fatal("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
dnsSettings, err := am.GetDNSSettings(context.Background(), account.Id, dnsAdminUserID)
@@ -124,12 +124,12 @@ func TestSaveDNSSettings(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
am, err := createDNSManager(t)
if err != nil {
- t.Error("failed to create account manager")
+ t.Fatalf("failed to create account manager")
}
account, err := initTestDNSAccount(t, am)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %v", err)
}
err = am.SaveDNSSettings(context.Background(), account.Id, testCase.userID, testCase.inputSettings)
@@ -156,22 +156,22 @@ func TestGetNetworkMap_DNSConfigSync(t *testing.T) {
am, err := createDNSManager(t)
if err != nil {
- t.Error("failed to create account manager")
+ t.Fatalf("failed to create account manager: %s", err)
}
account, err := initTestDNSAccount(t, am)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
peer1, err := account.FindPeerByPubKey(dnsPeer1Key)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
peer2, err := account.FindPeerByPubKey(dnsPeer2Key)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
newAccountDNSConfig, err := am.GetNetworkMap(context.Background(), peer1.ID)
diff --git a/management/server/geolocation/database.go b/management/server/geolocation/database.go
index 21ae93b9d..97ab398fb 100644
--- a/management/server/geolocation/database.go
+++ b/management/server/geolocation/database.go
@@ -123,7 +123,6 @@ func importCsvToSqlite(dataDir string, csvFile string, geonamesdbFile string) er
db, err := gorm.Open(sqlite.Open(path.Join(dataDir, geonamesdbFile)), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
CreateBatchSize: 1000,
- PrepareStmt: true,
})
if err != nil {
return err
diff --git a/management/server/geolocation/store.go b/management/server/geolocation/store.go
index 1f94bf47e..5af8276b5 100644
--- a/management/server/geolocation/store.go
+++ b/management/server/geolocation/store.go
@@ -132,8 +132,7 @@ func connectDB(ctx context.Context, filePath string) (*gorm.DB, error) {
}
db, err := gorm.Open(sqlite.Open(storeStr), &gorm.Config{
- Logger: logger.Default.LogMode(logger.Silent),
- PrepareStmt: true,
+ Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
return nil, err
diff --git a/management/server/group.go b/management/server/group.go
index 6a70e26bd..daff55b0c 100644
--- a/management/server/group.go
+++ b/management/server/group.go
@@ -463,7 +463,7 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty
if group.Issued == types.GroupIssuedIntegration {
executingUser, err := transaction.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
if err != nil {
- return err
+ return status.Errorf(status.Internal, "failed to get user")
}
if executingUser.Role != types.UserRoleAdmin || !executingUser.IsServiceUser {
return status.Errorf(status.PermissionDenied, "only service users with admin power can delete integration group")
@@ -505,7 +505,7 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty
func checkGroupLinkedToSettings(ctx context.Context, transaction store.Store, group *types.Group) error {
dnsSettings, err := transaction.GetAccountDNSSettings(ctx, store.LockingStrengthShare, group.AccountID)
if err != nil {
- return err
+ return status.Errorf(status.Internal, "failed to get DNS settings")
}
if slices.Contains(dnsSettings.DisabledManagementGroups, group.ID) {
@@ -514,7 +514,7 @@ func checkGroupLinkedToSettings(ctx context.Context, transaction store.Store, gr
settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, group.AccountID)
if err != nil {
- return err
+ return status.Errorf(status.Internal, "failed to get account settings")
}
if settings.Extra != nil && slices.Contains(settings.Extra.IntegratedValidatorGroups, group.ID) {
diff --git a/management/server/group_test.go b/management/server/group_test.go
index cc90f187b..b21b5e834 100644
--- a/management/server/group_test.go
+++ b/management/server/group_test.go
@@ -29,7 +29,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) {
_, account, err := initTestGroupAccount(am)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
for _, group := range account.Groups {
group.Issued = types.GroupIssuedIntegration
@@ -59,12 +59,12 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) {
func TestDefaultAccountManager_DeleteGroup(t *testing.T) {
am, err := createManager(t)
if err != nil {
- t.Error("failed to create account manager")
+ t.Fatalf("failed to create account manager: %s", err)
}
_, account, err := initTestGroupAccount(am)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
testCases := []struct {
diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go
index daa23d2ab..3d170afa4 100644
--- a/management/server/grpcserver.go
+++ b/management/server/grpcserver.go
@@ -15,12 +15,13 @@ import (
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
+ "google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"github.com/netbirdio/netbird/encryption"
"github.com/netbirdio/netbird/management/proto"
+ "github.com/netbirdio/netbird/management/server/auth"
nbContext "github.com/netbirdio/netbird/management/server/context"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/settings"
@@ -38,11 +39,10 @@ type GRPCServer struct {
peersUpdateManager *PeersUpdateManager
config *Config
secretsManager SecretsManager
- jwtValidator jwtclaims.JWTValidator
- jwtClaimsExtractor *jwtclaims.ClaimsExtractor
appMetrics telemetry.AppMetrics
ephemeralManager *EphemeralManager
peerLocks sync.Map
+ authManager auth.Manager
}
// NewServer creates a new Management server
@@ -55,29 +55,13 @@ func NewServer(
secretsManager SecretsManager,
appMetrics telemetry.AppMetrics,
ephemeralManager *EphemeralManager,
+ authManager auth.Manager,
) (*GRPCServer, error) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
}
- var jwtValidator jwtclaims.JWTValidator
-
- if config.HttpConfig != nil && config.HttpConfig.AuthIssuer != "" && config.HttpConfig.AuthAudience != "" && validateURL(config.HttpConfig.AuthKeysLocation) {
- jwtValidator, err = jwtclaims.NewJWTValidator(
- ctx,
- config.HttpConfig.AuthIssuer,
- config.GetAuthAudiences(),
- config.HttpConfig.AuthKeysLocation,
- config.HttpConfig.IdpSignKeyRefreshEnabled,
- )
- if err != nil {
- return nil, status.Errorf(codes.Internal, "unable to create new jwt middleware, err: %v", err)
- }
- } else {
- log.WithContext(ctx).Debug("unable to use http config to create new jwt middleware")
- }
-
if appMetrics != nil {
// update gauge based on number of connected peers which is equal to open gRPC streams
err = appMetrics.GRPCMetrics().RegisterConnectedStreams(func() int64 {
@@ -88,16 +72,6 @@ func NewServer(
}
}
- var audience, userIDClaim string
- if config.HttpConfig != nil {
- audience = config.HttpConfig.AuthAudience
- userIDClaim = config.HttpConfig.AuthUserIDClaim
- }
- jwtClaimsExtractor := jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(audience),
- jwtclaims.WithUserIDClaim(userIDClaim),
- )
-
return &GRPCServer{
wgKey: key,
// peerKey -> event channel
@@ -106,14 +80,25 @@ func NewServer(
settingsManager: settingsManager,
config: config,
secretsManager: secretsManager,
- jwtValidator: jwtValidator,
- jwtClaimsExtractor: jwtClaimsExtractor,
+ authManager: authManager,
appMetrics: appMetrics,
ephemeralManager: ephemeralManager,
}, nil
}
func (s *GRPCServer) GetServerKey(ctx context.Context, req *proto.Empty) (*proto.ServerKeyResponse, error) {
+ ip := ""
+ p, ok := peer.FromContext(ctx)
+ if ok {
+ ip = p.Addr.String()
+ }
+
+ log.WithContext(ctx).Tracef("GetServerKey request from %s", ip)
+ start := time.Now()
+ defer func() {
+ log.WithContext(ctx).Tracef("GetServerKey from %s took %v", ip, time.Since(start))
+ }()
+
// todo introduce something more meaningful with the key expiration/rotation
if s.appMetrics != nil {
s.appMetrics.GRPCMetrics().CountGetKeyRequest()
@@ -208,6 +193,8 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi
unlock()
unlock = nil
+ log.WithContext(ctx).Debugf("Sync: took %v", time.Since(reqStart))
+
return s.handleUpdates(ctx, accountID, peerKey, peer, updates, srv)
}
@@ -279,26 +266,37 @@ func (s *GRPCServer) cancelPeerRoutines(ctx context.Context, accountID string, p
}
func (s *GRPCServer) validateToken(ctx context.Context, jwtToken string) (string, error) {
- if s.jwtValidator == nil {
- return "", status.Error(codes.Internal, "no jwt validator set")
+ if s.authManager == nil {
+ return "", status.Errorf(codes.Internal, "missing auth manager")
}
- token, err := s.jwtValidator.ValidateAndParse(ctx, jwtToken)
+ userAuth, token, err := s.authManager.ValidateAndParseToken(ctx, jwtToken)
if err != nil {
return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err)
}
- claims := s.jwtClaimsExtractor.FromToken(token)
+
// we need to call this method because if user is new, we will automatically add it to existing or create a new account
- _, _, err = s.accountManager.GetAccountIDFromToken(ctx, claims)
+ accountId, _, err := s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth)
if err != nil {
return "", status.Errorf(codes.Internal, "unable to fetch account with claims, err: %v", err)
}
- if err := s.accountManager.CheckUserAccessByJWTGroups(ctx, claims); err != nil {
+ if userAuth.AccountId != accountId {
+ log.WithContext(ctx).Debugf("gRPC server sets accountId from ensure, before %s, now %s", userAuth.AccountId, accountId)
+ userAuth.AccountId = accountId
+ }
+
+ userAuth, err = s.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, token)
+ if err != nil {
return "", status.Error(codes.PermissionDenied, err.Error())
}
- return claims.UserId, nil
+ err = s.accountManager.SyncUserJWTGroups(ctx, userAuth)
+ if err != nil {
+ log.WithContext(ctx).Errorf("gRPC server failed to sync user JWT groups: %s", err)
+ }
+
+ return userAuth.UserId, nil
}
func (s *GRPCServer) acquirePeerLockByUID(ctx context.Context, uniqueID string) (unlock func()) {
@@ -379,7 +377,7 @@ func extractPeerMeta(ctx context.Context, meta *proto.PeerSystemMeta) nbpeer.Pee
Platform: meta.GetPlatform(),
OS: meta.GetOS(),
OSVersion: osVersion,
- WtVersion: meta.GetWiretrusteeVersion(),
+ WtVersion: meta.GetNetbirdVersion(),
UIVersion: meta.GetUiVersion(),
KernelVersion: meta.GetKernelVersion(),
NetworkAddresses: networkAddresses,
@@ -466,6 +464,7 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
UserID: userID,
SetupKey: loginReq.GetSetupKey(),
ConnectionIP: realIP,
+ ExtraDNSLabels: loginReq.GetDnsLabels(),
})
if err != nil {
log.WithContext(ctx).Warnf("failed logging in peer %s: %s", peerKey, err)
@@ -487,9 +486,9 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
// if peer has reached this point then it has logged in
loginResp := &proto.LoginResponse{
- WiretrusteeConfig: toWiretrusteeConfig(s.config, nil, relayToken),
- PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(), false),
- Checks: toProtocolChecks(ctx, postureChecks),
+ NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken),
+ PeerConfig: toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(), false),
+ Checks: toProtocolChecks(ctx, postureChecks),
}
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp)
if err != nil {
@@ -545,7 +544,7 @@ func ToResponseProto(configProto Protocol) proto.HostConfig_Protocol {
}
}
-func toWiretrusteeConfig(config *Config, turnCredentials *Token, relayToken *Token) *proto.WiretrusteeConfig {
+func toNetbirdConfig(config *Config, turnCredentials *Token, relayToken *Token) *proto.NetbirdConfig {
if config == nil {
return nil
}
@@ -593,7 +592,7 @@ func toWiretrusteeConfig(config *Config, turnCredentials *Token, relayToken *Tok
}
}
- return &proto.WiretrusteeConfig{
+ return &proto.NetbirdConfig{
Stuns: stuns,
Turns: turns,
Signal: &proto.HostConfig{
@@ -617,8 +616,8 @@ func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, dns
func toSyncResponse(ctx context.Context, config *Config, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *DNSConfigCache, dnsResolutionOnRoutingPeerEnbled bool) *proto.SyncResponse {
response := &proto.SyncResponse{
- WiretrusteeConfig: toWiretrusteeConfig(config, turnCredentials, relayCredentials),
- PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnbled),
+ NetbirdConfig: toNetbirdConfig(config, turnCredentials, relayCredentials),
+ PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnbled),
NetworkMap: &proto.NetworkMap{
Serial: networkMap.Network.CurrentSerial(),
Routes: toProtocolRoutes(networkMap.Routes),
@@ -715,6 +714,12 @@ func (s *GRPCServer) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, p
// This is used for initiating an Oauth 2 device authorization grant flow
// which will be used by our clients to Login
func (s *GRPCServer) GetDeviceAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
+ log.WithContext(ctx).Tracef("GetDeviceAuthorizationFlow request for pubKey: %s", req.WgPubKey)
+ start := time.Now()
+ defer func() {
+ log.WithContext(ctx).Tracef("GetDeviceAuthorizationFlow for pubKey: %s took %v", req.WgPubKey, time.Since(start))
+ }()
+
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
if err != nil {
errMSG := fmt.Sprintf("error while parsing peer's Wireguard public key %s on GetDeviceAuthorizationFlow request.", req.WgPubKey)
@@ -767,6 +772,12 @@ func (s *GRPCServer) GetDeviceAuthorizationFlow(ctx context.Context, req *proto.
// This is used for initiating an Oauth 2 pkce authorization grant flow
// which will be used by our clients to Login
func (s *GRPCServer) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
+ log.WithContext(ctx).Tracef("GetPKCEAuthorizationFlow request for pubKey: %s", req.WgPubKey)
+ start := time.Now()
+ defer func() {
+ log.WithContext(ctx).Tracef("GetPKCEAuthorizationFlow for pubKey %s took %v", req.WgPubKey, time.Since(start))
+ }()
+
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
if err != nil {
errMSG := fmt.Sprintf("error while parsing peer's Wireguard public key %s on GetPKCEAuthorizationFlow request.", req.WgPubKey)
diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml
index f53092415..83f45ef91 100644
--- a/management/server/http/api/openapi.yml
+++ b/management/server/http/api/openapi.yml
@@ -361,6 +361,12 @@ components:
description: System serial number
type: string
example: "C02XJ0J0JGH7"
+ extra_dns_labels:
+ description: Extra DNS labels added to the peer
+ type: array
+ items:
+ type: string
+ example: "stage-host-1"
required:
- city_name
- connected
@@ -384,6 +390,7 @@ components:
- ui_version
- approval_required
- serial_number
+ - extra_dns_labels
AccessiblePeer:
allOf:
- $ref: '#/components/schemas/PeerMinimum'
@@ -503,6 +510,10 @@ components:
description: Indicate that the peer will be ephemeral or not
type: boolean
example: true
+ allow_extra_dns_labels:
+ description: Allow extra DNS labels to be added to the peer
+ type: boolean
+ example: true
required:
- id
- key
@@ -518,6 +529,7 @@ components:
- updated_at
- usage_limit
- ephemeral
+ - allow_extra_dns_labels
SetupKeyClear:
allOf:
- $ref: '#/components/schemas/SetupKeyBase'
@@ -587,6 +599,10 @@ components:
description: Indicate that the peer will be ephemeral or not
type: boolean
example: true
+ allow_extra_dns_labels:
+ description: Allow extra DNS labels to be added to the peer
+ type: boolean
+ example: true
required:
- name
- type
diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go
index 943d1b327..eb57d5d66 100644
--- a/management/server/http/api/types.gen.go
+++ b/management/server/http/api/types.gen.go
@@ -297,6 +297,9 @@ type CountryCode = string
// CreateSetupKeyRequest defines model for CreateSetupKeyRequest.
type CreateSetupKeyRequest struct {
+ // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
+ AllowExtraDnsLabels *bool `json:"allow_extra_dns_labels,omitempty"`
+
// AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
@@ -689,6 +692,9 @@ type Peer struct {
// 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"`
+ // ExtraDnsLabels Extra DNS labels added to the peer
+ ExtraDnsLabels []string `json:"extra_dns_labels"`
+
// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
GeonameId int `json:"geoname_id"`
@@ -767,6 +773,9 @@ type PeerBatch struct {
// 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"`
+ // ExtraDnsLabels Extra DNS labels added to the peer
+ ExtraDnsLabels []string `json:"extra_dns_labels"`
+
// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
GeonameId int `json:"geoname_id"`
@@ -1230,6 +1239,9 @@ type RulePortRange struct {
// SetupKey defines model for SetupKey.
type SetupKey struct {
+ // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
+ AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
+
// AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
@@ -1275,6 +1287,9 @@ type SetupKey struct {
// SetupKeyBase defines model for SetupKeyBase.
type SetupKeyBase struct {
+ // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
+ AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
+
// AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
@@ -1317,6 +1332,9 @@ type SetupKeyBase struct {
// SetupKeyClear defines model for SetupKeyClear.
type SetupKeyClear struct {
+ // AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
+ AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
+
// AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
diff --git a/management/server/http/handler.go b/management/server/http/handler.go
index 1ddf10a6c..2b87c5f25 100644
--- a/management/server/http/handler.go
+++ b/management/server/http/handler.go
@@ -11,9 +11,9 @@ import (
"github.com/netbirdio/management-integrations/integrations"
s "github.com/netbirdio/netbird/management/server"
+ "github.com/netbirdio/netbird/management/server/auth"
"github.com/netbirdio/netbird/management/server/geolocation"
nbgroups "github.com/netbirdio/netbird/management/server/groups"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/handlers/accounts"
"github.com/netbirdio/netbird/management/server/http/handlers/dns"
"github.com/netbirdio/netbird/management/server/http/handlers/events"
@@ -26,7 +26,6 @@ import (
"github.com/netbirdio/netbird/management/server/http/handlers/users"
"github.com/netbirdio/netbird/management/server/http/middleware"
"github.com/netbirdio/netbird/management/server/integrated_validator"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
nbnetworks "github.com/netbirdio/netbird/management/server/networks"
"github.com/netbirdio/netbird/management/server/networks/resources"
"github.com/netbirdio/netbird/management/server/networks/routers"
@@ -36,55 +35,51 @@ import (
const apiPrefix = "/api"
// NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints.
-func NewAPIHandler(ctx context.Context, accountManager s.AccountManager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, jwtValidator jwtclaims.JWTValidator, appMetrics telemetry.AppMetrics, authCfg configs.AuthCfg, integratedValidator integrated_validator.IntegratedValidator) (http.Handler, error) {
- claimsExtractor := jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- )
+func NewAPIHandler(
+ ctx context.Context,
+ accountManager s.AccountManager,
+ networksManager nbnetworks.Manager,
+ resourceManager resources.Manager,
+ routerManager routers.Manager,
+ groupsManager nbgroups.Manager,
+ LocationManager geolocation.Geolocation,
+ authManager auth.Manager,
+ appMetrics telemetry.AppMetrics,
+ config *s.Config,
+ integratedValidator integrated_validator.IntegratedValidator) (http.Handler, error) {
authMiddleware := middleware.NewAuthMiddleware(
- accountManager.GetAccountInfoFromPAT,
- jwtValidator.ValidateAndParse,
- accountManager.MarkPATUsed,
- accountManager.CheckUserAccessByJWTGroups,
- claimsExtractor,
- authCfg.Audience,
- authCfg.UserIDClaim,
+ authManager,
+ accountManager.GetAccountIDFromUserAuth,
+ accountManager.SyncUserJWTGroups,
)
corsMiddleware := cors.AllowAll()
- claimsExtractor = jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- )
-
- acMiddleware := middleware.NewAccessControl(
- authCfg.Audience,
- authCfg.UserIDClaim,
- accountManager.GetUser)
+ acMiddleware := middleware.NewAccessControl(accountManager.GetUserFromUserAuth)
rootRouter := mux.NewRouter()
metricsMiddleware := appMetrics.HTTPMiddleware()
prefix := apiPrefix
router := rootRouter.PathPrefix(prefix).Subrouter()
+
router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler, acMiddleware.Handler)
- if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, claimsExtractor, integratedValidator, appMetrics.GetMeter()); err != nil {
+ if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, integratedValidator, appMetrics.GetMeter()); err != nil {
return nil, fmt.Errorf("register integrations endpoints: %w", err)
}
- accounts.AddEndpoints(accountManager, authCfg, router)
- peers.AddEndpoints(accountManager, authCfg, router)
- users.AddEndpoints(accountManager, authCfg, router)
- setup_keys.AddEndpoints(accountManager, authCfg, router)
- policies.AddEndpoints(accountManager, LocationManager, authCfg, router)
- groups.AddEndpoints(accountManager, authCfg, router)
- routes.AddEndpoints(accountManager, authCfg, router)
- dns.AddEndpoints(accountManager, authCfg, router)
- events.AddEndpoints(accountManager, authCfg, router)
- networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, accountManager.GetAccountIDFromToken, authCfg, router)
+ accounts.AddEndpoints(accountManager, router)
+ peers.AddEndpoints(accountManager, router)
+ users.AddEndpoints(accountManager, router)
+ setup_keys.AddEndpoints(accountManager, router)
+ policies.AddEndpoints(accountManager, LocationManager, router)
+ groups.AddEndpoints(accountManager, router)
+ routes.AddEndpoints(accountManager, router)
+ dns.AddEndpoints(accountManager, router)
+ events.AddEndpoints(accountManager, router)
+ networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, router)
return rootRouter, nil
}
diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go
index a23628cdc..bc0054a7f 100644
--- a/management/server/http/handlers/accounts/accounts_handler.go
+++ b/management/server/http/handlers/accounts/accounts_handler.go
@@ -9,47 +9,42 @@ import (
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/account"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
)
// handler is a handler that handles the server.Account HTTP endpoints
type handler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- accountsHandler := newHandler(accountManager, authCfg)
+func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
+ accountsHandler := newHandler(accountManager)
router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS")
router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS")
router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS")
}
// newHandler creates a new handler HTTP handler
-func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
+func newHandler(accountManager server.AccountManager) *handler {
return &handler{
accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
// getAllAccounts is HTTP GET handler that returns a list of accounts. Effectively returns just a single account.
func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
settings, err := h.accountManager.GetAccountSettings(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -62,13 +57,14 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
// updateAccount is HTTP PUT handler that updates the provided account. Updates only account settings (server.Settings)
func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- _, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ _, userID := userAuth.AccountId, userAuth.UserId
+
vars := mux.Vars(r)
accountID := vars["accountId"]
if len(accountID) == 0 {
@@ -125,7 +121,12 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
// deleteAccount is a HTTP DELETE handler to delete an account
func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
+ if err != nil {
+ util.WriteError(r.Context(), err, w)
+ return
+ }
+
vars := mux.Vars(r)
targetAccountID := vars["accountId"]
if len(targetAccountID) == 0 {
@@ -133,7 +134,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
return
}
- err := h.accountManager.DeleteAccount(r.Context(), targetAccountID, claims.UserId)
+ err = h.accountManager.DeleteAccount(r.Context(), targetAccountID, userAuth.UserId)
if err != nil {
util.WriteError(r.Context(), err, w)
return
diff --git a/management/server/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go
index e8a599863..a8d57a13f 100644
--- a/management/server/http/handlers/accounts/accounts_handler_test.go
+++ b/management/server/http/handlers/accounts/accounts_handler_test.go
@@ -13,19 +13,16 @@ import (
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
)
-func initAccountsTestData(account *types.Account, admin *types.User) *handler {
+func initAccountsTestData(account *types.Account) *handler {
return &handler{
accountManager: &mock_server.MockAccountManager{
- GetAccountIDFromTokenFunc: func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return account.Id, admin.Id, nil
- },
GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
return account.Settings, nil
},
@@ -44,15 +41,6 @@ func initAccountsTestData(account *types.Account, admin *types.User) *handler {
return accCopy, nil
},
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: "test_user",
- Domain: "hotmail.com",
- AccountId: "test_account",
- }
- }),
- ),
}
}
@@ -75,7 +63,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
PeerLoginExpiration: time.Hour,
RegularUsersViewBlocked: true,
},
- }, adminUser)
+ })
tt := []struct {
name string
@@ -191,6 +179,11 @@ func TestAccounts_AccountsHandler(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: adminUser.Id,
+ AccountId: accountID,
+ Domain: "hotmail.com",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/accounts", handler.getAllAccounts).Methods("GET")
diff --git a/management/server/http/handlers/dns/dns_settings_handler.go b/management/server/http/handlers/dns/dns_settings_handler.go
index 112eee179..6ff938369 100644
--- a/management/server/http/handlers/dns/dns_settings_handler.go
+++ b/management/server/http/handlers/dns/dns_settings_handler.go
@@ -8,51 +8,44 @@ import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/types"
)
// dnsSettingsHandler is a handler that returns the DNS settings of the account
type dnsSettingsHandler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- addDNSSettingEndpoint(accountManager, authCfg, router)
- addDNSNameserversEndpoint(accountManager, authCfg, router)
+func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
+ addDNSSettingEndpoint(accountManager, router)
+ addDNSNameserversEndpoint(accountManager, router)
}
-func addDNSSettingEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- dnsSettingsHandler := newDNSSettingsHandler(accountManager, authCfg)
+func addDNSSettingEndpoint(accountManager server.AccountManager, router *mux.Router) {
+ dnsSettingsHandler := newDNSSettingsHandler(accountManager)
router.HandleFunc("/dns/settings", dnsSettingsHandler.getDNSSettings).Methods("GET", "OPTIONS")
router.HandleFunc("/dns/settings", dnsSettingsHandler.updateDNSSettings).Methods("PUT", "OPTIONS")
}
// newDNSSettingsHandler returns a new instance of dnsSettingsHandler handler
-func newDNSSettingsHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *dnsSettingsHandler {
- return &dnsSettingsHandler{
- accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
- }
+func newDNSSettingsHandler(accountManager server.AccountManager) *dnsSettingsHandler {
+ return &dnsSettingsHandler{accountManager: accountManager}
}
// getDNSSettings returns the DNS settings for the account
func (h *dnsSettingsHandler) getDNSSettings(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
log.WithContext(r.Context()).Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
dnsSettings, err := h.accountManager.GetDNSSettings(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -68,13 +61,14 @@ func (h *dnsSettingsHandler) getDNSSettings(w http.ResponseWriter, r *http.Reque
// updateDNSSettings handles update to DNS settings of an account
func (h *dnsSettingsHandler) updateDNSSettings(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
var req api.PutApiDnsSettingsJSONRequestBody
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
diff --git a/management/server/http/handlers/dns/dns_settings_handler_test.go b/management/server/http/handlers/dns/dns_settings_handler_test.go
index 9ca1dc032..ca81adf43 100644
--- a/management/server/http/handlers/dns/dns_settings_handler_test.go
+++ b/management/server/http/handlers/dns/dns_settings_handler_test.go
@@ -17,7 +17,8 @@ import (
"github.com/gorilla/mux"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
+
"github.com/netbirdio/netbird/management/server/mock_server"
)
@@ -52,19 +53,7 @@ func initDNSSettingsTestData() *dnsSettingsHandler {
}
return status.Errorf(status.InvalidArgument, "the dns settings provided are nil")
},
- GetAccountIDFromTokenFunc: func(ctx context.Context, _ jwtclaims.AuthorizationClaims) (string, string, error) {
- return testingDNSSettingsAccount.Id, testingDNSSettingsAccount.Users[testDNSSettingsUserID].Id, nil
- },
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: "test_user",
- Domain: "hotmail.com",
- AccountId: testDNSSettingsAccountID,
- }
- }),
- ),
}
}
@@ -118,6 +107,11 @@ func TestDNSSettingsHandlers(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: testingDNSSettingsAccount.Users[testDNSSettingsUserID].Id,
+ AccountId: testingDNSSettingsAccount.Id,
+ Domain: testingDNSSettingsAccount.Domain,
+ })
router := mux.NewRouter()
router.HandleFunc("/api/dns/settings", p.getDNSSettings).Methods("GET")
diff --git a/management/server/http/handlers/dns/nameservers_handler.go b/management/server/http/handlers/dns/nameservers_handler.go
index 09047e231..33d070477 100644
--- a/management/server/http/handlers/dns/nameservers_handler.go
+++ b/management/server/http/handlers/dns/nameservers_handler.go
@@ -10,21 +10,19 @@ import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
)
// nameserversHandler is the nameserver group handler of the account
type nameserversHandler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func addDNSNameserversEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- nameserversHandler := newNameserversHandler(accountManager, authCfg)
+func addDNSNameserversEndpoint(accountManager server.AccountManager, router *mux.Router) {
+ nameserversHandler := newNameserversHandler(accountManager)
router.HandleFunc("/dns/nameservers", nameserversHandler.getAllNameservers).Methods("GET", "OPTIONS")
router.HandleFunc("/dns/nameservers", nameserversHandler.createNameserverGroup).Methods("POST", "OPTIONS")
router.HandleFunc("/dns/nameservers/{nsgroupId}", nameserversHandler.updateNameserverGroup).Methods("PUT", "OPTIONS")
@@ -33,26 +31,21 @@ func addDNSNameserversEndpoint(accountManager server.AccountManager, authCfg con
}
// newNameserversHandler returns a new instance of nameserversHandler handler
-func newNameserversHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *nameserversHandler {
- return &nameserversHandler{
- accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
- }
+func newNameserversHandler(accountManager server.AccountManager) *nameserversHandler {
+ return &nameserversHandler{accountManager: accountManager}
}
// getAllNameservers returns the list of nameserver groups for the account
func (h *nameserversHandler) getAllNameservers(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
log.WithContext(r.Context()).Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
nsGroups, err := h.accountManager.ListNameServerGroups(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -69,13 +62,14 @@ func (h *nameserversHandler) getAllNameservers(w http.ResponseWriter, r *http.Re
// createNameserverGroup handles nameserver group creation request
func (h *nameserversHandler) createNameserverGroup(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
var req api.PostApiDnsNameserversJSONRequestBody
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -102,13 +96,14 @@ func (h *nameserversHandler) createNameserverGroup(w http.ResponseWriter, r *htt
// updateNameserverGroup handles update to a nameserver group identified by a given ID
func (h *nameserversHandler) updateNameserverGroup(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
nsGroupID := mux.Vars(r)["nsgroupId"]
if len(nsGroupID) == 0 {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w)
@@ -153,13 +148,14 @@ func (h *nameserversHandler) updateNameserverGroup(w http.ResponseWriter, r *htt
// deleteNameserverGroup handles nameserver group deletion request
func (h *nameserversHandler) deleteNameserverGroup(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
nsGroupID := mux.Vars(r)["nsgroupId"]
if len(nsGroupID) == 0 {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w)
@@ -177,14 +173,14 @@ func (h *nameserversHandler) deleteNameserverGroup(w http.ResponseWriter, r *htt
// getNameserverGroup handles a nameserver group Get request identified by ID
func (h *nameserversHandler) getNameserverGroup(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
- log.WithContext(r.Context()).Error(err)
- http.Redirect(w, r, "/", http.StatusInternalServerError)
+ util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
nsGroupID := mux.Vars(r)["nsgroupId"]
if len(nsGroupID) == 0 {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid nameserver group ID"), w)
diff --git a/management/server/http/handlers/dns/nameservers_handler_test.go b/management/server/http/handlers/dns/nameservers_handler_test.go
index c6561e4d8..45283bc37 100644
--- a/management/server/http/handlers/dns/nameservers_handler_test.go
+++ b/management/server/http/handlers/dns/nameservers_handler_test.go
@@ -18,7 +18,8 @@ import (
"github.com/gorilla/mux"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
+
"github.com/netbirdio/netbird/management/server/mock_server"
)
@@ -81,19 +82,7 @@ func initNameserversTestData() *nameserversHandler {
}
return status.Errorf(status.NotFound, "nameserver group with ID %s was not found", nsGroupToSave.ID)
},
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return claims.AccountId, claims.UserId, nil
- },
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: "test_user",
- Domain: "hotmail.com",
- AccountId: testNSGroupAccountID,
- }
- }),
- ),
}
}
@@ -204,6 +193,11 @@ func TestNameserversHandlers(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ AccountId: testNSGroupAccountID,
+ Domain: "hotmail.com",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/dns/nameservers/{nsgroupId}", p.getNameserverGroup).Methods("GET")
diff --git a/management/server/http/handlers/events/events_handler.go b/management/server/http/handlers/events/events_handler.go
index 62da59535..0fb2295a8 100644
--- a/management/server/http/handlers/events/events_handler.go
+++ b/management/server/http/handlers/events/events_handler.go
@@ -10,44 +10,37 @@ import (
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
)
// handler HTTP handler
type handler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- eventsHandler := newHandler(accountManager, authCfg)
+func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
+ eventsHandler := newHandler(accountManager)
router.HandleFunc("/events", eventsHandler.getAllEvents).Methods("GET", "OPTIONS")
}
// newHandler creates a new events handler
-func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
- return &handler{
- accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
- }
+func newHandler(accountManager server.AccountManager) *handler {
+ return &handler{accountManager: accountManager}
}
// getAllEvents list of the given account
func (h *handler) getAllEvents(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
log.WithContext(r.Context()).Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
accountEvents, err := h.accountManager.GetEvents(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
diff --git a/management/server/http/handlers/events/events_handler_test.go b/management/server/http/handlers/events/events_handler_test.go
index 17478aba3..3a643fe90 100644
--- a/management/server/http/handlers/events/events_handler_test.go
+++ b/management/server/http/handlers/events/events_handler_test.go
@@ -13,9 +13,10 @@ import (
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
+
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/types"
)
@@ -29,22 +30,10 @@ func initEventsTestData(account string, events ...*activity.Event) *handler {
}
return []*activity.Event{}, nil
},
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return claims.AccountId, claims.UserId, nil
- },
- GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) ([]*types.UserInfo, error) {
- return make([]*types.UserInfo, 0), nil
+ GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) {
+ return make(map[string]*types.UserInfo), nil
},
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: "test_user",
- Domain: "hotmail.com",
- AccountId: "test_account",
- }
- }),
- ),
}
}
@@ -199,6 +188,11 @@ func TestEvents_GetEvents(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_account",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/events/", handler.getAllEvents).Methods("GET")
diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go
index 0ecea7ec2..040c08b87 100644
--- a/management/server/http/handlers/groups/groups_handler.go
+++ b/management/server/http/handlers/groups/groups_handler.go
@@ -7,25 +7,23 @@ import (
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
- "github.com/netbirdio/netbird/management/server/http/configs"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
- "github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
+ "github.com/netbirdio/netbird/management/server/types"
)
// handler is a handler that returns groups of the account
type handler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- groupsHandler := newHandler(accountManager, authCfg)
+func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
+ groupsHandler := newHandler(accountManager)
router.HandleFunc("/groups", groupsHandler.getAllGroups).Methods("GET", "OPTIONS")
router.HandleFunc("/groups", groupsHandler.createGroup).Methods("POST", "OPTIONS")
router.HandleFunc("/groups/{groupId}", groupsHandler.updateGroup).Methods("PUT", "OPTIONS")
@@ -34,25 +32,21 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
}
// newHandler creates a new groups handler
-func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
+func newHandler(accountManager server.AccountManager) *handler {
return &handler{
accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
// getAllGroups list for the account
func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
log.WithContext(r.Context()).Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
groups, err := h.accountManager.GetAllGroups(r.Context(), accountID, userID)
if err != nil {
@@ -76,13 +70,14 @@ func (h *handler) getAllGroups(w http.ResponseWriter, r *http.Request) {
// updateGroup handles update to a group identified by a given ID
func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
vars := mux.Vars(r)
groupID, ok := vars["groupId"]
if !ok {
@@ -165,13 +160,14 @@ func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) {
// createGroup handles group creation request
func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
var req api.PostApiGroupsJSONRequestBody
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -224,13 +220,14 @@ func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) {
// deleteGroup handles group deletion request
func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
groupID := mux.Vars(r)["groupId"]
if len(groupID) == 0 {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid group ID"), w)
@@ -239,8 +236,9 @@ func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) {
err = h.accountManager.DeleteGroup(r.Context(), accountID, userID, groupID)
if err != nil {
- _, ok := err.(*server.GroupLinkError)
- if ok {
+ wrappedErr, ok := err.(interface{ Unwrap() []error })
+ if ok && len(wrappedErr.Unwrap()) > 0 {
+ err = wrappedErr.Unwrap()[0]
util.WriteErrorResponse(err.Error(), http.StatusBadRequest, w)
return
}
@@ -253,12 +251,13 @@ func (h *handler) deleteGroup(w http.ResponseWriter, r *http.Request) {
// getGroup returns a group
func (h *handler) getGroup(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+
+ accountID, userID := userAuth.AccountId, userAuth.UserId
groupID := mux.Vars(r)["groupId"]
if len(groupID) == 0 {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid group ID"), w)
diff --git a/management/server/http/handlers/groups/groups_handler_test.go b/management/server/http/handlers/groups/groups_handler_test.go
index 49805ca9b..c4b9e46ab 100644
--- a/management/server/http/handlers/groups/groups_handler_test.go
+++ b/management/server/http/handlers/groups/groups_handler_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
+ "errors"
"fmt"
"io"
"net"
@@ -13,13 +14,13 @@ import (
"testing"
"github.com/gorilla/mux"
- "github.com/magiconair/properties/assert"
+ "github.com/stretchr/testify/assert"
"golang.org/x/exp/maps"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/status"
@@ -58,9 +59,6 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
return group, nil
},
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return claims.AccountId, claims.UserId, nil
- },
GetGroupByNameFunc: func(ctx context.Context, groupName, _ string) (*types.Group, error) {
if groupName == "All" {
return &types.Group{ID: "id-all", Name: "All", Issued: types.GroupIssuedAPI}, nil
@@ -73,10 +71,12 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
},
DeleteGroupFunc: func(_ context.Context, accountID, userId, groupID string) error {
if groupID == "linked-grp" {
- return &server.GroupLinkError{
+ err := &server.GroupLinkError{
Resource: "something",
Name: "linked-grp",
}
+ var allErrors error
+ return errors.Join(allErrors, err)
}
if groupID == "invalid-grp" {
return fmt.Errorf("internal error")
@@ -84,15 +84,6 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
return nil
},
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: "test_user",
- Domain: "hotmail.com",
- AccountId: "test_id",
- }
- }),
- ),
}
}
@@ -131,6 +122,11 @@ func TestGetGroup(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/groups/{groupId}", p.getGroup).Methods("GET")
@@ -252,6 +248,11 @@ func TestWriteGroup(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/groups", p.createGroup).Methods("POST")
@@ -329,7 +330,11 @@ func TestDeleteGroup(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
-
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/groups/{groupId}", p.deleteGroup).Methods("DELETE")
router.ServeHTTP(recorder, req)
diff --git a/management/server/http/handlers/networks/handler.go b/management/server/http/handlers/networks/handler.go
index 316b93611..bb6b97267 100644
--- a/management/server/http/handlers/networks/handler.go
+++ b/management/server/http/handlers/networks/handler.go
@@ -7,13 +7,13 @@ import (
"net/http"
"github.com/gorilla/mux"
+ log "github.com/sirupsen/logrus"
s "github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/groups"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/networks"
"github.com/netbirdio/netbird/management/server/networks/resources"
"github.com/netbirdio/netbird/management/server/networks/routers"
@@ -30,16 +30,14 @@ type handler struct {
routerManager routers.Manager
accountManager s.AccountManager
- groupsManager groups.Manager
- extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
- claimsExtractor *jwtclaims.ClaimsExtractor
+ groupsManager groups.Manager
}
-func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) {
- addRouterEndpoints(routerManager, extractFromToken, authCfg, router)
- addResourceEndpoints(resourceManager, groupsManager, extractFromToken, authCfg, router)
+func AddEndpoints(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, router *mux.Router) {
+ addRouterEndpoints(routerManager, router)
+ addResourceEndpoints(resourceManager, groupsManager, router)
- networksHandler := newHandler(networksManager, resourceManager, routerManager, groupsManager, accountManager, extractFromToken, authCfg)
+ networksHandler := newHandler(networksManager, resourceManager, routerManager, groupsManager, accountManager)
router.HandleFunc("/networks", networksHandler.getAllNetworks).Methods("GET", "OPTIONS")
router.HandleFunc("/networks", networksHandler.createNetwork).Methods("POST", "OPTIONS")
router.HandleFunc("/networks/{networkId}", networksHandler.getNetwork).Methods("GET", "OPTIONS")
@@ -47,29 +45,25 @@ func AddEndpoints(networksManager networks.Manager, resourceManager resources.Ma
router.HandleFunc("/networks/{networkId}", networksHandler.deleteNetwork).Methods("DELETE", "OPTIONS")
}
-func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *handler {
+func newHandler(networksManager networks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager groups.Manager, accountManager s.AccountManager) *handler {
return &handler{
- networksManager: networksManager,
- resourceManager: resourceManager,
- routerManager: routerManager,
- groupsManager: groupsManager,
- accountManager: accountManager,
- extractFromToken: extractFromToken,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
+ networksManager: networksManager,
+ resourceManager: resourceManager,
+ routerManager: routerManager,
+ groupsManager: groupsManager,
+ accountManager: accountManager,
}
}
func (h *handler) getAllNetworks(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
networks, err := h.networksManager.GetAllNetworks(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -104,12 +98,12 @@ func (h *handler) getAllNetworks(w http.ResponseWriter, r *http.Request) {
}
func (h *handler) createNetwork(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
var req api.NetworkRequest
err = json.NewDecoder(r.Body).Decode(&req)
@@ -140,12 +134,12 @@ func (h *handler) createNetwork(w http.ResponseWriter, r *http.Request) {
}
func (h *handler) getNetwork(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
networkID := vars["networkId"]
@@ -178,13 +172,13 @@ func (h *handler) getNetwork(w http.ResponseWriter, r *http.Request) {
}
func (h *handler) updateNetwork(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
networkID := vars["networkId"]
if len(networkID) == 0 {
@@ -228,13 +222,13 @@ func (h *handler) updateNetwork(w http.ResponseWriter, r *http.Request) {
}
func (h *handler) deleteNetwork(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
networkID := vars["networkId"]
if len(networkID) == 0 {
@@ -281,7 +275,12 @@ func (h *handler) collectIDsInNetwork(ctx context.Context, accountID, userID, ne
}
if len(router.PeerGroups) > 0 {
for _, groupID := range router.PeerGroups {
- peerCounter += len(groups[groupID].Peers)
+ group, ok := groups[groupID]
+ if !ok {
+ log.WithContext(ctx).Warnf("group %s not found", groupID)
+ continue
+ }
+ peerCounter += len(group.Peers)
}
}
}
diff --git a/management/server/http/handlers/networks/resources_handler.go b/management/server/http/handlers/networks/resources_handler.go
index f2dc8e3b8..fba7026e8 100644
--- a/management/server/http/handlers/networks/resources_handler.go
+++ b/management/server/http/handlers/networks/resources_handler.go
@@ -1,30 +1,26 @@
package networks
import (
- "context"
"encoding/json"
"net/http"
"github.com/gorilla/mux"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/groups"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/networks/resources"
"github.com/netbirdio/netbird/management/server/networks/resources/types"
)
type resourceHandler struct {
- resourceManager resources.Manager
- groupsManager groups.Manager
- extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
- claimsExtractor *jwtclaims.ClaimsExtractor
+ resourceManager resources.Manager
+ groupsManager groups.Manager
}
-func addResourceEndpoints(resourcesManager resources.Manager, groupsManager groups.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) {
- resourceHandler := newResourceHandler(resourcesManager, groupsManager, extractFromToken, authCfg)
+func addResourceEndpoints(resourcesManager resources.Manager, groupsManager groups.Manager, router *mux.Router) {
+ resourceHandler := newResourceHandler(resourcesManager, groupsManager)
router.HandleFunc("/networks/resources", resourceHandler.getAllResourcesInAccount).Methods("GET", "OPTIONS")
router.HandleFunc("/networks/{networkId}/resources", resourceHandler.getAllResourcesInNetwork).Methods("GET", "OPTIONS")
router.HandleFunc("/networks/{networkId}/resources", resourceHandler.createResource).Methods("POST", "OPTIONS")
@@ -33,26 +29,21 @@ func addResourceEndpoints(resourcesManager resources.Manager, groupsManager grou
router.HandleFunc("/networks/{networkId}/resources/{resourceId}", resourceHandler.deleteResource).Methods("DELETE", "OPTIONS")
}
-func newResourceHandler(resourceManager resources.Manager, groupsManager groups.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *resourceHandler {
+func newResourceHandler(resourceManager resources.Manager, groupsManager groups.Manager) *resourceHandler {
return &resourceHandler{
- resourceManager: resourceManager,
- groupsManager: groupsManager,
- extractFromToken: extractFromToken,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
+ resourceManager: resourceManager,
+ groupsManager: groupsManager,
}
}
func (h *resourceHandler) getAllResourcesInNetwork(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
networkID := mux.Vars(r)["networkId"]
resources, err := h.resourceManager.GetAllResourcesInNetwork(r.Context(), accountID, userID, networkID)
if err != nil {
@@ -76,13 +67,14 @@ func (h *resourceHandler) getAllResourcesInNetwork(w http.ResponseWriter, r *htt
util.WriteJSONObject(r.Context(), w, resourcesResponse)
}
func (h *resourceHandler) getAllResourcesInAccount(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
resources, err := h.resourceManager.GetAllResourcesInAccount(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -106,13 +98,14 @@ func (h *resourceHandler) getAllResourcesInAccount(w http.ResponseWriter, r *htt
}
func (h *resourceHandler) createResource(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
var req api.NetworkResourceRequest
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -144,13 +137,13 @@ func (h *resourceHandler) createResource(w http.ResponseWriter, r *http.Request)
}
func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
networkID := mux.Vars(r)["networkId"]
resourceID := mux.Vars(r)["resourceId"]
resource, err := h.resourceManager.GetResource(r.Context(), accountID, userID, networkID, resourceID)
@@ -171,13 +164,13 @@ func (h *resourceHandler) getResource(w http.ResponseWriter, r *http.Request) {
}
func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
var req api.NetworkResourceRequest
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -209,12 +202,12 @@ func (h *resourceHandler) updateResource(w http.ResponseWriter, r *http.Request)
}
func (h *resourceHandler) deleteResource(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
networkID := mux.Vars(r)["networkId"]
resourceID := mux.Vars(r)["resourceId"]
diff --git a/management/server/http/handlers/networks/routers_handler.go b/management/server/http/handlers/networks/routers_handler.go
index 7ca95d902..f98da4966 100644
--- a/management/server/http/handlers/networks/routers_handler.go
+++ b/management/server/http/handlers/networks/routers_handler.go
@@ -1,28 +1,24 @@
package networks
import (
- "context"
"encoding/json"
"net/http"
"github.com/gorilla/mux"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/networks/routers"
"github.com/netbirdio/netbird/management/server/networks/routers/types"
)
type routersHandler struct {
- routersManager routers.Manager
- extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
- claimsExtractor *jwtclaims.ClaimsExtractor
+ routersManager routers.Manager
}
-func addRouterEndpoints(routersManager routers.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg, router *mux.Router) {
- routersHandler := newRoutersHandler(routersManager, extractFromToken, authCfg)
+func addRouterEndpoints(routersManager routers.Manager, router *mux.Router) {
+ routersHandler := newRoutersHandler(routersManager)
router.HandleFunc("/networks/{networkId}/routers", routersHandler.getAllRouters).Methods("GET", "OPTIONS")
router.HandleFunc("/networks/{networkId}/routers", routersHandler.createRouter).Methods("POST", "OPTIONS")
router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.getRouter).Methods("GET", "OPTIONS")
@@ -30,25 +26,21 @@ func addRouterEndpoints(routersManager routers.Manager, extractFromToken func(ct
router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.deleteRouter).Methods("DELETE", "OPTIONS")
}
-func newRoutersHandler(routersManager routers.Manager, extractFromToken func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error), authCfg configs.AuthCfg) *routersHandler {
+func newRoutersHandler(routersManager routers.Manager) *routersHandler {
return &routersHandler{
- routersManager: routersManager,
- extractFromToken: extractFromToken,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
+ routersManager: routersManager,
}
}
func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
networkID := mux.Vars(r)["networkId"]
routers, err := h.routersManager.GetAllRoutersInNetwork(r.Context(), accountID, userID, networkID)
if err != nil {
@@ -65,13 +57,14 @@ func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) {
}
func (h *routersHandler) createRouter(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
networkID := mux.Vars(r)["networkId"]
var req api.NetworkRouterRequest
err = json.NewDecoder(r.Body).Decode(&req)
@@ -96,13 +89,14 @@ func (h *routersHandler) createRouter(w http.ResponseWriter, r *http.Request) {
}
func (h *routersHandler) getRouter(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
routerID := mux.Vars(r)["routerId"]
networkID := mux.Vars(r)["networkId"]
router, err := h.routersManager.GetRouter(r.Context(), accountID, userID, networkID, routerID)
@@ -115,13 +109,14 @@ func (h *routersHandler) getRouter(w http.ResponseWriter, r *http.Request) {
}
func (h *routersHandler) updateRouter(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
var req api.NetworkRouterRequest
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -146,13 +141,13 @@ func (h *routersHandler) updateRouter(w http.ResponseWriter, r *http.Request) {
}
func (h *routersHandler) deleteRouter(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.extractFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
routerID := mux.Vars(r)["routerId"]
networkID := mux.Vars(r)["networkId"]
err = h.routersManager.DeleteRouter(r.Context(), accountID, userID, networkID, routerID)
diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go
index cdd8026f2..709ba64d0 100644
--- a/management/server/http/handlers/peers/peers_handler.go
+++ b/management/server/http/handlers/peers/peers_handler.go
@@ -10,11 +10,10 @@ import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/groups"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
@@ -22,12 +21,11 @@ import (
// Handler is a handler that returns peers of the account
type Handler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- peersHandler := NewHandler(accountManager, authCfg)
+func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
+ peersHandler := NewHandler(accountManager)
router.HandleFunc("/peers", peersHandler.GetAllPeers).Methods("GET", "OPTIONS")
router.HandleFunc("/peers/{peerId}", peersHandler.HandlePeer).
Methods("GET", "PUT", "DELETE", "OPTIONS")
@@ -35,13 +33,9 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
}
// NewHandler creates a new peers Handler
-func NewHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *Handler {
+func NewHandler(accountManager server.AccountManager) *Handler {
return &Handler{
accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
@@ -149,12 +143,13 @@ func (h *Handler) deletePeer(ctx context.Context, accountID, userID string, peer
// HandlePeer handles all peer requests for GET, PUT and DELETE operations
func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
peerID := vars["peerId"]
if len(peerID) == 0 {
@@ -179,13 +174,14 @@ func (h *Handler) HandlePeer(w http.ResponseWriter, r *http.Request) {
// GetAllPeers returns a list of all peers associated with a provided account
func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
peers, err := h.accountManager.GetPeers(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -230,13 +226,14 @@ func (h *Handler) setApprovalRequiredFlag(respBody []*api.PeerBatch, approvedPee
// GetAccessiblePeers returns a list of all peers that the specified peer can connect to within the network.
func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
vars := mux.Vars(r)
peerID := vars["peerId"]
if len(peerID) == 0 {
@@ -338,6 +335,7 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain),
+ ExtraDnsLabels: fqdnList(peer.ExtraDNSLabels, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.GetLastLogin(),
LoginExpired: peer.Status.LoginExpired,
@@ -372,6 +370,7 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain),
+ ExtraDnsLabels: fqdnList(peer.ExtraDNSLabels, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.GetLastLogin(),
LoginExpired: peer.Status.LoginExpired,
@@ -392,3 +391,11 @@ func fqdn(peer *nbpeer.Peer, dnsDomain string) string {
return fqdn
}
}
+func fqdnList(extraLabels []string, dnsDomain string) []string {
+ fqdnList := make([]string, 0, len(extraLabels))
+ for _, label := range extraLabels {
+ fqdn := fmt.Sprintf("%s.%s", label, dnsDomain)
+ fqdnList = append(fqdnList, fqdn)
+ }
+ return fqdnList
+}
diff --git a/management/server/http/handlers/peers/peers_handler_test.go b/management/server/http/handlers/peers/peers_handler_test.go
index 16065a677..8e07567c7 100644
--- a/management/server/http/handlers/peers/peers_handler_test.go
+++ b/management/server/http/handlers/peers/peers_handler_test.go
@@ -15,8 +15,8 @@ import (
"github.com/gorilla/mux"
"golang.org/x/exp/maps"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/types"
@@ -25,16 +25,13 @@ import (
"github.com/netbirdio/netbird/management/server/mock_server"
)
-type ctxKey string
-
const (
testPeerID = "test_peer"
noUpdateChannelTestPeerID = "no-update-channel"
- adminUser = "admin_user"
- regularUser = "regular_user"
- serviceUser = "service_user"
- userIDKey ctxKey = "user_id"
+ adminUser = "admin_user"
+ regularUser = "regular_user"
+ serviceUser = "service_user"
)
func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
@@ -146,8 +143,8 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
GetDNSDomainFunc: func() string {
return "netbird.selfhosted"
},
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return claims.AccountId, claims.UserId, nil
+ GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) {
+ return account, nil
},
GetAccountFunc: func(ctx context.Context, accountID string) (*types.Account, error) {
return account, nil
@@ -167,16 +164,6 @@ func initTestMetaData(peers ...*nbpeer.Peer) *Handler {
return ok
},
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- userID := r.Context().Value(userIDKey).(string)
- return jwtclaims.AuthorizationClaims{
- UserId: userID,
- Domain: "hotmail.com",
- AccountId: "test_id",
- }
- }),
- ),
}
}
@@ -267,8 +254,11 @@ func TestGetPeers(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
- ctx := context.WithValue(context.Background(), userIDKey, "admin_user")
- req = req.WithContext(ctx)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "admin_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/peers/", p.GetAllPeers).Methods("GET")
@@ -412,8 +402,11 @@ func TestGetAccessiblePeers(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/peers/%s/accessible-peers", tc.peerID), nil)
- ctx := context.WithValue(context.Background(), userIDKey, tc.callerUserID)
- req = req.WithContext(ctx)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: tc.callerUserID,
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/peers/{peerId}/accessible-peers", p.GetAccessiblePeers).Methods("GET")
diff --git a/management/server/http/handlers/policies/geolocation_handler_test.go b/management/server/http/handlers/policies/geolocation_handler_test.go
index fc5839baa..fbdc324d6 100644
--- a/management/server/http/handlers/policies/geolocation_handler_test.go
+++ b/management/server/http/handlers/policies/geolocation_handler_test.go
@@ -13,9 +13,9 @@ import (
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/util"
@@ -43,23 +43,11 @@ func initGeolocationTestData(t *testing.T) *geolocationsHandler {
return &geolocationsHandler{
accountManager: &mock_server.MockAccountManager{
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return claims.AccountId, claims.UserId, nil
- },
GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) {
return types.NewAdminUser(id), nil
},
},
geolocationManager: geo,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: "test_user",
- Domain: "hotmail.com",
- AccountId: "test_id",
- }
- }),
- ),
}
}
@@ -112,6 +100,11 @@ func TestGetCitiesByCountry(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/locations/countries/{country}/cities", geolocationHandler.getCitiesByCountry).Methods("GET")
@@ -200,6 +193,11 @@ func TestGetAllCountries(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/locations/countries", geolocationHandler.getAllCountries).Methods("GET")
diff --git a/management/server/http/handlers/policies/geolocations_handler.go b/management/server/http/handlers/policies/geolocations_handler.go
index 161d97402..c4868f879 100644
--- a/management/server/http/handlers/policies/geolocations_handler.go
+++ b/management/server/http/handlers/policies/geolocations_handler.go
@@ -7,11 +7,10 @@ import (
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
)
@@ -23,24 +22,19 @@ var (
type geolocationsHandler struct {
accountManager server.AccountManager
geolocationManager geolocation.Geolocation
- claimsExtractor *jwtclaims.ClaimsExtractor
}
-func addLocationsEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) {
- locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager, authCfg)
+func addLocationsEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) {
+ locationHandler := newGeolocationsHandlerHandler(accountManager, locationManager)
router.HandleFunc("/locations/countries", locationHandler.getAllCountries).Methods("GET", "OPTIONS")
router.HandleFunc("/locations/countries/{country}/cities", locationHandler.getCitiesByCountry).Methods("GET", "OPTIONS")
}
// newGeolocationsHandlerHandler creates a new Geolocations handler
-func newGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation, authCfg configs.AuthCfg) *geolocationsHandler {
+func newGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *geolocationsHandler {
return &geolocationsHandler{
accountManager: accountManager,
geolocationManager: geolocationManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
@@ -104,12 +98,13 @@ func (l *geolocationsHandler) getCitiesByCountry(w http.ResponseWriter, r *http.
}
func (l *geolocationsHandler) authenticateUser(r *http.Request) error {
- claims := l.claimsExtractor.FromRequestContext(r)
- _, userID, err := l.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
return err
}
+ _, userID := userAuth.AccountId, userAuth.UserId
+
user, err := l.accountManager.GetUserByID(r.Context(), userID)
if err != nil {
return err
diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go
index a748e73b8..63fc8a03b 100644
--- a/management/server/http/handlers/policies/policies_handler.go
+++ b/management/server/http/handlers/policies/policies_handler.go
@@ -8,51 +8,46 @@ import (
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
)
// handler is a handler that returns policy of the account
type handler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func AddEndpoints(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) {
- policiesHandler := newHandler(accountManager, authCfg)
+func AddEndpoints(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) {
+ policiesHandler := newHandler(accountManager)
router.HandleFunc("/policies", policiesHandler.getAllPolicies).Methods("GET", "OPTIONS")
router.HandleFunc("/policies", policiesHandler.createPolicy).Methods("POST", "OPTIONS")
router.HandleFunc("/policies/{policyId}", policiesHandler.updatePolicy).Methods("PUT", "OPTIONS")
router.HandleFunc("/policies/{policyId}", policiesHandler.getPolicy).Methods("GET", "OPTIONS")
router.HandleFunc("/policies/{policyId}", policiesHandler.deletePolicy).Methods("DELETE", "OPTIONS")
- addPostureCheckEndpoint(accountManager, locationManager, authCfg, router)
+ addPostureCheckEndpoint(accountManager, locationManager, router)
}
// newHandler creates a new policies handler
-func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
+func newHandler(accountManager server.AccountManager) *handler {
return &handler{
accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
// getAllPolicies list for the account
func (h *handler) getAllPolicies(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
listPolicies, err := h.accountManager.ListPolicies(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -80,13 +75,14 @@ func (h *handler) getAllPolicies(w http.ResponseWriter, r *http.Request) {
// updatePolicy handles update to a policy identified by a given ID
func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
vars := mux.Vars(r)
policyID := vars["policyId"]
if len(policyID) == 0 {
@@ -105,13 +101,14 @@ func (h *handler) updatePolicy(w http.ResponseWriter, r *http.Request) {
// createPolicy handles policy creation request
func (h *handler) createPolicy(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
h.savePolicy(w, r, accountID, userID, "")
}
@@ -306,13 +303,13 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s
// deletePolicy handles policy deletion request
func (h *handler) deletePolicy(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
policyID := vars["policyId"]
if len(policyID) == 0 {
@@ -330,13 +327,14 @@ func (h *handler) deletePolicy(w http.ResponseWriter, r *http.Request) {
// getPolicy handles a group Get request identified by ID
func (h *handler) getPolicy(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
vars := mux.Vars(r)
policyID := vars["policyId"]
if len(policyID) == 0 {
diff --git a/management/server/http/handlers/policies/policies_handler_test.go b/management/server/http/handlers/policies/policies_handler_test.go
index 3e1be187c..6450295eb 100644
--- a/management/server/http/handlers/policies/policies_handler_test.go
+++ b/management/server/http/handlers/policies/policies_handler_test.go
@@ -10,17 +10,14 @@ import (
"strings"
"testing"
+ "github.com/gorilla/mux"
+ "github.com/stretchr/testify/assert"
+
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
+ "github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
-
- "github.com/gorilla/mux"
-
- "github.com/netbirdio/netbird/management/server/jwtclaims"
-
- "github.com/magiconair/properties/assert"
-
- "github.com/netbirdio/netbird/management/server/mock_server"
)
func initPoliciesTestData(policies ...*types.Policy) *handler {
@@ -47,9 +44,6 @@ func initPoliciesTestData(policies ...*types.Policy) *handler {
GetAllGroupsFunc: func(ctx context.Context, accountID, userID string) ([]*types.Group, error) {
return []*types.Group{{ID: "F"}, {ID: "G"}}, nil
},
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return claims.AccountId, claims.UserId, nil
- },
GetAccountByIDFunc: func(ctx context.Context, accountID string, userID string) (*types.Account, error) {
user := types.NewAdminUser(userID)
return &types.Account{
@@ -68,15 +62,6 @@ func initPoliciesTestData(policies ...*types.Policy) *handler {
}, nil
},
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: "test_user",
- Domain: "hotmail.com",
- AccountId: "test_id",
- }
- }),
- ),
}
}
@@ -118,6 +103,11 @@ func TestPoliciesGetPolicy(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/policies/{policyId}", p.getPolicy).Methods("GET")
@@ -277,6 +267,11 @@ func TestPoliciesWritePolicy(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/policies", p.createPolicy).Methods("POST")
diff --git a/management/server/http/handlers/policies/posture_checks_handler.go b/management/server/http/handlers/policies/posture_checks_handler.go
index ce0d4878c..e6e58da58 100644
--- a/management/server/http/handlers/policies/posture_checks_handler.go
+++ b/management/server/http/handlers/policies/posture_checks_handler.go
@@ -7,11 +7,10 @@ import (
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/status"
)
@@ -20,40 +19,35 @@ import (
type postureChecksHandler struct {
accountManager server.AccountManager
geolocationManager geolocation.Geolocation
- claimsExtractor *jwtclaims.ClaimsExtractor
}
-func addPostureCheckEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, authCfg configs.AuthCfg, router *mux.Router) {
- postureCheckHandler := newPostureChecksHandler(accountManager, locationManager, authCfg)
+func addPostureCheckEndpoint(accountManager server.AccountManager, locationManager geolocation.Geolocation, router *mux.Router) {
+ postureCheckHandler := newPostureChecksHandler(accountManager, locationManager)
router.HandleFunc("/posture-checks", postureCheckHandler.getAllPostureChecks).Methods("GET", "OPTIONS")
router.HandleFunc("/posture-checks", postureCheckHandler.createPostureCheck).Methods("POST", "OPTIONS")
router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.updatePostureCheck).Methods("PUT", "OPTIONS")
router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.getPostureCheck).Methods("GET", "OPTIONS")
router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.deletePostureCheck).Methods("DELETE", "OPTIONS")
- addLocationsEndpoint(accountManager, locationManager, authCfg, router)
+ addLocationsEndpoint(accountManager, locationManager, router)
}
// newPostureChecksHandler creates a new PostureChecks handler
-func newPostureChecksHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation, authCfg configs.AuthCfg) *postureChecksHandler {
+func newPostureChecksHandler(accountManager server.AccountManager, geolocationManager geolocation.Geolocation) *postureChecksHandler {
return &postureChecksHandler{
accountManager: accountManager,
geolocationManager: geolocationManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
// getAllPostureChecks list for the account
func (p *postureChecksHandler) getAllPostureChecks(w http.ResponseWriter, r *http.Request) {
- claims := p.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
listPostureChecks, err := p.accountManager.ListPostureChecks(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -70,13 +64,14 @@ func (p *postureChecksHandler) getAllPostureChecks(w http.ResponseWriter, r *htt
// updatePostureCheck handles update to a posture check identified by a given ID
func (p *postureChecksHandler) updatePostureCheck(w http.ResponseWriter, r *http.Request) {
- claims := p.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
vars := mux.Vars(r)
postureChecksID := vars["postureCheckId"]
if len(postureChecksID) == 0 {
@@ -95,25 +90,26 @@ func (p *postureChecksHandler) updatePostureCheck(w http.ResponseWriter, r *http
// createPostureCheck handles posture check creation request
func (p *postureChecksHandler) createPostureCheck(w http.ResponseWriter, r *http.Request) {
- claims := p.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
p.savePostureChecks(w, r, accountID, userID, "")
}
// getPostureCheck handles a posture check Get request identified by ID
func (p *postureChecksHandler) getPostureCheck(w http.ResponseWriter, r *http.Request) {
- claims := p.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
postureChecksID := vars["postureCheckId"]
if len(postureChecksID) == 0 {
@@ -132,13 +128,13 @@ func (p *postureChecksHandler) getPostureCheck(w http.ResponseWriter, r *http.Re
// deletePostureCheck handles posture check deletion request
func (p *postureChecksHandler) deletePostureCheck(w http.ResponseWriter, r *http.Request) {
- claims := p.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := p.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
postureChecksID := vars["postureCheckId"]
if len(postureChecksID) == 0 {
diff --git a/management/server/http/handlers/policies/posture_checks_handler_test.go b/management/server/http/handlers/policies/posture_checks_handler_test.go
index 237687fd4..e3844caa2 100644
--- a/management/server/http/handlers/policies/posture_checks_handler_test.go
+++ b/management/server/http/handlers/policies/posture_checks_handler_test.go
@@ -14,9 +14,9 @@ import (
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/status"
@@ -66,20 +66,8 @@ func initPostureChecksTestData(postureChecks ...*posture.Checks) *postureChecksH
}
return accountPostureChecks, nil
},
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return claims.AccountId, claims.UserId, nil
- },
},
geolocationManager: &geolocation.Mock{},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: "test_user",
- Domain: "hotmail.com",
- AccountId: "test_id",
- }
- }),
- ),
}
}
@@ -187,6 +175,11 @@ func TestGetPostureCheck(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/posture-checks/"+tc.id, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/posture-checks/{postureCheckId}", p.getPostureCheck).Methods("GET")
@@ -835,6 +828,11 @@ func TestPostureCheckUpdate(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: "test_id",
+ })
defaultHandler := *p
if tc.setupHandlerFunc != nil {
diff --git a/management/server/http/handlers/routes/routes_handler.go b/management/server/http/handlers/routes/routes_handler.go
index a29ba4562..0f0d24780 100644
--- a/management/server/http/handlers/routes/routes_handler.go
+++ b/management/server/http/handlers/routes/routes_handler.go
@@ -2,36 +2,30 @@ package routes
import (
"encoding/json"
- "fmt"
"net/http"
"net/netip"
- "regexp"
- "strings"
"unicode/utf8"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/domain"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/route"
)
-const maxDomains = 32
const failedToConvertRoute = "failed to convert route to response: %v"
// handler is the routes handler of the account
type handler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- routesHandler := newHandler(accountManager, authCfg)
+func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
+ routesHandler := newHandler(accountManager)
router.HandleFunc("/routes", routesHandler.getAllRoutes).Methods("GET", "OPTIONS")
router.HandleFunc("/routes", routesHandler.createRoute).Methods("POST", "OPTIONS")
router.HandleFunc("/routes/{routeId}", routesHandler.updateRoute).Methods("PUT", "OPTIONS")
@@ -40,25 +34,22 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
}
// newHandler returns a new instance of routes handler
-func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
+func newHandler(accountManager server.AccountManager) *handler {
return &handler{
accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
// getAllRoutes returns the list of routes for the account
func (h *handler) getAllRoutes(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
routes, err := h.accountManager.ListRoutes(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -79,13 +70,14 @@ func (h *handler) getAllRoutes(w http.ResponseWriter, r *http.Request) {
// createRoute handles route creation request
func (h *handler) createRoute(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
var req api.PostApiRoutesJSONRequestBody
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -102,7 +94,7 @@ func (h *handler) createRoute(w http.ResponseWriter, r *http.Request) {
var networkType route.NetworkType
var newPrefix netip.Prefix
if req.Domains != nil {
- d, err := validateDomains(*req.Domains)
+ d, err := domain.ValidateDomains(*req.Domains)
if err != nil {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w)
return
@@ -176,13 +168,13 @@ func (h *handler) validateRoute(req api.PostApiRoutesJSONRequestBody) error {
// updateRoute handles update to a route identified by a given ID
func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
routeID := vars["routeId"]
if len(routeID) == 0 {
@@ -225,7 +217,7 @@ func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
}
if req.Domains != nil {
- d, err := validateDomains(*req.Domains)
+ d, err := domain.ValidateDomains(*req.Domains)
if err != nil {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w)
return
@@ -269,13 +261,13 @@ func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
// deleteRoute handles route deletion request
func (h *handler) deleteRoute(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
routeID := mux.Vars(r)["routeId"]
if len(routeID) == 0 {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid route ID"), w)
@@ -293,13 +285,14 @@ func (h *handler) deleteRoute(w http.ResponseWriter, r *http.Request) {
// getRoute handles a route Get request identified by ID
func (h *handler) getRoute(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
+
routeID := mux.Vars(r)["routeId"]
if len(routeID) == 0 {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid route ID"), w)
@@ -350,34 +343,3 @@ func toRouteResponse(serverRoute *route.Route) (*api.Route, error) {
}
return route, nil
}
-
-// validateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
-func validateDomains(domains []string) (domain.List, error) {
- if len(domains) == 0 {
- return nil, fmt.Errorf("domains list is empty")
- }
- if len(domains) > maxDomains {
- return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
- }
-
- domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
-
- var domainList domain.List
-
- for _, d := range domains {
- d := strings.ToLower(d)
-
- // handles length and idna conversion
- punycode, err := domain.FromString(d)
- if err != nil {
- return domainList, fmt.Errorf("failed to convert domain to punycode: %s: %v", d, err)
- }
-
- if !domainRegex.MatchString(string(punycode)) {
- return domainList, fmt.Errorf("invalid domain format: %s", d)
- }
-
- domainList = append(domainList, punycode)
- }
- return domainList, nil
-}
diff --git a/management/server/http/handlers/routes/routes_handler_test.go b/management/server/http/handlers/routes/routes_handler_test.go
index 45c465587..ad1f8912d 100644
--- a/management/server/http/handlers/routes/routes_handler_test.go
+++ b/management/server/http/handlers/routes/routes_handler_test.go
@@ -11,21 +11,17 @@ import (
"net/netip"
"testing"
- "github.com/netbirdio/netbird/management/server/util"
+ "github.com/gorilla/mux"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/netbirdio/netbird/management/server/http/api"
- nbpeer "github.com/netbirdio/netbird/management/server/peer"
- "github.com/netbirdio/netbird/management/server/status"
- "github.com/netbirdio/netbird/management/server/types"
- "github.com/netbirdio/netbird/route"
-
- "github.com/gorilla/mux"
- "github.com/magiconair/properties/assert"
-
"github.com/netbirdio/netbird/management/domain"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
+ "github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/mock_server"
+ "github.com/netbirdio/netbird/management/server/status"
+ "github.com/netbirdio/netbird/management/server/util"
+ "github.com/netbirdio/netbird/route"
)
const (
@@ -62,32 +58,6 @@ var baseExistingRoute = &route.Route{
Groups: []string{existingGroupID},
}
-var testingAccount = &types.Account{
- Id: testAccountID,
- Domain: "hotmail.com",
- Peers: map[string]*nbpeer.Peer{
- existingPeerID: {
- Key: existingPeerKey,
- IP: netip.MustParseAddr(existingPeerIP1).AsSlice(),
- ID: existingPeerID,
- Meta: nbpeer.PeerSystemMeta{
- GoOS: "linux",
- },
- },
- nonLinuxExistingPeerID: {
- Key: nonLinuxExistingPeerID,
- IP: netip.MustParseAddr(existingPeerIP2).AsSlice(),
- ID: nonLinuxExistingPeerID,
- Meta: nbpeer.PeerSystemMeta{
- GoOS: "darwin",
- },
- },
- },
- Users: map[string]*types.User{
- "test_user": types.NewAdminUser("test_user"),
- },
-}
-
func initRoutesTestData() *handler {
return &handler{
accountManager: &mock_server.MockAccountManager{
@@ -152,20 +122,7 @@ func initRoutesTestData() *handler {
}
return nil
},
- GetAccountIDFromTokenFunc: func(_ context.Context, _ jwtclaims.AuthorizationClaims) (string, string, error) {
- // return testingAccount, testingAccount.Users["test_user"], nil
- return testingAccount.Id, testingAccount.Users["test_user"].Id, nil
- },
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: "test_user",
- Domain: "hotmail.com",
- AccountId: testAccountID,
- }
- }),
- ),
}
}
@@ -528,6 +485,11 @@ func TestRoutesHandlers(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: "test_user",
+ Domain: "hotmail.com",
+ AccountId: testAccountID,
+ })
router := mux.NewRouter()
router.HandleFunc("/api/routes/{routeId}", p.getRoute).Methods("GET")
@@ -563,96 +525,6 @@ func TestRoutesHandlers(t *testing.T) {
}
}
-func TestValidateDomains(t *testing.T) {
- tests := []struct {
- name string
- domains []string
- expected domain.List
- wantErr bool
- }{
- {
- name: "Empty list",
- domains: nil,
- expected: nil,
- wantErr: true,
- },
- {
- name: "Valid ASCII domain",
- domains: []string{"sub.ex-ample.com"},
- expected: domain.List{"sub.ex-ample.com"},
- wantErr: false,
- },
- {
- name: "Valid Unicode domain",
- domains: []string{"münchen.de"},
- expected: domain.List{"xn--mnchen-3ya.de"},
- wantErr: false,
- },
- {
- name: "Valid Unicode, all labels",
- domains: []string{"中国.中国.中国"},
- expected: domain.List{"xn--fiqs8s.xn--fiqs8s.xn--fiqs8s"},
- wantErr: false,
- },
- {
- name: "With underscores",
- domains: []string{"_jabber._tcp.gmail.com"},
- expected: domain.List{"_jabber._tcp.gmail.com"},
- wantErr: false,
- },
- {
- name: "Invalid domain format",
- domains: []string{"-example.com"},
- expected: nil,
- wantErr: true,
- },
- {
- name: "Invalid domain format 2",
- domains: []string{"example.com-"},
- expected: nil,
- wantErr: true,
- },
- {
- name: "Multiple domains valid and invalid",
- domains: []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
- expected: domain.List{"google.com"},
- wantErr: true,
- },
- {
- name: "Valid wildcard domain",
- domains: []string{"*.example.com"},
- expected: domain.List{"*.example.com"},
- wantErr: false,
- },
- {
- name: "Wildcard with dot domain",
- domains: []string{".*.example.com"},
- expected: nil,
- wantErr: true,
- },
- {
- name: "Wildcard with dot domain",
- domains: []string{".*.example.com"},
- expected: nil,
- wantErr: true,
- },
- {
- name: "Invalid wildcard domain",
- domains: []string{"a.*.example.com"},
- expected: nil,
- wantErr: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := validateDomains(tt.domains)
- assert.Equal(t, tt.wantErr, err != nil)
- assert.Equal(t, got, tt.expected)
- })
- }
-}
-
func toApiRoute(t *testing.T, r *route.Route) *api.Route {
t.Helper()
diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler.go b/management/server/http/handlers/setup_keys/setupkeys_handler.go
index 67e296901..8095f43b0 100644
--- a/management/server/http/handlers/setup_keys/setupkeys_handler.go
+++ b/management/server/http/handlers/setup_keys/setupkeys_handler.go
@@ -3,28 +3,27 @@ package setup_keys
import (
"context"
"encoding/json"
+
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
)
// handler is a handler that returns a list of setup keys of the account
type handler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- keysHandler := newHandler(accountManager, authCfg)
+func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
+ keysHandler := newHandler(accountManager)
router.HandleFunc("/setup-keys", keysHandler.getAllSetupKeys).Methods("GET", "OPTIONS")
router.HandleFunc("/setup-keys", keysHandler.createSetupKey).Methods("POST", "OPTIONS")
router.HandleFunc("/setup-keys/{keyId}", keysHandler.getSetupKey).Methods("GET", "OPTIONS")
@@ -33,25 +32,21 @@ func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg,
}
// newHandler creates a new setup key handler
-func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
+func newHandler(accountManager server.AccountManager) *handler {
return &handler{
accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
// createSetupKey is a POST requests that creates a new SetupKey
func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
req := &api.PostApiSetupKeysJSONRequestBody{}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -86,8 +81,13 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
ephemeral = *req.Ephemeral
}
+ var allowExtraDNSLabels bool
+ if req.AllowExtraDnsLabels != nil {
+ allowExtraDNSLabels = *req.AllowExtraDnsLabels
+ }
+
setupKey, err := h.accountManager.CreateSetupKey(r.Context(), accountID, req.Name, types.SetupKeyType(req.Type), expiresIn,
- req.AutoGroups, req.UsageLimit, userID, ephemeral)
+ req.AutoGroups, req.UsageLimit, userID, ephemeral, allowExtraDNSLabels)
if err != nil {
util.WriteError(r.Context(), err, w)
return
@@ -102,12 +102,12 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
// getSetupKey is a GET request to get a SetupKey by ID
func (h *handler) getSetupKey(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
keyID := vars["keyId"]
@@ -127,13 +127,13 @@ func (h *handler) getSetupKey(w http.ResponseWriter, r *http.Request) {
// updateSetupKey is a PUT request to update server.SetupKey
func (h *handler) updateSetupKey(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
keyID := vars["keyId"]
if len(keyID) == 0 {
@@ -168,13 +168,13 @@ func (h *handler) updateSetupKey(w http.ResponseWriter, r *http.Request) {
// getAllSetupKeys is a GET request that returns a list of SetupKey
func (h *handler) getAllSetupKeys(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
setupKeys, err := h.accountManager.ListSetupKeys(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -190,13 +190,13 @@ func (h *handler) getAllSetupKeys(w http.ResponseWriter, r *http.Request) {
}
func (h *handler) deleteSetupKey(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
keyID := vars["keyId"]
if len(keyID) == 0 {
@@ -237,19 +237,20 @@ func ToResponseBody(key *types.SetupKey) *api.SetupKey {
}
return &api.SetupKey{
- Id: key.Id,
- Key: key.KeySecret,
- Name: key.Name,
- Expires: key.GetExpiresAt(),
- Type: string(key.Type),
- Valid: key.IsValid(),
- Revoked: key.Revoked,
- UsedTimes: key.UsedTimes,
- LastUsed: key.GetLastUsed(),
- State: state,
- AutoGroups: key.AutoGroups,
- UpdatedAt: key.UpdatedAt,
- UsageLimit: key.UsageLimit,
- Ephemeral: key.Ephemeral,
+ Id: key.Id,
+ Key: key.KeySecret,
+ Name: key.Name,
+ Expires: key.GetExpiresAt(),
+ Type: string(key.Type),
+ Valid: key.IsValid(),
+ Revoked: key.Revoked,
+ UsedTimes: key.UsedTimes,
+ LastUsed: key.GetLastUsed(),
+ State: state,
+ AutoGroups: key.AutoGroups,
+ UpdatedAt: key.UpdatedAt,
+ UsageLimit: key.UsageLimit,
+ Ephemeral: key.Ephemeral,
+ AllowExtraDnsLabels: key.AllowExtraDNSLabels,
}
}
diff --git a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
index f56227c10..e9135469f 100644
--- a/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
+++ b/management/server/http/handlers/setup_keys/setupkeys_handler_test.go
@@ -14,8 +14,8 @@ import (
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
@@ -28,20 +28,16 @@ const (
notFoundSetupKeyID = "notFoundSetupKeyID"
)
-func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKey, updatedSetupKey *types.SetupKey,
- user *types.User,
-) *handler {
+func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKey, updatedSetupKey *types.SetupKey) *handler {
return &handler{
accountManager: &mock_server.MockAccountManager{
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return claims.AccountId, claims.UserId, nil
- },
CreateSetupKeyFunc: func(_ context.Context, _ string, keyName string, typ types.SetupKeyType, _ time.Duration, _ []string,
- _ int, _ string, ephemeral bool,
+ _ int, _ string, ephemeral bool, allowExtraDNSLabels bool,
) (*types.SetupKey, error) {
if keyName == newKey.Name || typ != newKey.Type {
nk := newKey.Copy()
nk.Ephemeral = ephemeral
+ nk.AllowExtraDNSLabels = allowExtraDNSLabels
return nk, nil
}
return nil, fmt.Errorf("failed creating setup key")
@@ -75,15 +71,6 @@ func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKe
return status.Errorf(status.NotFound, "key %s not found", keyID)
},
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: user.Id,
- Domain: "hotmail.com",
- AccountId: "testAccountId",
- }
- }),
- ),
}
}
@@ -94,7 +81,7 @@ func TestSetupKeysHandlers(t *testing.T) {
adminUser := types.NewAdminUser("test_user")
newSetupKey, plainKey := types.GenerateSetupKey(newSetupKeyName, types.SetupKeyReusable, 0, []string{"group-1"},
- types.SetupKeyUnlimitedUsage, true)
+ types.SetupKeyUnlimitedUsage, true, false)
newSetupKey.Key = plainKey
updatedDefaultSetupKey := defaultSetupKey.Copy()
updatedDefaultSetupKey.AutoGroups = []string{"group-1"}
@@ -170,12 +157,17 @@ func TestSetupKeysHandlers(t *testing.T) {
},
}
- handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey, adminUser)
+ handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey)
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: adminUser.Id,
+ Domain: "hotmail.com",
+ AccountId: "testAccountId",
+ })
router := mux.NewRouter()
router.HandleFunc("/api/setup-keys", handler.getAllSetupKeys).Methods("GET", "OPTIONS")
diff --git a/management/server/http/handlers/users/pat_handler.go b/management/server/http/handlers/users/pat_handler.go
index 7b93d2ae1..84fbef93e 100644
--- a/management/server/http/handlers/users/pat_handler.go
+++ b/management/server/http/handlers/users/pat_handler.go
@@ -7,22 +7,20 @@ import (
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
)
// patHandler is the nameserver group handler of the account
type patHandler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func addUsersTokensEndpoint(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- tokenHandler := newPATsHandler(accountManager, authCfg)
+func addUsersTokensEndpoint(accountManager server.AccountManager, router *mux.Router) {
+ tokenHandler := newPATsHandler(accountManager)
router.HandleFunc("/users/{userId}/tokens", tokenHandler.getAllTokens).Methods("GET", "OPTIONS")
router.HandleFunc("/users/{userId}/tokens", tokenHandler.createToken).Methods("POST", "OPTIONS")
router.HandleFunc("/users/{userId}/tokens/{tokenId}", tokenHandler.getToken).Methods("GET", "OPTIONS")
@@ -30,25 +28,21 @@ func addUsersTokensEndpoint(accountManager server.AccountManager, authCfg config
}
// newPATsHandler creates a new patHandler HTTP handler
-func newPATsHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *patHandler {
+func newPATsHandler(accountManager server.AccountManager) *patHandler {
return &patHandler{
accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
// getAllTokens is HTTP GET handler that returns a list of all personal access tokens for the given user
func (h *patHandler) getAllTokens(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
targetUserID := vars["userId"]
if len(userID) == 0 {
@@ -72,13 +66,13 @@ func (h *patHandler) getAllTokens(w http.ResponseWriter, r *http.Request) {
// getToken is HTTP GET handler that returns a personal access token for the given user
func (h *patHandler) getToken(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
targetUserID := vars["userId"]
if len(targetUserID) == 0 {
@@ -103,13 +97,13 @@ func (h *patHandler) getToken(w http.ResponseWriter, r *http.Request) {
// createToken is HTTP POST handler that creates a personal access token for the given user
func (h *patHandler) createToken(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
targetUserID := vars["userId"]
if len(targetUserID) == 0 {
@@ -135,13 +129,13 @@ func (h *patHandler) createToken(w http.ResponseWriter, r *http.Request) {
// deleteToken is HTTP DELETE handler that deletes a personal access token for the given user
func (h *patHandler) deleteToken(w http.ResponseWriter, r *http.Request) {
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
targetUserID := vars["userId"]
if len(targetUserID) == 0 {
diff --git a/management/server/http/handlers/users/pat_handler_test.go b/management/server/http/handlers/users/pat_handler_test.go
index 9388067a4..6593de64a 100644
--- a/management/server/http/handlers/users/pat_handler_test.go
+++ b/management/server/http/handlers/users/pat_handler_test.go
@@ -12,11 +12,12 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/gorilla/mux"
- "github.com/netbirdio/netbird/management/server/util"
"github.com/stretchr/testify/assert"
+ "github.com/netbirdio/netbird/management/server/util"
+
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
@@ -77,10 +78,6 @@ func initPATTestData() *patHandler {
PersonalAccessToken: types.PersonalAccessToken{},
}, nil
},
-
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return claims.AccountId, claims.UserId, nil
- },
DeletePATFunc: func(_ context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error {
if accountID != existingAccountID {
return status.Errorf(status.NotFound, "account with ID %s not found", accountID)
@@ -115,15 +112,6 @@ func initPATTestData() *patHandler {
return []*types.PersonalAccessToken{testAccount.Users[existingUserID].PATs[existingTokenID], testAccount.Users[existingUserID].PATs["token2"]}, nil
},
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: existingUserID,
- Domain: testDomain,
- AccountId: existingAccountID,
- }
- }),
- ),
}
}
@@ -185,6 +173,11 @@ func TestTokenHandlers(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: existingUserID,
+ Domain: testDomain,
+ AccountId: existingAccountID,
+ })
router := mux.NewRouter()
router.HandleFunc("/api/users/{userId}/tokens", p.getAllTokens).Methods("GET")
diff --git a/management/server/http/handlers/users/users_handler.go b/management/server/http/handlers/users/users_handler.go
index 7380dd97e..3869f21f0 100644
--- a/management/server/http/handlers/users/users_handler.go
+++ b/management/server/http/handlers/users/users_handler.go
@@ -9,39 +9,33 @@ import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/http/configs"
"github.com/netbirdio/netbird/management/server/http/util"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
)
// handler is a handler that returns users of the account
type handler struct {
- accountManager server.AccountManager
- claimsExtractor *jwtclaims.ClaimsExtractor
+ accountManager server.AccountManager
}
-func AddEndpoints(accountManager server.AccountManager, authCfg configs.AuthCfg, router *mux.Router) {
- userHandler := newHandler(accountManager, authCfg)
+func AddEndpoints(accountManager server.AccountManager, router *mux.Router) {
+ userHandler := newHandler(accountManager)
router.HandleFunc("/users", userHandler.getAllUsers).Methods("GET", "OPTIONS")
router.HandleFunc("/users/{userId}", userHandler.updateUser).Methods("PUT", "OPTIONS")
router.HandleFunc("/users/{userId}", userHandler.deleteUser).Methods("DELETE", "OPTIONS")
router.HandleFunc("/users", userHandler.createUser).Methods("POST", "OPTIONS")
router.HandleFunc("/users/{userId}/invite", userHandler.inviteUser).Methods("POST", "OPTIONS")
- addUsersTokensEndpoint(accountManager, authCfg, router)
+ addUsersTokensEndpoint(accountManager, router)
}
// newHandler creates a new UsersHandler HTTP handler
-func newHandler(accountManager server.AccountManager, authCfg configs.AuthCfg) *handler {
+func newHandler(accountManager server.AccountManager) *handler {
return &handler{
accountManager: accountManager,
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(authCfg.Audience),
- jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
- ),
}
}
@@ -52,13 +46,13 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
return
}
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
targetUserID := vars["userId"]
if len(targetUserID) == 0 {
@@ -103,7 +97,7 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
util.WriteError(r.Context(), err, w)
return
}
- util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, claims.UserId))
+ util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID))
}
// deleteUser is a DELETE request to delete a user
@@ -113,13 +107,13 @@ func (h *handler) deleteUser(w http.ResponseWriter, r *http.Request) {
return
}
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
targetUserID := vars["userId"]
if len(targetUserID) == 0 {
@@ -143,12 +137,12 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
return
}
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
req := &api.PostApiUsersJSONRequestBody{}
err = json.NewDecoder(r.Body).Decode(&req)
@@ -184,7 +178,7 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
util.WriteError(r.Context(), err, w)
return
}
- util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, claims.UserId))
+ util.WriteJSONObject(r.Context(), w, toUserResponse(newUser, userID))
}
// getAllUsers returns a list of users of the account this user belongs to.
@@ -195,13 +189,13 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
return
}
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
data, err := h.accountManager.GetUsersFromAccount(r.Context(), accountID, userID)
if err != nil {
util.WriteError(r.Context(), err, w)
@@ -216,7 +210,7 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
continue
}
if serviceUser == "" {
- users = append(users, toUserResponse(d, claims.UserId))
+ users = append(users, toUserResponse(d, userID))
continue
}
@@ -227,7 +221,7 @@ func (h *handler) getAllUsers(w http.ResponseWriter, r *http.Request) {
return
}
if includeServiceUser == d.IsServiceUser {
- users = append(users, toUserResponse(d, claims.UserId))
+ users = append(users, toUserResponse(d, userID))
}
}
@@ -242,12 +236,12 @@ func (h *handler) inviteUser(w http.ResponseWriter, r *http.Request) {
return
}
- claims := h.claimsExtractor.FromRequestContext(r)
- accountID, userID, err := h.accountManager.GetAccountIDFromToken(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
+ accountID, userID := userAuth.AccountId, userAuth.UserId
vars := mux.Vars(r)
targetUserID := vars["userId"]
diff --git a/management/server/http/handlers/users/users_handler_test.go b/management/server/http/handlers/users/users_handler_test.go
index 90081830a..a6a904a4c 100644
--- a/management/server/http/handlers/users/users_handler_test.go
+++ b/management/server/http/handlers/users/users_handler_test.go
@@ -13,8 +13,8 @@ import (
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/api"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
@@ -52,7 +52,7 @@ var usersTestAccount = &types.Account{
Issued: types.UserIssuedAPI,
},
nonDeletableServiceUserID: {
- Id: serviceUserID,
+ Id: nonDeletableServiceUserID,
Role: "admin",
IsServiceUser: true,
NonDeletable: true,
@@ -64,16 +64,13 @@ var usersTestAccount = &types.Account{
func initUsersTestData() *handler {
return &handler{
accountManager: &mock_server.MockAccountManager{
- GetAccountIDFromTokenFunc: func(_ context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- return usersTestAccount.Id, claims.UserId, nil
- },
GetUserByIDFunc: func(ctx context.Context, id string) (*types.User, error) {
return usersTestAccount.Users[id], nil
},
- GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) ([]*types.UserInfo, error) {
- users := make([]*types.UserInfo, 0)
+ GetUsersFromAccountFunc: func(_ context.Context, accountID, userID string) (map[string]*types.UserInfo, error) {
+ usersInfos := make(map[string]*types.UserInfo)
for _, v := range usersTestAccount.Users {
- users = append(users, &types.UserInfo{
+ usersInfos[v.Id] = &types.UserInfo{
ID: v.Id,
Role: string(v.Role),
Name: "",
@@ -81,9 +78,9 @@ func initUsersTestData() *handler {
IsServiceUser: v.IsServiceUser,
NonDeletable: v.NonDeletable,
Issued: v.Issued,
- })
+ }
}
- return users, nil
+ return usersInfos, nil
},
CreateUserFunc: func(_ context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error) {
if userID != existingUserID {
@@ -127,15 +124,6 @@ func initUsersTestData() *handler {
return nil
},
},
- claimsExtractor: jwtclaims.NewClaimsExtractor(
- jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
- return jwtclaims.AuthorizationClaims{
- UserId: existingUserID,
- Domain: testDomain,
- AccountId: existingAccountID,
- }
- }),
- ),
}
}
@@ -158,6 +146,11 @@ func TestGetUsers(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: existingUserID,
+ Domain: testDomain,
+ AccountId: existingAccountID,
+ })
userHandler.getAllUsers(recorder, req)
@@ -263,6 +256,11 @@ func TestUpdateUser(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: existingUserID,
+ Domain: testDomain,
+ AccountId: existingAccountID,
+ })
router := mux.NewRouter()
router.HandleFunc("/api/users/{userId}", userHandler.updateUser).Methods("PUT")
@@ -355,6 +353,11 @@ func TestCreateUser(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
rr := httptest.NewRecorder()
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: existingUserID,
+ Domain: testDomain,
+ AccountId: existingAccountID,
+ })
userHandler.createUser(rr, req)
@@ -399,6 +402,12 @@ func TestInviteUser(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
req = mux.SetURLVars(req, tc.requestVars)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: existingUserID,
+ Domain: testDomain,
+ AccountId: existingAccountID,
+ })
+
rr := httptest.NewRecorder()
userHandler.inviteUser(rr, req)
@@ -452,6 +461,12 @@ func TestDeleteUser(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
req = mux.SetURLVars(req, tc.requestVars)
+ req = nbcontext.SetUserAuthInRequest(req, nbcontext.UserAuth{
+ UserId: existingUserID,
+ Domain: testDomain,
+ AccountId: existingAccountID,
+ })
+
rr := httptest.NewRecorder()
userHandler.deleteUser(rr, req)
diff --git a/management/server/http/middleware/access_control.go b/management/server/http/middleware/access_control.go
index c5bdf5fe7..4ed90f47b 100644
--- a/management/server/http/middleware/access_control.go
+++ b/management/server/http/middleware/access_control.go
@@ -7,30 +7,24 @@ import (
log "github.com/sirupsen/logrus"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
"github.com/netbirdio/netbird/management/server/http/util"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/types"
-
- "github.com/netbirdio/netbird/management/server/jwtclaims"
)
// GetUser function defines a function to fetch user from Account by jwtclaims.AuthorizationClaims
-type GetUser func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error)
+type GetUser func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
// AccessControl middleware to restrict to make POST/PUT/DELETE requests by admin only
type AccessControl struct {
- claimsExtract jwtclaims.ClaimsExtractor
- getUser GetUser
+ getUser GetUser
}
// NewAccessControl instance constructor
-func NewAccessControl(audience, userIDClaim string, getUser GetUser) *AccessControl {
+func NewAccessControl(getUser GetUser) *AccessControl {
return &AccessControl{
- claimsExtract: *jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(audience),
- jwtclaims.WithUserIDClaim(userIDClaim),
- ),
getUser: getUser,
}
}
@@ -45,12 +39,16 @@ func (a *AccessControl) Handler(h http.Handler) http.Handler {
return
}
- claims := a.claimsExtract.FromRequestContext(r)
-
- user, err := a.getUser(r.Context(), claims)
+ userAuth, err := nbcontext.GetUserAuthFromRequest(r)
if err != nil {
- log.WithContext(r.Context()).Errorf("failed to get user from claims: %s", err)
- util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid JWT"), w)
+ log.WithContext(r.Context()).Errorf("failed to get user auth from request: %s", err)
+ util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid user auth"), w)
+ }
+
+ user, err := a.getUser(r.Context(), userAuth)
+ if err != nil {
+ log.WithContext(r.Context()).Errorf("failed to get user: %s", err)
+ util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "invalid user auth"), w)
return
}
diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go
index dcf73259a..a8e6790a9 100644
--- a/management/server/http/middleware/auth_middleware.go
+++ b/management/server/http/middleware/auth_middleware.go
@@ -8,67 +8,41 @@ import (
"strings"
"time"
- "github.com/golang-jwt/jwt"
log "github.com/sirupsen/logrus"
- nbContext "github.com/netbirdio/netbird/management/server/context"
+ "github.com/netbirdio/netbird/management/server/auth"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
"github.com/netbirdio/netbird/management/server/http/util"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/status"
- "github.com/netbirdio/netbird/management/server/types"
)
-// GetAccountInfoFromPATFunc function
-type GetAccountInfoFromPATFunc func(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
-
-// ValidateAndParseTokenFunc function
-type ValidateAndParseTokenFunc func(ctx context.Context, token string) (*jwt.Token, error)
-
-// MarkPATUsedFunc function
-type MarkPATUsedFunc func(ctx context.Context, token string) error
-
-// CheckUserAccessByJWTGroupsFunc function
-type CheckUserAccessByJWTGroupsFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) error
+type EnsureAccountFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
+type SyncUserJWTGroupsFunc func(ctx context.Context, userAuth nbcontext.UserAuth) error
// AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens
type AuthMiddleware struct {
- getAccountInfoFromPAT GetAccountInfoFromPATFunc
- validateAndParseToken ValidateAndParseTokenFunc
- markPATUsed MarkPATUsedFunc
- checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc
- claimsExtractor *jwtclaims.ClaimsExtractor
- audience string
- userIDClaim string
+ authManager auth.Manager
+ ensureAccount EnsureAccountFunc
+ syncUserJWTGroups SyncUserJWTGroupsFunc
}
-const (
- userProperty = "user"
-)
-
// NewAuthMiddleware instance constructor
-func NewAuthMiddleware(getAccountInfoFromPAT GetAccountInfoFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc,
- markPATUsed MarkPATUsedFunc, checkUserAccessByJWTGroups CheckUserAccessByJWTGroupsFunc, claimsExtractor *jwtclaims.ClaimsExtractor,
- audience string, userIdClaim string) *AuthMiddleware {
- if userIdClaim == "" {
- userIdClaim = jwtclaims.UserIDClaim
- }
-
+func NewAuthMiddleware(
+ authManager auth.Manager,
+ ensureAccount EnsureAccountFunc,
+ syncUserJWTGroups SyncUserJWTGroupsFunc,
+) *AuthMiddleware {
return &AuthMiddleware{
- getAccountInfoFromPAT: getAccountInfoFromPAT,
- validateAndParseToken: validateAndParseToken,
- markPATUsed: markPATUsed,
- checkUserAccessByJWTGroups: checkUserAccessByJWTGroups,
- claimsExtractor: claimsExtractor,
- audience: audience,
- userIDClaim: userIdClaim,
+ authManager: authManager,
+ ensureAccount: ensureAccount,
+ syncUserJWTGroups: syncUserJWTGroups,
}
}
// Handler method of the middleware which authenticates a user either by JWT claims or by PAT
func (m *AuthMiddleware) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
if bypass.ShouldBypass(r.URL.Path, h, w, r) {
return
}
@@ -84,108 +58,111 @@ func (m *AuthMiddleware) Handler(h http.Handler) http.Handler {
switch authType {
case "bearer":
- err := m.checkJWTFromRequest(w, r, auth)
+ request, err := m.checkJWTFromRequest(r, auth)
if err != nil {
- log.WithContext(r.Context()).Errorf("Error when validating JWT claims: %s", err.Error())
+ log.WithContext(r.Context()).Errorf("Error when validating JWT: %s", err.Error())
util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w)
return
}
+
+ h.ServeHTTP(w, request)
case "token":
- err := m.checkPATFromRequest(w, r, auth)
+ request, err := m.checkPATFromRequest(r, auth)
if err != nil {
- log.WithContext(r.Context()).Debugf("Error when validating PAT claims: %s", err.Error())
+ log.WithContext(r.Context()).Debugf("Error when validating PAT: %s", err.Error())
util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w)
return
}
+ h.ServeHTTP(w, request)
default:
util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "no valid authentication provided"), w)
return
}
- claims := m.claimsExtractor.FromRequestContext(r)
- //nolint
- ctx := context.WithValue(r.Context(), nbContext.UserIDKey, claims.UserId)
- //nolint
- ctx = context.WithValue(ctx, nbContext.AccountIDKey, claims.AccountId)
- h.ServeHTTP(w, r.WithContext(ctx))
})
}
// CheckJWTFromRequest checks if the JWT is valid
-func (m *AuthMiddleware) checkJWTFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error {
+func (m *AuthMiddleware) checkJWTFromRequest(r *http.Request, auth []string) (*http.Request, error) {
token, err := getTokenFromJWTRequest(auth)
// If an error occurs, call the error handler and return an error
if err != nil {
- return fmt.Errorf("Error extracting token: %w", err)
+ return r, fmt.Errorf("error extracting token: %w", err)
}
- validatedToken, err := m.validateAndParseToken(r.Context(), token)
+ ctx := r.Context()
+
+ userAuth, validatedToken, err := m.authManager.ValidateAndParseToken(ctx, token)
if err != nil {
- return err
+ return r, err
}
- if validatedToken == nil {
- return nil
+ if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 {
+ userAuth.AccountId = impersonate[0]
+ userAuth.IsChild = ok
}
- if err := m.verifyUserAccess(r.Context(), validatedToken); err != nil {
- return err
+ // we need to call this method because if user is new, we will automatically add it to existing or create a new account
+ accountId, _, err := m.ensureAccount(ctx, userAuth)
+ if err != nil {
+ return r, err
}
- // If we get here, everything worked and we can set the
- // user property in context.
- newRequest := r.WithContext(context.WithValue(r.Context(), userProperty, validatedToken)) //nolint
- // Update the current request with the new context information.
- *r = *newRequest
- return nil
-}
+ if userAuth.AccountId != accountId {
+ log.WithContext(ctx).Debugf("Auth middleware sets accountId from ensure, before %s, now %s", userAuth.AccountId, accountId)
+ userAuth.AccountId = accountId
+ }
-// verifyUserAccess checks if a user, based on a validated JWT token,
-// is allowed access, particularly in cases where the admin enabled JWT
-// group propagation and designated certain groups with access permissions.
-func (m *AuthMiddleware) verifyUserAccess(ctx context.Context, validatedToken *jwt.Token) error {
- authClaims := m.claimsExtractor.FromToken(validatedToken)
- return m.checkUserAccessByJWTGroups(ctx, authClaims)
+ userAuth, err = m.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, validatedToken)
+ if err != nil {
+ return r, err
+ }
+
+ err = m.syncUserJWTGroups(ctx, userAuth)
+ if err != nil {
+ log.WithContext(ctx).Errorf("HTTP server failed to sync user JWT groups: %s", err)
+ }
+
+ return nbcontext.SetUserAuthInRequest(r, userAuth), nil
}
// CheckPATFromRequest checks if the PAT is valid
-func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error {
+func (m *AuthMiddleware) checkPATFromRequest(r *http.Request, auth []string) (*http.Request, error) {
token, err := getTokenFromPATRequest(auth)
if err != nil {
- return fmt.Errorf("error extracting token: %w", err)
+ return r, fmt.Errorf("error extracting token: %w", err)
}
- user, pat, accDomain, accCategory, err := m.getAccountInfoFromPAT(r.Context(), token)
+ ctx := r.Context()
+ user, pat, accDomain, accCategory, err := m.authManager.GetPATInfo(ctx, token)
if err != nil {
- return fmt.Errorf("invalid Token: %w", err)
+ return r, fmt.Errorf("invalid Token: %w", err)
}
if time.Now().After(pat.GetExpirationDate()) {
- return fmt.Errorf("token expired")
+ return r, fmt.Errorf("token expired")
}
- err = m.markPATUsed(r.Context(), pat.ID)
+ err = m.authManager.MarkPATUsed(ctx, pat.ID)
if err != nil {
- return err
+ return r, err
}
- claimMaps := jwt.MapClaims{}
- claimMaps[m.userIDClaim] = user.Id
- claimMaps[m.audience+jwtclaims.AccountIDSuffix] = user.AccountID
- claimMaps[m.audience+jwtclaims.DomainIDSuffix] = accDomain
- claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = accCategory
- claimMaps[jwtclaims.IsToken] = true
- jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
- newRequest := r.WithContext(context.WithValue(r.Context(), jwtclaims.TokenUserProperty, jwtToken)) //nolint
- // Update the current request with the new context information.
- *r = *newRequest
- return nil
+ userAuth := nbcontext.UserAuth{
+ UserId: user.Id,
+ AccountId: user.AccountID,
+ Domain: accDomain,
+ DomainCategory: accCategory,
+ IsPAT: true,
+ }
+
+ return nbcontext.SetUserAuthInRequest(r, userAuth), nil
}
// getTokenFromJWTRequest is a "TokenExtractor" that takes auth header parts and extracts
// the JWT token from the Authorization header.
func getTokenFromJWTRequest(authHeaderParts []string) (string, error) {
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
- return "", errors.New("Authorization header format must be Bearer {token}")
+ return "", errors.New("authorization header format must be Bearer {token}")
}
return authHeaderParts[1], nil
@@ -195,7 +172,7 @@ func getTokenFromJWTRequest(authHeaderParts []string) (string, error) {
// the PAT token from the Authorization header.
func getTokenFromPATRequest(authHeaderParts []string) (string, error) {
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "token" {
- return "", errors.New("Authorization header format must be Token {token}")
+ return "", errors.New("authorization header format must be Token {token}")
}
return authHeaderParts[1], nil
diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go
index c1686ed44..3dc7d51cb 100644
--- a/management/server/http/middleware/auth_middleware_test.go
+++ b/management/server/http/middleware/auth_middleware_test.go
@@ -9,10 +9,14 @@ import (
"time"
"github.com/golang-jwt/jwt"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/netbirdio/netbird/management/server/auth"
+ nbjwt "github.com/netbirdio/netbird/management/server/auth/jwt"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/util"
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/types"
)
@@ -58,17 +62,23 @@ func mockGetAccountInfoFromPAT(_ context.Context, token string) (user *types.Use
return nil, nil, "", "", fmt.Errorf("PAT invalid")
}
-func mockValidateAndParseToken(_ context.Context, token string) (*jwt.Token, error) {
+func mockValidateAndParseToken(_ context.Context, token string) (nbcontext.UserAuth, *jwt.Token, error) {
if token == JWT {
- return &jwt.Token{
- Claims: jwt.MapClaims{
- userIDClaim: userID,
- audience + jwtclaims.AccountIDSuffix: accountID,
+ return nbcontext.UserAuth{
+ UserId: userID,
+ AccountId: accountID,
+ Domain: testAccount.Domain,
+ DomainCategory: testAccount.DomainCategory,
},
- Valid: true,
- }, nil
+ &jwt.Token{
+ Claims: jwt.MapClaims{
+ userIDClaim: userID,
+ audience + nbjwt.AccountIDSuffix: accountID,
+ },
+ Valid: true,
+ }, nil
}
- return nil, fmt.Errorf("JWT invalid")
+ return nbcontext.UserAuth{}, nil, fmt.Errorf("JWT invalid")
}
func mockMarkPATUsed(_ context.Context, token string) error {
@@ -78,16 +88,20 @@ func mockMarkPATUsed(_ context.Context, token string) error {
return fmt.Errorf("Should never get reached")
}
-func mockCheckUserAccessByJWTGroups(_ context.Context, claims jwtclaims.AuthorizationClaims) error {
- if testAccount.Id != claims.AccountId {
- return fmt.Errorf("account with id %s does not exist", claims.AccountId)
+func mockEnsureUserAccessByJWTGroups(_ context.Context, userAuth nbcontext.UserAuth, token *jwt.Token) (nbcontext.UserAuth, error) {
+ if userAuth.IsChild || userAuth.IsPAT {
+ return userAuth, nil
}
- if _, ok := testAccount.Users[claims.UserId]; !ok {
- return fmt.Errorf("user with id %s does not exist", claims.UserId)
+ if testAccount.Id != userAuth.AccountId {
+ return userAuth, fmt.Errorf("account with id %s does not exist", userAuth.AccountId)
}
- return nil
+ if _, ok := testAccount.Users[userAuth.UserId]; !ok {
+ return userAuth, fmt.Errorf("user with id %s does not exist", userAuth.UserId)
+ }
+
+ return userAuth, nil
}
func TestAuthMiddleware_Handler(t *testing.T) {
@@ -158,22 +172,24 @@ func TestAuthMiddleware_Handler(t *testing.T) {
}
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // do nothing
+
})
- claimsExtractor := jwtclaims.NewClaimsExtractor(
- jwtclaims.WithAudience(audience),
- jwtclaims.WithUserIDClaim(userIDClaim),
- )
+ mockAuth := &auth.MockManager{
+ ValidateAndParseTokenFunc: mockValidateAndParseToken,
+ EnsureUserAccessByJWTGroupsFunc: mockEnsureUserAccessByJWTGroups,
+ MarkPATUsedFunc: mockMarkPATUsed,
+ GetPATInfoFunc: mockGetAccountInfoFromPAT,
+ }
authMiddleware := NewAuthMiddleware(
- mockGetAccountInfoFromPAT,
- mockValidateAndParseToken,
- mockMarkPATUsed,
- mockCheckUserAccessByJWTGroups,
- claimsExtractor,
- audience,
- userIDClaim,
+ mockAuth,
+ func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
+ return userAuth.AccountId, userAuth.UserId, nil
+ },
+ func(ctx context.Context, userAuth nbcontext.UserAuth) error {
+ return nil
+ },
)
handlerToTest := authMiddleware.Handler(nextHandler)
@@ -195,9 +211,115 @@ func TestAuthMiddleware_Handler(t *testing.T) {
result := rec.Result()
defer result.Body.Close()
+
if result.StatusCode != tc.expectedStatusCode {
t.Errorf("expected status code %d, got %d", tc.expectedStatusCode, result.StatusCode)
}
})
}
}
+
+func TestAuthMiddleware_Handler_Child(t *testing.T) {
+ tt := []struct {
+ name string
+ path string
+ authHeader string
+ expectedUserAuth *nbcontext.UserAuth // nil expects 401 response status
+ }{
+ {
+ name: "Valid PAT Token",
+ path: "/test",
+ authHeader: "Token " + PAT,
+ expectedUserAuth: &nbcontext.UserAuth{
+ AccountId: accountID,
+ UserId: userID,
+ Domain: testAccount.Domain,
+ DomainCategory: testAccount.DomainCategory,
+ IsPAT: true,
+ },
+ },
+ {
+ name: "Valid PAT Token ignores child",
+ path: "/test?account=xyz",
+ authHeader: "Token " + PAT,
+ expectedUserAuth: &nbcontext.UserAuth{
+ AccountId: accountID,
+ UserId: userID,
+ Domain: testAccount.Domain,
+ DomainCategory: testAccount.DomainCategory,
+ IsPAT: true,
+ },
+ },
+ {
+ name: "Valid JWT Token",
+ path: "/test",
+ authHeader: "Bearer " + JWT,
+ expectedUserAuth: &nbcontext.UserAuth{
+ AccountId: accountID,
+ UserId: userID,
+ Domain: testAccount.Domain,
+ DomainCategory: testAccount.DomainCategory,
+ },
+ },
+
+ {
+ name: "Valid JWT Token with child",
+ path: "/test?account=xyz",
+ authHeader: "Bearer " + JWT,
+ expectedUserAuth: &nbcontext.UserAuth{
+ AccountId: "xyz",
+ UserId: userID,
+ Domain: testAccount.Domain,
+ DomainCategory: testAccount.DomainCategory,
+ IsChild: true,
+ },
+ },
+ }
+
+ mockAuth := &auth.MockManager{
+ ValidateAndParseTokenFunc: mockValidateAndParseToken,
+ EnsureUserAccessByJWTGroupsFunc: mockEnsureUserAccessByJWTGroups,
+ MarkPATUsedFunc: mockMarkPATUsed,
+ GetPATInfoFunc: mockGetAccountInfoFromPAT,
+ }
+
+ authMiddleware := NewAuthMiddleware(
+ mockAuth,
+ func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
+ return userAuth.AccountId, userAuth.UserId, nil
+ },
+ func(ctx context.Context, userAuth nbcontext.UserAuth) error {
+ return nil
+ },
+ )
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ handlerToTest := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ userAuth, err := nbcontext.GetUserAuthFromRequest(r)
+ if tc.expectedUserAuth != nil {
+ assert.NoError(t, err)
+ assert.Equal(t, *tc.expectedUserAuth, userAuth)
+ } else {
+ assert.Error(t, err)
+ assert.Empty(t, userAuth)
+ }
+ }))
+
+ req := httptest.NewRequest("GET", "http://testing"+tc.path, nil)
+ req.Header.Set("Authorization", tc.authHeader)
+ rec := httptest.NewRecorder()
+
+ handlerToTest.ServeHTTP(rec, req)
+
+ result := rec.Result()
+ defer result.Body.Close()
+
+ if tc.expectedUserAuth != nil {
+ assert.Equal(t, 200, result.StatusCode)
+ } else {
+ assert.Equal(t, 401, result.StatusCode)
+ }
+ })
+ }
+}
diff --git a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
index 2eb50e4b4..e2c2c1d85 100644
--- a/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
+++ b/management/server/http/testing/benchmarks/peers_handler_benchmark_test.go
@@ -77,13 +77,13 @@ func BenchmarkUpdatePeer(b *testing.B) {
func BenchmarkGetOnePeer(b *testing.B) {
var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Peers - XS": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 70},
- "Peers - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 30},
- "Peers - M": {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 15, MaxMsPerOpCICD: 50},
- "Peers - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130},
- "Groups - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 30, MaxMsPerOpCICD: 200},
- "Users - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130},
- "Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 130},
+ "Peers - XS": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 40, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
+ "Peers - S": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
+ "Peers - M": {MinMsPerOpLocal: 9, MaxMsPerOpLocal: 18, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 70},
+ "Peers - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
+ "Groups - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 130, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
+ "Users - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
+ "Setup Keys - L": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 90, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 200},
"Peers - XL": {MinMsPerOpLocal: 200, MaxMsPerOpLocal: 400, MinMsPerOpCICD: 200, MaxMsPerOpCICD: 750},
}
@@ -111,9 +111,9 @@ func BenchmarkGetOnePeer(b *testing.B) {
func BenchmarkGetAllPeers(b *testing.B) {
var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Peers - XS": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 150},
- "Peers - S": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 30},
- "Peers - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 70},
+ "Peers - XS": {MinMsPerOpLocal: 40, MaxMsPerOpLocal: 70, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
+ "Peers - S": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
+ "Peers - M": {MinMsPerOpLocal: 20, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 100},
"Peers - L": {MinMsPerOpLocal: 110, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300},
"Groups - L": {MinMsPerOpLocal: 150, MaxMsPerOpLocal: 200, MinMsPerOpCICD: 130, MaxMsPerOpCICD: 500},
"Users - L": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 170, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 400},
@@ -145,14 +145,14 @@ func BenchmarkGetAllPeers(b *testing.B) {
func BenchmarkDeletePeer(b *testing.B) {
var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
- "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
- "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
- "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
- "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
- "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
- "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
- "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
+ "Peers - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
+ "Peers - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
+ "Peers - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
+ "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
+ "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
+ "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
+ "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
+ "Peers - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 4, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 18},
}
log.SetOutput(io.Discard)
diff --git a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
index e9bc63f9b..b7deab334 100644
--- a/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
+++ b/management/server/http/testing/benchmarks/users_handler_benchmark_test.go
@@ -37,24 +37,23 @@ func BenchmarkUpdateUser(b *testing.B) {
var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
"Users - XS": {MinMsPerOpLocal: 100, MaxMsPerOpLocal: 160, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 310},
"Users - S": {MinMsPerOpLocal: 0.3, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 15},
- "Users - M": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 3, MaxMsPerOpCICD: 15},
- "Users - L": {MinMsPerOpLocal: 0.3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 20},
+ "Users - M": {MinMsPerOpLocal: 1, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 3, MaxMsPerOpCICD: 20},
+ "Users - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50},
"Peers - L": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 150, MinMsPerOpCICD: 80, MaxMsPerOpCICD: 310},
- "Groups - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 30, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 60},
- "Setup Keys - L": {MinMsPerOpLocal: 0.3, MaxMsPerOpLocal: 3, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 15},
- "Users - XL": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 30},
+ "Groups - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 50, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 120},
+ "Setup Keys - L": {MinMsPerOpLocal: 5, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 2, MaxMsPerOpCICD: 50},
+ "Users - XL": {MinMsPerOpLocal: 30, MaxMsPerOpLocal: 100, MinMsPerOpCICD: 60, MaxMsPerOpCICD: 280},
}
log.SetOutput(io.Discard)
defer log.SetOutput(os.Stderr)
- recorder := httptest.NewRecorder()
-
for name, bc := range benchCasesUsers {
b.Run(name, func(b *testing.B) {
apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys)
+ recorder := httptest.NewRecorder()
b.ResetTimer()
start := time.Now()
for i := 0; i < b.N; i++ {
@@ -97,13 +96,12 @@ func BenchmarkGetOneUser(b *testing.B) {
log.SetOutput(io.Discard)
defer log.SetOutput(os.Stderr)
- recorder := httptest.NewRecorder()
-
for name, bc := range benchCasesUsers {
b.Run(name, func(b *testing.B) {
apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys)
+ recorder := httptest.NewRecorder()
b.ResetTimer()
start := time.Now()
for i := 0; i < b.N; i++ {
@@ -118,26 +116,25 @@ func BenchmarkGetOneUser(b *testing.B) {
func BenchmarkGetAllUsers(b *testing.B) {
var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10},
- "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 10},
- "Users - M": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 15},
- "Users - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 50},
- "Peers - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 20, MaxMsPerOpCICD: 55},
- "Groups - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55},
- "Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 25, MaxMsPerOpCICD: 55},
- "Users - XL": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 100, MaxMsPerOpCICD: 300},
+ "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
+ "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 2, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
+ "Users - M": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 0, MaxMsPerOpCICD: 75},
+ "Users - L": {MinMsPerOpLocal: 10, MaxMsPerOpLocal: 20, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
+ "Peers - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
+ "Groups - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
+ "Setup Keys - L": {MinMsPerOpLocal: 15, MaxMsPerOpLocal: 25, MinMsPerOpCICD: 10, MaxMsPerOpCICD: 100},
+ "Users - XL": {MinMsPerOpLocal: 80, MaxMsPerOpLocal: 120, MinMsPerOpCICD: 50, MaxMsPerOpCICD: 300},
}
log.SetOutput(io.Discard)
defer log.SetOutput(os.Stderr)
- recorder := httptest.NewRecorder()
-
for name, bc := range benchCasesUsers {
b.Run(name, func(b *testing.B) {
apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, bc.Users, bc.SetupKeys)
+ recorder := httptest.NewRecorder()
b.ResetTimer()
start := time.Now()
for i := 0; i < b.N; i++ {
@@ -152,26 +149,25 @@ func BenchmarkGetAllUsers(b *testing.B) {
func BenchmarkDeleteUsers(b *testing.B) {
var expectedMetrics = map[string]testing_tools.PerformanceMetrics{
- "Users - XS": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 25},
- "Users - S": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20},
- "Users - M": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20},
- "Users - L": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20},
- "Peers - L": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 25},
- "Groups - L": {MinMsPerOpLocal: 2, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20},
- "Setup Keys - L": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20},
- "Users - XL": {MinMsPerOpLocal: 3, MaxMsPerOpLocal: 10, MinMsPerOpCICD: 5, MaxMsPerOpCICD: 20},
+ "Users - XS": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
+ "Users - S": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
+ "Users - M": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
+ "Users - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
+ "Peers - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
+ "Groups - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
+ "Setup Keys - L": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
+ "Users - XL": {MinMsPerOpLocal: 0, MaxMsPerOpLocal: 5, MinMsPerOpCICD: 1, MaxMsPerOpCICD: 50},
}
log.SetOutput(io.Discard)
defer log.SetOutput(os.Stderr)
- recorder := httptest.NewRecorder()
-
for name, bc := range benchCasesUsers {
b.Run(name, func(b *testing.B) {
apiHandler, am, _ := testing_tools.BuildApiBlackBoxWithDBState(b, "../testdata/users.sql", nil, false)
testing_tools.PopulateTestData(b, am.(*server.DefaultAccountManager), bc.Peers, bc.Groups, 1000, bc.SetupKeys)
+ recorder := httptest.NewRecorder()
b.ResetTimer()
start := time.Now()
for i := 0; i < b.N; i++ {
diff --git a/management/server/http/testing/testing_tools/tools.go b/management/server/http/testing/testing_tools/tools.go
index 006d5679c..e534dac46 100644
--- a/management/server/http/testing/testing_tools/tools.go
+++ b/management/server/http/testing/testing_tools/tools.go
@@ -3,6 +3,7 @@ package testing_tools
import (
"bytes"
"context"
+ "errors"
"fmt"
"io"
"net"
@@ -13,17 +14,17 @@ import (
"testing"
"time"
- "github.com/netbirdio/netbird/management/server/util"
+ "github.com/golang-jwt/jwt"
"github.com/stretchr/testify/assert"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity"
+ "github.com/netbirdio/netbird/management/server/auth"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/groups"
nbhttp "github.com/netbirdio/netbird/management/server/http"
- "github.com/netbirdio/netbird/management/server/http/configs"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/management/server/networks"
"github.com/netbirdio/netbird/management/server/networks/resources"
"github.com/netbirdio/netbird/management/server/networks/routers"
@@ -32,6 +33,7 @@ import (
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/netbirdio/netbird/management/server/types"
+ "github.com/netbirdio/netbird/management/server/util"
)
const (
@@ -115,11 +117,20 @@ func BuildApiBlackBoxWithDBState(t TB, sqlFile string, expectedPeerUpdate *serve
t.Fatalf("Failed to create manager: %v", err)
}
+ // @note this is required so that PAT's validate from store, but JWT's are mocked
+ authManager := auth.NewManager(store, "", "", "", "", []string{}, false)
+ authManagerMock := &auth.MockManager{
+ ValidateAndParseTokenFunc: mockValidateAndParseToken,
+ EnsureUserAccessByJWTGroupsFunc: authManager.EnsureUserAccessByJWTGroups,
+ MarkPATUsedFunc: authManager.MarkPATUsed,
+ GetPATInfoFunc: authManager.GetPATInfo,
+ }
+
networksManagerMock := networks.NewManagerMock()
resourcesManagerMock := resources.NewManagerMock()
routersManagerMock := routers.NewManagerMock()
groupsManagerMock := groups.NewManagerMock()
- apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, &jwtclaims.JwtValidatorMock{}, metrics, configs.AuthCfg{}, validatorMock)
+ apiHandler, err := nbhttp.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, &server.Config{}, validatorMock)
if err != nil {
t.Fatalf("Failed to create API handler: %v", err)
}
@@ -309,3 +320,25 @@ func EvaluateBenchmarkResults(b *testing.B, name string, duration time.Duration,
b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", name, msPerOp, maxExpected)
}
}
+
+func mockValidateAndParseToken(_ context.Context, token string) (nbcontext.UserAuth, *jwt.Token, error) {
+ userAuth := nbcontext.UserAuth{}
+
+ switch token {
+ case "testUserId", "testAdminId", "testOwnerId", "testServiceUserId", "testServiceAdminId", "blockedUserId":
+ userAuth.UserId = token
+ userAuth.AccountId = "testAccountId"
+ userAuth.Domain = "test.com"
+ userAuth.DomainCategory = "private"
+ case "otherUserId":
+ userAuth.UserId = "otherUserId"
+ userAuth.AccountId = "otherAccountId"
+ userAuth.Domain = "other.com"
+ userAuth.DomainCategory = "private"
+ case "invalidToken":
+ return userAuth, nil, errors.New("invalid token")
+ }
+
+ jwtToken := jwt.New(jwt.SigningMethodHS256)
+ return userAuth, jwtToken, nil
+}
diff --git a/management/server/idp/idp.go b/management/server/idp/idp.go
index 419220942..0f1ff0f1f 100644
--- a/management/server/idp/idp.go
+++ b/management/server/idp/idp.go
@@ -149,6 +149,7 @@ func NewManager(ctx context.Context, config Config, appMetrics telemetry.AppMetr
GrantType: config.ClientConfig.GrantType,
TokenEndpoint: config.ClientConfig.TokenEndpoint,
ManagementEndpoint: config.ExtraConfig["ManagementEndpoint"],
+ PAT: config.ExtraConfig["PAT"],
}
}
diff --git a/management/server/idp/zitadel.go b/management/server/idp/zitadel.go
index 9d7626844..343357927 100644
--- a/management/server/idp/zitadel.go
+++ b/management/server/idp/zitadel.go
@@ -34,6 +34,7 @@ type ZitadelClientConfig struct {
GrantType string
TokenEndpoint string
ManagementEndpoint string
+ PAT string
}
// ZitadelCredentials zitadel authentication information.
@@ -135,6 +136,28 @@ func readZitadelError(body io.ReadCloser) error {
return errors.New(strings.Join(errsOut, " "))
}
+// verifyJWTConfig ensures necessary values are set in the ZitadelClientConfig for JWTs to be generated.
+func verifyJWTConfig(config ZitadelClientConfig) error {
+
+ if config.ClientID == "" {
+ return fmt.Errorf("zitadel IdP configuration is incomplete, clientID is missing")
+ }
+
+ if config.ClientSecret == "" {
+ return fmt.Errorf("zitadel IdP configuration is incomplete, ClientSecret is missing")
+ }
+
+ if config.TokenEndpoint == "" {
+ return fmt.Errorf("zitadel IdP configuration is incomplete, TokenEndpoint is missing")
+ }
+
+ if config.GrantType == "" {
+ return fmt.Errorf("zitadel IdP configuration is incomplete, GrantType is missing")
+ }
+
+ return nil
+}
+
// NewZitadelManager creates a new instance of the ZitadelManager.
func NewZitadelManager(config ZitadelClientConfig, appMetrics telemetry.AppMetrics) (*ZitadelManager, error) {
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
@@ -146,26 +169,18 @@ func NewZitadelManager(config ZitadelClientConfig, appMetrics telemetry.AppMetri
}
helper := JsonParser{}
- if config.ClientID == "" {
- return nil, fmt.Errorf("zitadel IdP configuration is incomplete, clientID is missing")
- }
-
- if config.ClientSecret == "" {
- return nil, fmt.Errorf("zitadel IdP configuration is incomplete, ClientSecret is missing")
- }
-
- if config.TokenEndpoint == "" {
- return nil, fmt.Errorf("zitadel IdP configuration is incomplete, TokenEndpoint is missing")
+ hasPAT := config.PAT != ""
+ if !hasPAT {
+ jwtErr := verifyJWTConfig(config)
+ if jwtErr != nil {
+ return nil, jwtErr
+ }
}
if config.ManagementEndpoint == "" {
return nil, fmt.Errorf("zitadel IdP configuration is incomplete, ManagementEndpoint is missing")
}
- if config.GrantType == "" {
- return nil, fmt.Errorf("zitadel IdP configuration is incomplete, GrantType is missing")
- }
-
credentials := &ZitadelCredentials{
clientConfig: config,
httpClient: httpClient,
@@ -254,6 +269,20 @@ func (zc *ZitadelCredentials) parseRequestJWTResponse(rawBody io.ReadCloser) (JW
return jwtToken, nil
}
+// generatePATToken creates a functional JWTToken instance which will pass the
+// PAT to the API directly and skip requesting a token.
+func (zc *ZitadelCredentials) generatePATToken() (JWTToken, error) {
+ tok := JWTToken{
+ AccessToken: zc.clientConfig.PAT,
+ Scope: "openid",
+ ExpiresIn: 9999,
+ TokenType: "PAT",
+ }
+ tok.expiresInTime = time.Now().Add(time.Duration(tok.ExpiresIn) * time.Second)
+ zc.jwtToken = tok
+ return tok, nil
+}
+
// Authenticate retrieves access token to use the Zitadel Management API.
func (zc *ZitadelCredentials) Authenticate(ctx context.Context) (JWTToken, error) {
zc.mux.Lock()
@@ -269,6 +298,10 @@ func (zc *ZitadelCredentials) Authenticate(ctx context.Context) (JWTToken, error
return zc.jwtToken, nil
}
+ if zc.clientConfig.PAT != "" {
+ return zc.generatePATToken()
+ }
+
resp, err := zc.requestJWTToken(ctx)
if err != nil {
return zc.jwtToken, err
diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go
index dcb9400f6..b9827f457 100644
--- a/management/server/integrated_validator.go
+++ b/management/server/integrated_validator.go
@@ -89,17 +89,17 @@ func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountI
}
peers, err = transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID)
- if err != nil {
- return err
- }
-
- settings, err = transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
return err
})
if err != nil {
return nil, err
}
+ settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+ if err != nil {
+ return nil, err
+ }
+
return am.integratedPeerValidator.GetValidatedPeers(accountID, groups, peers, settings.Extra)
}
diff --git a/management/server/jwtclaims/claims.go b/management/server/jwtclaims/claims.go
deleted file mode 100644
index 2527acbe3..000000000
--- a/management/server/jwtclaims/claims.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package jwtclaims
-
-import (
- "time"
-
- "github.com/golang-jwt/jwt"
-)
-
-// AuthorizationClaims stores authorization information from JWTs
-type AuthorizationClaims struct {
- UserId string
- AccountId string
- Domain string
- DomainCategory string
- LastLogin time.Time
- Invited bool
-
- Raw jwt.MapClaims
-}
diff --git a/management/server/jwtclaims/extractor_test.go b/management/server/jwtclaims/extractor_test.go
deleted file mode 100644
index eccd7c9e7..000000000
--- a/management/server/jwtclaims/extractor_test.go
+++ /dev/null
@@ -1,227 +0,0 @@
-package jwtclaims
-
-import (
- "context"
- "net/http"
- "testing"
- "time"
-
- "github.com/golang-jwt/jwt"
- "github.com/stretchr/testify/require"
-)
-
-func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audience string) *http.Request {
- t.Helper()
- const layout = "2006-01-02T15:04:05.999Z"
-
- claimMaps := jwt.MapClaims{}
- if claims.UserId != "" {
- claimMaps[UserIDClaim] = claims.UserId
- }
- if claims.AccountId != "" {
- claimMaps[audience+AccountIDSuffix] = claims.AccountId
- }
- if claims.Domain != "" {
- claimMaps[audience+DomainIDSuffix] = claims.Domain
- }
- if claims.DomainCategory != "" {
- claimMaps[audience+DomainCategorySuffix] = claims.DomainCategory
- }
- if claims.LastLogin != (time.Time{}) {
- claimMaps[audience+LastLoginSuffix] = claims.LastLogin.Format(layout)
- }
-
- if claims.Invited {
- claimMaps[audience+Invited] = true
- }
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
- r, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
- require.NoError(t, err, "creating testing request failed")
- testRequest := r.WithContext(context.WithValue(r.Context(), TokenUserProperty, token)) // nolint
-
- return testRequest
-}
-
-func TestExtractClaimsFromRequestContext(t *testing.T) {
- type test struct {
- name string
- inputAuthorizationClaims AuthorizationClaims
- inputAudiance string
- testingFunc require.ComparisonAssertionFunc
- expectedMSG string
- }
-
- const layout = "2006-01-02T15:04:05.999Z"
- lastLogin, _ := time.Parse(layout, "2023-08-17T09:30:40.465Z")
-
- testCase1 := test{
- name: "All Claim Fields",
- inputAudiance: "https://login/",
- inputAuthorizationClaims: AuthorizationClaims{
- UserId: "test",
- Domain: "test.com",
- AccountId: "testAcc",
- LastLogin: lastLogin,
- DomainCategory: "public",
- Invited: true,
- Raw: jwt.MapClaims{
- "https://login/wt_account_domain": "test.com",
- "https://login/wt_account_domain_category": "public",
- "https://login/wt_account_id": "testAcc",
- "https://login/nb_last_login": lastLogin.Format(layout),
- "sub": "test",
- "https://login/" + Invited: true,
- },
- },
- testingFunc: require.EqualValues,
- expectedMSG: "extracted claims should match input claims",
- }
-
- testCase2 := test{
- name: "Domain Is Empty",
- inputAudiance: "https://login/",
- inputAuthorizationClaims: AuthorizationClaims{
- UserId: "test",
- AccountId: "testAcc",
- Raw: jwt.MapClaims{
- "https://login/wt_account_id": "testAcc",
- "sub": "test",
- },
- },
- testingFunc: require.EqualValues,
- expectedMSG: "extracted claims should match input claims",
- }
-
- testCase3 := test{
- name: "Account ID Is Empty",
- inputAudiance: "https://login/",
- inputAuthorizationClaims: AuthorizationClaims{
- UserId: "test",
- Domain: "test.com",
- Raw: jwt.MapClaims{
- "https://login/wt_account_domain": "test.com",
- "sub": "test",
- },
- },
- testingFunc: require.EqualValues,
- expectedMSG: "extracted claims should match input claims",
- }
-
- testCase4 := test{
- name: "Category Is Empty",
- inputAudiance: "https://login/",
- inputAuthorizationClaims: AuthorizationClaims{
- UserId: "test",
- Domain: "test.com",
- AccountId: "testAcc",
- Raw: jwt.MapClaims{
- "https://login/wt_account_domain": "test.com",
- "https://login/wt_account_id": "testAcc",
- "sub": "test",
- },
- },
- testingFunc: require.EqualValues,
- expectedMSG: "extracted claims should match input claims",
- }
-
- testCase5 := test{
- name: "Only User ID Is set",
- inputAudiance: "https://login/",
- inputAuthorizationClaims: AuthorizationClaims{
- UserId: "test",
- Raw: jwt.MapClaims{
- "sub": "test",
- },
- },
- testingFunc: require.EqualValues,
- expectedMSG: "extracted claims should match input claims",
- }
-
- for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5} {
- t.Run(testCase.name, func(t *testing.T) {
- request := newTestRequestWithJWT(t, testCase.inputAuthorizationClaims, testCase.inputAudiance)
-
- extractor := NewClaimsExtractor(WithAudience(testCase.inputAudiance))
- extractedClaims := extractor.FromRequestContext(request)
-
- testCase.testingFunc(t, testCase.inputAuthorizationClaims, extractedClaims, testCase.expectedMSG)
- })
- }
-}
-
-func TestExtractClaimsSetOptions(t *testing.T) {
- t.Helper()
- type test struct {
- name string
- extractor *ClaimsExtractor
- check func(t *testing.T, c test)
- }
-
- testCase1 := test{
- name: "No custom options",
- extractor: NewClaimsExtractor(),
- check: func(t *testing.T, c test) {
- t.Helper()
- if c.extractor.authAudience != "" {
- t.Error("audience should be empty")
- return
- }
- if c.extractor.userIDClaim != UserIDClaim {
- t.Errorf("user id claim should be default, expected %s, got %s", UserIDClaim, c.extractor.userIDClaim)
- return
- }
- if c.extractor.FromRequestContext == nil {
- t.Error("from request context should not be nil")
- return
- }
- },
- }
-
- testCase2 := test{
- name: "Custom audience",
- extractor: NewClaimsExtractor(WithAudience("https://login/")),
- check: func(t *testing.T, c test) {
- t.Helper()
- if c.extractor.authAudience != "https://login/" {
- t.Errorf("audience expected %s, got %s", "https://login/", c.extractor.authAudience)
- return
- }
- },
- }
-
- testCase3 := test{
- name: "Custom user id claim",
- extractor: NewClaimsExtractor(WithUserIDClaim("customUserId")),
- check: func(t *testing.T, c test) {
- t.Helper()
- if c.extractor.userIDClaim != "customUserId" {
- t.Errorf("user id claim expected %s, got %s", "customUserId", c.extractor.userIDClaim)
- return
- }
- },
- }
-
- testCase4 := test{
- name: "Custom extractor from request context",
- extractor: NewClaimsExtractor(
- WithFromRequestContext(func(r *http.Request) AuthorizationClaims {
- return AuthorizationClaims{
- UserId: "testCustomRequest",
- }
- })),
- check: func(t *testing.T, c test) {
- t.Helper()
- claims := c.extractor.FromRequestContext(&http.Request{})
- if claims.UserId != "testCustomRequest" {
- t.Errorf("user id claim expected %s, got %s", "testCustomRequest", claims.UserId)
- return
- }
- },
- }
-
- for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4} {
- t.Run(testCase.name, func(t *testing.T) {
- testCase.check(t, testCase)
- })
- }
-}
diff --git a/management/server/jwtclaims/jwtValidator.go b/management/server/jwtclaims/jwtValidator.go
deleted file mode 100644
index 79e59e76f..000000000
--- a/management/server/jwtclaims/jwtValidator.go
+++ /dev/null
@@ -1,349 +0,0 @@
-package jwtclaims
-
-import (
- "context"
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rsa"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "math/big"
- "net/http"
- "strconv"
- "strings"
- "sync"
- "time"
-
- "github.com/golang-jwt/jwt"
- log "github.com/sirupsen/logrus"
-)
-
-// Options is a struct for specifying configuration options for the middleware.
-type Options struct {
- // The function that will return the Key to validate the JWT.
- // It can be either a shared secret or a public key.
- // Default value: nil
- ValidationKeyGetter jwt.Keyfunc
- // The name of the property in the request where the user information
- // from the JWT will be stored.
- // Default value: "user"
- UserProperty string
- // The function that will be called when there's an error validating the token
- // Default value:
- CredentialsOptional bool
- // A function that extracts the token from the request
- // Default: FromAuthHeader (i.e., from Authorization header as bearer token)
- Debug bool
- // When set, all requests with the OPTIONS method will use authentication
- // Default: false
- EnableAuthOnOptions bool
-}
-
-// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation
-type Jwks struct {
- Keys []JSONWebKey `json:"keys"`
- expiresInTime time.Time
-}
-
-// The supported elliptic curves types
-const (
- // p256 represents a cryptographic elliptical curve type.
- p256 = "P-256"
-
- // p384 represents a cryptographic elliptical curve type.
- p384 = "P-384"
-
- // p521 represents a cryptographic elliptical curve type.
- p521 = "P-521"
-)
-
-// JSONWebKey is a representation of a Jason Web Key
-type JSONWebKey struct {
- Kty string `json:"kty"`
- Kid string `json:"kid"`
- Use string `json:"use"`
- N string `json:"n"`
- E string `json:"e"`
- Crv string `json:"crv"`
- X string `json:"x"`
- Y string `json:"y"`
- X5c []string `json:"x5c"`
-}
-
-type JWTValidator interface {
- ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error)
-}
-
-// jwtValidatorImpl struct to handle token validation and parsing
-type jwtValidatorImpl struct {
- options Options
-}
-
-var keyNotFound = errors.New("unable to find appropriate key")
-
-// NewJWTValidator constructor
-func NewJWTValidator(ctx context.Context, issuer string, audienceList []string, keysLocation string, idpSignkeyRefreshEnabled bool) (JWTValidator, error) {
- keys, err := getPemKeys(ctx, keysLocation)
- if err != nil {
- return nil, err
- }
-
- var lock sync.Mutex
- options := Options{
- ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
- // Verify 'aud' claim
- var checkAud bool
- for _, audience := range audienceList {
- checkAud = token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
- if checkAud {
- break
- }
- }
- if !checkAud {
- return token, errors.New("invalid audience")
- }
- // Verify 'issuer' claim
- checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(issuer, false)
- if !checkIss {
- return token, errors.New("invalid issuer")
- }
-
- // If keys are rotated, verify the keys prior to token validation
- if idpSignkeyRefreshEnabled {
- // If the keys are invalid, retrieve new ones
- if !keys.stillValid() {
- lock.Lock()
- defer lock.Unlock()
-
- refreshedKeys, err := getPemKeys(ctx, keysLocation)
- if err != nil {
- log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
- refreshedKeys = keys
- }
-
- log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
-
- keys = refreshedKeys
- }
- }
-
- publicKey, err := getPublicKey(ctx, token, keys)
- if err == nil {
- return publicKey, nil
- }
-
- msg := fmt.Sprintf("getPublicKey error: %s", err)
- if errors.Is(err, keyNotFound) && !idpSignkeyRefreshEnabled {
- msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err)
- }
-
- log.WithContext(ctx).Error(msg)
-
- return nil, err
- },
- EnableAuthOnOptions: false,
- }
-
- if options.UserProperty == "" {
- options.UserProperty = "user"
- }
-
- return &jwtValidatorImpl{
- options: options,
- }, nil
-}
-
-// ValidateAndParse validates the token and returns the parsed token
-func (m *jwtValidatorImpl) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
- // If the token is empty...
- if token == "" {
- // Check if it was required
- if m.options.CredentialsOptional {
- log.WithContext(ctx).Debugf("no credentials found (CredentialsOptional=true)")
- // No error, just no token (and that is ok given that CredentialsOptional is true)
- return nil, nil //nolint:nilnil
- }
-
- // If we get here, the required token is missing
- errorMsg := "required authorization token not found"
- log.WithContext(ctx).Debugf(" Error: No credentials found (CredentialsOptional=false)")
- return nil, errors.New(errorMsg)
- }
-
- // Now parse the token
- parsedToken, err := jwt.Parse(token, m.options.ValidationKeyGetter)
-
- // Check if there was an error in parsing...
- if err != nil {
- log.WithContext(ctx).Errorf("error parsing token: %v", err)
- return nil, fmt.Errorf("error parsing token: %w", err)
- }
-
- // Check if the parsed token is valid...
- if !parsedToken.Valid {
- errorMsg := "token is invalid"
- log.WithContext(ctx).Debug(errorMsg)
- return nil, errors.New(errorMsg)
- }
-
- return parsedToken, nil
-}
-
-// stillValid returns true if the JSONWebKey still valid and have enough time to be used
-func (jwks *Jwks) stillValid() bool {
- return !jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime)
-}
-
-func getPemKeys(ctx context.Context, keysLocation string) (*Jwks, error) {
- resp, err := http.Get(keysLocation)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- jwks := &Jwks{}
- err = json.NewDecoder(resp.Body).Decode(jwks)
- if err != nil {
- return jwks, err
- }
-
- cacheControlHeader := resp.Header.Get("Cache-Control")
- expiresIn := getMaxAgeFromCacheHeader(ctx, cacheControlHeader)
- jwks.expiresInTime = time.Now().Add(time.Duration(expiresIn) * time.Second)
-
- return jwks, err
-}
-
-func getPublicKey(ctx context.Context, token *jwt.Token, jwks *Jwks) (interface{}, error) {
- // todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time
-
- for k := range jwks.Keys {
- if token.Header["kid"] != jwks.Keys[k].Kid {
- continue
- }
-
- if len(jwks.Keys[k].X5c) != 0 {
- cert := "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
- return jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
- }
-
- if jwks.Keys[k].Kty == "RSA" {
- log.WithContext(ctx).Debugf("generating PublicKey from RSA JWK")
- return getPublicKeyFromRSA(jwks.Keys[k])
- }
- if jwks.Keys[k].Kty == "EC" {
- log.WithContext(ctx).Debugf("generating PublicKey from ECDSA JWK")
- return getPublicKeyFromECDSA(jwks.Keys[k])
- }
-
- log.WithContext(ctx).Debugf("Key Type: %s not yet supported, please raise ticket!", jwks.Keys[k].Kty)
- }
-
- return nil, keyNotFound
-}
-
-func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) {
-
- if jwk.X == "" || jwk.Y == "" || jwk.Crv == "" {
- return nil, fmt.Errorf("ecdsa key incomplete")
- }
-
- var xCoordinate []byte
- if xCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.X); err != nil {
- return nil, err
- }
-
- var yCoordinate []byte
- if yCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.Y); err != nil {
- return nil, err
- }
-
- publicKey = &ecdsa.PublicKey{}
-
- var curve elliptic.Curve
- switch jwk.Crv {
- case p256:
- curve = elliptic.P256()
- case p384:
- curve = elliptic.P384()
- case p521:
- curve = elliptic.P521()
- }
-
- publicKey.Curve = curve
- publicKey.X = big.NewInt(0).SetBytes(xCoordinate)
- publicKey.Y = big.NewInt(0).SetBytes(yCoordinate)
-
- return publicKey, nil
-}
-
-func getPublicKeyFromRSA(jwk JSONWebKey) (*rsa.PublicKey, error) {
-
- decodedE, err := base64.RawURLEncoding.DecodeString(jwk.E)
- if err != nil {
- return nil, err
- }
- decodedN, err := base64.RawURLEncoding.DecodeString(jwk.N)
- if err != nil {
- return nil, err
- }
-
- var n, e big.Int
- e.SetBytes(decodedE)
- n.SetBytes(decodedN)
-
- return &rsa.PublicKey{
- E: int(e.Int64()),
- N: &n,
- }, nil
-}
-
-// getMaxAgeFromCacheHeader extracts max-age directive from the Cache-Control header
-func getMaxAgeFromCacheHeader(ctx context.Context, cacheControl string) int {
- // Split into individual directives
- directives := strings.Split(cacheControl, ",")
-
- for _, directive := range directives {
- directive = strings.TrimSpace(directive)
- if strings.HasPrefix(directive, "max-age=") {
- // Extract the max-age value
- maxAgeStr := strings.TrimPrefix(directive, "max-age=")
- maxAge, err := strconv.Atoi(maxAgeStr)
- if err != nil {
- log.WithContext(ctx).Debugf("error parsing max-age: %v", err)
- return 0
- }
-
- return maxAge
- }
- }
-
- return 0
-}
-
-type JwtValidatorMock struct{}
-
-func (j *JwtValidatorMock) ValidateAndParse(ctx context.Context, token string) (*jwt.Token, error) {
- claimMaps := jwt.MapClaims{}
-
- switch token {
- case "testUserId", "testAdminId", "testOwnerId", "testServiceUserId", "testServiceAdminId", "blockedUserId":
- claimMaps[UserIDClaim] = token
- claimMaps[AccountIDSuffix] = "testAccountId"
- claimMaps[DomainIDSuffix] = "test.com"
- claimMaps[DomainCategorySuffix] = "private"
- case "otherUserId":
- claimMaps[UserIDClaim] = "otherUserId"
- claimMaps[AccountIDSuffix] = "otherAccountId"
- claimMaps[DomainIDSuffix] = "other.com"
- claimMaps[DomainCategorySuffix] = "private"
- case "invalidToken":
- return nil, errors.New("invalid token")
- }
-
- jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
- return jwtToken, nil
-}
-
diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go
index 0df2462f4..4d0630f0f 100644
--- a/management/server/management_proto_test.go
+++ b/management/server/management_proto_test.go
@@ -94,7 +94,7 @@ func Test_SyncProtocol(t *testing.T) {
mgmtServer, _, mgmtAddr, cleanup, err := startManagementForTest(t, "testdata/store_with_expired_peers.sql", &Config{
Stuns: []*Host{{
Proto: "udp",
- URI: "stun:stun.wiretrustee.com:3468",
+ URI: "stun:stun.netbird.io:3468",
}},
TURNConfig: &TURNConfig{
TimeBasedCredentials: false,
@@ -102,12 +102,12 @@ func Test_SyncProtocol(t *testing.T) {
Secret: "whatever",
Turns: []*Host{{
Proto: "udp",
- URI: "turn:stun.wiretrustee.com:3468",
+ URI: "turn:stun.netbird.io:3468",
}},
},
Signal: &Host{
Proto: "http",
- URI: "signal.wiretrustee.com:10000",
+ URI: "signal.netbird.io:10000",
},
Datadir: dir,
HttpConfig: nil,
@@ -173,64 +173,64 @@ func Test_SyncProtocol(t *testing.T) {
return
}
- wiretrusteeConfig := syncResp.GetWiretrusteeConfig()
- if wiretrusteeConfig == nil {
- t.Fatal("expecting SyncResponse to have non-nil WiretrusteeConfig")
+ netbirdConfig := syncResp.GetNetbirdConfig()
+ if netbirdConfig == nil {
+ t.Fatal("expecting SyncResponse to have non-nil NetbirdConfig")
}
- if wiretrusteeConfig.GetSignal() == nil {
- t.Fatal("expecting SyncResponse to have WiretrusteeConfig with non-nil Signal config")
+ if netbirdConfig.GetSignal() == nil {
+ t.Fatal("expecting SyncResponse to have NetbirdConfig with non-nil Signal config")
}
expectedSignalConfig := &mgmtProto.HostConfig{
- Uri: "signal.wiretrustee.com:10000",
+ Uri: "signal.netbird.io:10000",
Protocol: mgmtProto.HostConfig_HTTP,
}
- if wiretrusteeConfig.GetSignal().GetUri() != expectedSignalConfig.GetUri() {
- t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected Signal URI: %v, actual: %v",
+ if netbirdConfig.GetSignal().GetUri() != expectedSignalConfig.GetUri() {
+ t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected Signal URI: %v, actual: %v",
expectedSignalConfig.GetUri(),
- wiretrusteeConfig.GetSignal().GetUri())
+ netbirdConfig.GetSignal().GetUri())
}
- if wiretrusteeConfig.GetSignal().GetProtocol() != expectedSignalConfig.GetProtocol() {
- t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected Signal Protocol: %v, actual: %v",
+ if netbirdConfig.GetSignal().GetProtocol() != expectedSignalConfig.GetProtocol() {
+ t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected Signal Protocol: %v, actual: %v",
expectedSignalConfig.GetProtocol().String(),
- wiretrusteeConfig.GetSignal().GetProtocol())
+ netbirdConfig.GetSignal().GetProtocol())
}
expectedStunsConfig := &mgmtProto.HostConfig{
- Uri: "stun:stun.wiretrustee.com:3468",
+ Uri: "stun:stun.netbird.io:3468",
Protocol: mgmtProto.HostConfig_UDP,
}
- if wiretrusteeConfig.GetStuns()[0].GetUri() != expectedStunsConfig.GetUri() {
- t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected STUN URI: %v, actual: %v",
+ if netbirdConfig.GetStuns()[0].GetUri() != expectedStunsConfig.GetUri() {
+ t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected STUN URI: %v, actual: %v",
expectedStunsConfig.GetUri(),
- wiretrusteeConfig.GetStuns()[0].GetUri())
+ netbirdConfig.GetStuns()[0].GetUri())
}
- if wiretrusteeConfig.GetStuns()[0].GetProtocol() != expectedStunsConfig.GetProtocol() {
- t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected STUN Protocol: %v, actual: %v",
+ if netbirdConfig.GetStuns()[0].GetProtocol() != expectedStunsConfig.GetProtocol() {
+ t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected STUN Protocol: %v, actual: %v",
expectedStunsConfig.GetProtocol(),
- wiretrusteeConfig.GetStuns()[0].GetProtocol())
+ netbirdConfig.GetStuns()[0].GetProtocol())
}
expectedTRUNHost := &mgmtProto.HostConfig{
- Uri: "turn:stun.wiretrustee.com:3468",
+ Uri: "turn:stun.netbird.io:3468",
Protocol: mgmtProto.HostConfig_UDP,
}
- if wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetUri() != expectedTRUNHost.GetUri() {
- t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected TURN URI: %v, actual: %v",
+ if netbirdConfig.GetTurns()[0].GetHostConfig().GetUri() != expectedTRUNHost.GetUri() {
+ t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected TURN URI: %v, actual: %v",
expectedTRUNHost.GetUri(),
- wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetUri())
+ netbirdConfig.GetTurns()[0].GetHostConfig().GetUri())
}
- if wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetProtocol() != expectedTRUNHost.GetProtocol() {
- t.Fatalf("expecting SyncResponse to have WiretrusteeConfig with expected TURN Protocol: %v, actual: %v",
+ if netbirdConfig.GetTurns()[0].GetHostConfig().GetProtocol() != expectedTRUNHost.GetProtocol() {
+ t.Fatalf("expecting SyncResponse to have NetbirdConfig with expected TURN Protocol: %v, actual: %v",
expectedTRUNHost.GetProtocol().String(),
- wiretrusteeConfig.GetTurns()[0].GetHostConfig().GetProtocol())
+ netbirdConfig.GetTurns()[0].GetHostConfig().GetProtocol())
}
// ensure backward compatibility
@@ -285,13 +285,13 @@ func loginPeerWithValidSetupKey(key wgtypes.Key, client mgmtProto.ManagementServ
}
meta := &mgmtProto.PeerSystemMeta{
- Hostname: key.PublicKey().String(),
- GoOS: runtime.GOOS,
- OS: runtime.GOOS,
- Core: "core",
- Platform: "platform",
- Kernel: "kernel",
- WiretrusteeVersion: "",
+ Hostname: key.PublicKey().String(),
+ GoOS: runtime.GOOS,
+ OS: runtime.GOOS,
+ Core: "core",
+ Platform: "platform",
+ Kernel: "kernel",
+ NetbirdVersion: "",
}
message, err := encryption.EncryptMessage(*serverKey, key, &mgmtProto.LoginRequest{SetupKey: TestValidSetupKey, Meta: meta})
if err != nil {
@@ -440,7 +440,7 @@ func startManagementForTest(t *testing.T, testFile string, config *Config) (*grp
secretsManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
ephemeralMgr := NewEphemeralManager(store, accountManager)
- mgmtServer, err := NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, ephemeralMgr)
+ mgmtServer, err := NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, ephemeralMgr, nil)
if err != nil {
return nil, nil, "", cleanup, err
}
@@ -498,7 +498,7 @@ func testSyncStatusRace(t *testing.T) {
mgmtServer, am, mgmtAddr, cleanup, err := startManagementForTest(t, "testdata/store_with_expired_peers.sql", &Config{
Stuns: []*Host{{
Proto: "udp",
- URI: "stun:stun.wiretrustee.com:3468",
+ URI: "stun:stun.netbird.io:3468",
}},
TURNConfig: &TURNConfig{
TimeBasedCredentials: false,
@@ -506,12 +506,12 @@ func testSyncStatusRace(t *testing.T) {
Secret: "whatever",
Turns: []*Host{{
Proto: "udp",
- URI: "turn:stun.wiretrustee.com:3468",
+ URI: "turn:stun.netbird.io:3468",
}},
},
Signal: &Host{
Proto: "http",
- URI: "signal.wiretrustee.com:10000",
+ URI: "signal.netbird.io:10000",
},
Datadir: dir,
HttpConfig: nil,
@@ -670,7 +670,7 @@ func Test_LoginPerformance(t *testing.T) {
mgmtServer, am, _, cleanup, err := startManagementForTest(t, "testdata/store_with_expired_peers.sql", &Config{
Stuns: []*Host{{
Proto: "udp",
- URI: "stun:stun.wiretrustee.com:3468",
+ URI: "stun:stun.netbird.io:3468",
}},
TURNConfig: &TURNConfig{
TimeBasedCredentials: false,
@@ -678,12 +678,12 @@ func Test_LoginPerformance(t *testing.T) {
Secret: "whatever",
Turns: []*Host{{
Proto: "udp",
- URI: "turn:stun.wiretrustee.com:3468",
+ URI: "turn:stun.netbird.io:3468",
}},
},
Signal: &Host{
Proto: "http",
- URI: "signal.wiretrustee.com:10000",
+ URI: "signal.netbird.io:10000",
},
Datadir: dir,
HttpConfig: nil,
@@ -714,7 +714,7 @@ func Test_LoginPerformance(t *testing.T) {
return
}
- setupKey, err := am.CreateSetupKey(context.Background(), account.Id, fmt.Sprintf("key-%d", j), types.SetupKeyReusable, time.Hour, nil, 0, fmt.Sprintf("user-%d", j), false)
+ setupKey, err := am.CreateSetupKey(context.Background(), account.Id, fmt.Sprintf("key-%d", j), types.SetupKeyReusable, time.Hour, nil, 0, fmt.Sprintf("user-%d", j), false, false)
if err != nil {
t.Logf("error creating setup key: %v", err)
return
@@ -730,13 +730,13 @@ func Test_LoginPerformance(t *testing.T) {
}
meta := &mgmtProto.PeerSystemMeta{
- Hostname: key.PublicKey().String(),
- GoOS: runtime.GOOS,
- OS: runtime.GOOS,
- Core: "core",
- Platform: "platform",
- Kernel: "kernel",
- WiretrusteeVersion: "",
+ Hostname: key.PublicKey().String(),
+ GoOS: runtime.GOOS,
+ OS: runtime.GOOS,
+ Core: "core",
+ Platform: "platform",
+ Kernel: "kernel",
+ NetbirdVersion: "",
}
peerLogin := PeerLogin{
diff --git a/management/server/management_suite_test.go b/management/server/management_suite_test.go
deleted file mode 100644
index cc99624a0..000000000
--- a/management/server/management_suite_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package server_test
-
-import (
- . "github.com/onsi/ginkgo"
- . "github.com/onsi/gomega"
-
- "testing"
-)
-
-func TestManagement(t *testing.T) {
- RegisterFailHandler(Fail)
- RunSpecs(t, "Management Service Suite")
-}
diff --git a/management/server/management_test.go b/management/server/management_test.go
index cfa2c138f..fd82d8037 100644
--- a/management/server/management_test.go
+++ b/management/server/management_test.go
@@ -6,13 +6,13 @@ import (
"net"
"os"
"runtime"
- sync2 "sync"
+ "sync"
+ "testing"
"time"
pb "github.com/golang/protobuf/proto" //nolint
- . "github.com/onsi/ginkgo"
- . "github.com/onsi/gomega"
log "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -30,450 +30,110 @@ import (
const (
ValidSetupKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
- AccountKey = "bf1c8084-ba50-4ce7-9439-34653001fc3b"
)
-var _ = Describe("Management service", func() {
- var (
- addr string
- s *grpc.Server
- dataDir string
- client mgmtProto.ManagementServiceClient
- serverPubKey wgtypes.Key
- conn *grpc.ClientConn
- )
-
- BeforeEach(func() {
- level, _ := log.ParseLevel("Debug")
- log.SetLevel(level)
- var err error
- dataDir, err = os.MkdirTemp("", "wiretrustee_mgmt_test_tmp_*")
- Expect(err).NotTo(HaveOccurred())
-
- var listener net.Listener
-
- config := &server.Config{}
- _, err = util.ReadJson("testdata/management.json", config)
- Expect(err).NotTo(HaveOccurred())
- config.Datadir = dataDir
-
- s, listener = startServer(config, dataDir, "testdata/store.sql")
- addr = listener.Addr().String()
- client, conn = createRawClient(addr)
-
- // s public key
- resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{})
- Expect(err).NotTo(HaveOccurred())
- serverPubKey, err = wgtypes.ParseKey(resp.Key)
- Expect(err).NotTo(HaveOccurred())
- })
-
- AfterEach(func() {
- s.Stop()
- err := conn.Close()
- Expect(err).NotTo(HaveOccurred())
- os.RemoveAll(dataDir)
- })
-
- Context("when calling IsHealthy endpoint", func() {
- Specify("a non-error result is returned", func() {
- healthy, err := client.IsHealthy(context.TODO(), &mgmtProto.Empty{})
-
- Expect(err).NotTo(HaveOccurred())
- Expect(healthy).ToNot(BeNil())
- })
- })
-
- Context("when calling Sync endpoint", func() {
- Context("when there is a new peer registered", func() {
- Specify("a proper configuration is returned", func() {
- key, _ := wgtypes.GenerateKey()
- loginPeerWithValidSetupKey(serverPubKey, key, client)
-
- syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}
- encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq)
- Expect(err).NotTo(HaveOccurred())
-
- sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
- WgPubKey: key.PublicKey().String(),
- Body: encryptedBytes,
- })
- Expect(err).NotTo(HaveOccurred())
-
- encryptedResponse := &mgmtProto.EncryptedMessage{}
- err = sync.RecvMsg(encryptedResponse)
- Expect(err).NotTo(HaveOccurred())
-
- resp := &mgmtProto.SyncResponse{}
- err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp)
- Expect(err).NotTo(HaveOccurred())
-
- expectedSignalConfig := &mgmtProto.HostConfig{
- Uri: "signal.wiretrustee.com:10000",
- Protocol: mgmtProto.HostConfig_HTTP,
- }
- expectedStunsConfig := &mgmtProto.HostConfig{
- Uri: "stun:stun.wiretrustee.com:3468",
- Protocol: mgmtProto.HostConfig_UDP,
- }
- expectedTRUNHost := &mgmtProto.HostConfig{
- Uri: "turn:stun.wiretrustee.com:3468",
- Protocol: mgmtProto.HostConfig_UDP,
- }
-
- Expect(resp.WiretrusteeConfig.Signal).To(BeEquivalentTo(expectedSignalConfig))
- Expect(resp.WiretrusteeConfig.Stuns).To(ConsistOf(expectedStunsConfig))
- // TURN validation is special because credentials are dynamically generated
- Expect(resp.WiretrusteeConfig.Turns).To(HaveLen(1))
- actualTURN := resp.WiretrusteeConfig.Turns[0]
- Expect(len(actualTURN.User) > 0).To(BeTrue())
- Expect(actualTURN.HostConfig).To(BeEquivalentTo(expectedTRUNHost))
- Expect(len(resp.NetworkMap.OfflinePeers) == 0).To(BeTrue())
- })
- })
-
- Context("when there are 3 peers registered under one account", func() {
- Specify("a list containing other 2 peers is returned", func() {
- key, _ := wgtypes.GenerateKey()
- key1, _ := wgtypes.GenerateKey()
- key2, _ := wgtypes.GenerateKey()
- loginPeerWithValidSetupKey(serverPubKey, key, client)
- loginPeerWithValidSetupKey(serverPubKey, key1, client)
- loginPeerWithValidSetupKey(serverPubKey, key2, client)
-
- messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}})
- Expect(err).NotTo(HaveOccurred())
- encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key)
- Expect(err).NotTo(HaveOccurred())
-
- sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
- WgPubKey: key.PublicKey().String(),
- Body: encryptedBytes,
- })
- Expect(err).NotTo(HaveOccurred())
-
- encryptedResponse := &mgmtProto.EncryptedMessage{}
- err = sync.RecvMsg(encryptedResponse)
- Expect(err).NotTo(HaveOccurred())
- decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key)
- Expect(err).NotTo(HaveOccurred())
-
- resp := &mgmtProto.SyncResponse{}
- err = pb.Unmarshal(decryptedBytes, resp)
- Expect(err).NotTo(HaveOccurred())
-
- Expect(resp.GetRemotePeers()).To(HaveLen(2))
- peers := []string{resp.GetRemotePeers()[0].WgPubKey, resp.GetRemotePeers()[1].WgPubKey}
- Expect(peers).To(ContainElements(key1.PublicKey().String(), key2.PublicKey().String()))
- })
- })
-
- Context("when there is a new peer registered", func() {
- Specify("an update is returned", func() {
- // register only a single peer
- key, _ := wgtypes.GenerateKey()
- loginPeerWithValidSetupKey(serverPubKey, key, client)
-
- messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}})
- Expect(err).NotTo(HaveOccurred())
- encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key)
- Expect(err).NotTo(HaveOccurred())
-
- sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
- WgPubKey: key.PublicKey().String(),
- Body: encryptedBytes,
- })
- Expect(err).NotTo(HaveOccurred())
-
- // after the initial sync call we have 0 peer updates
- encryptedResponse := &mgmtProto.EncryptedMessage{}
- err = sync.RecvMsg(encryptedResponse)
- Expect(err).NotTo(HaveOccurred())
- decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key)
- Expect(err).NotTo(HaveOccurred())
- resp := &mgmtProto.SyncResponse{}
- err = pb.Unmarshal(decryptedBytes, resp)
- Expect(resp.GetRemotePeers()).To(HaveLen(0))
-
- wg := sync2.WaitGroup{}
- wg.Add(1)
-
- // continue listening on updates for a peer
- go func() {
- err = sync.RecvMsg(encryptedResponse)
-
- decryptedBytes, err = encryption.Decrypt(encryptedResponse.Body, serverPubKey, key)
- Expect(err).NotTo(HaveOccurred())
- resp = &mgmtProto.SyncResponse{}
- err = pb.Unmarshal(decryptedBytes, resp)
- wg.Done()
- }()
-
- // register a new peer
- key1, _ := wgtypes.GenerateKey()
- loginPeerWithValidSetupKey(serverPubKey, key1, client)
-
- wg.Wait()
-
- Expect(err).NotTo(HaveOccurred())
- Expect(resp.GetRemotePeers()).To(HaveLen(1))
- Expect(resp.GetRemotePeers()[0].WgPubKey).To(BeEquivalentTo(key1.PublicKey().String()))
- })
- })
- })
-
- Context("when calling GetServerKey endpoint", func() {
- Specify("a public Wireguard key of the service is returned", func() {
- resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{})
-
- Expect(err).NotTo(HaveOccurred())
- Expect(resp).ToNot(BeNil())
- Expect(resp.Key).ToNot(BeNil())
- Expect(resp.ExpiresAt).ToNot(BeNil())
-
- // check if the key is a valid Wireguard key
- key, err := wgtypes.ParseKey(resp.Key)
- Expect(err).NotTo(HaveOccurred())
- Expect(key).ToNot(BeNil())
- })
- })
-
- Context("when calling Login endpoint", func() {
- Context("with an invalid setup key", func() {
- Specify("an error is returned", func() {
- key, _ := wgtypes.GenerateKey()
- message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: "invalid setup key",
- Meta: &mgmtProto.PeerSystemMeta{}})
- Expect(err).NotTo(HaveOccurred())
-
- resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{
- WgPubKey: key.PublicKey().String(),
- Body: message,
- })
-
- Expect(err).To(HaveOccurred())
- Expect(resp).To(BeNil())
- })
- })
-
- Context("with a valid setup key", func() {
- It("a non error result is returned", func() {
- key, _ := wgtypes.GenerateKey()
- resp := loginPeerWithValidSetupKey(serverPubKey, key, client)
-
- Expect(resp).ToNot(BeNil())
- })
- })
-
- Context("with a registered peer", func() {
- It("a non error result is returned", func() {
- key, _ := wgtypes.GenerateKey()
- regResp := loginPeerWithValidSetupKey(serverPubKey, key, client)
- Expect(regResp).NotTo(BeNil())
-
- // just login without registration
- message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}})
- Expect(err).NotTo(HaveOccurred())
- loginResp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{
- WgPubKey: key.PublicKey().String(),
- Body: message,
- })
-
- Expect(err).NotTo(HaveOccurred())
-
- decryptedResp := &mgmtProto.LoginResponse{}
- err = encryption.DecryptMessage(serverPubKey, key, loginResp.Body, decryptedResp)
- Expect(err).NotTo(HaveOccurred())
-
- expectedSignalConfig := &mgmtProto.HostConfig{
- Uri: "signal.wiretrustee.com:10000",
- Protocol: mgmtProto.HostConfig_HTTP,
- }
- expectedStunsConfig := &mgmtProto.HostConfig{
- Uri: "stun:stun.wiretrustee.com:3468",
- Protocol: mgmtProto.HostConfig_UDP,
- }
- expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{
- HostConfig: &mgmtProto.HostConfig{
- Uri: "turn:stun.wiretrustee.com:3468",
- Protocol: mgmtProto.HostConfig_UDP,
- },
- User: "some_user",
- Password: "some_password",
- }
-
- Expect(decryptedResp.GetWiretrusteeConfig().Signal).To(BeEquivalentTo(expectedSignalConfig))
- Expect(decryptedResp.GetWiretrusteeConfig().Stuns).To(ConsistOf(expectedStunsConfig))
- Expect(decryptedResp.GetWiretrusteeConfig().Turns).To(ConsistOf(expectedTurnsConfig))
- })
- })
- })
-
- Context("when there are 10 peers registered under one account", func() {
- Context("when there are 10 more peers registered under the same account", func() {
- Specify("all of the 10 peers will get updates of 10 newly registered peers", func() {
- initialPeers := 10
- additionalPeers := 10
-
- var peers []wgtypes.Key
- for i := 0; i < initialPeers; i++ {
- key, _ := wgtypes.GenerateKey()
- loginPeerWithValidSetupKey(serverPubKey, key, client)
- peers = append(peers, key)
- }
-
- wg := sync2.WaitGroup{}
- wg.Add(initialPeers + initialPeers*additionalPeers)
-
- var clients []mgmtProto.ManagementService_SyncClient
- for _, peer := range peers {
- messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}})
- Expect(err).NotTo(HaveOccurred())
- encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, peer)
- Expect(err).NotTo(HaveOccurred())
-
- // open stream
- sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
- WgPubKey: peer.PublicKey().String(),
- Body: encryptedBytes,
- })
- Expect(err).NotTo(HaveOccurred())
- clients = append(clients, sync)
-
- // receive stream
- peer := peer
- go func() {
- for {
- encryptedResponse := &mgmtProto.EncryptedMessage{}
- err = sync.RecvMsg(encryptedResponse)
- if err != nil {
- break
- }
- decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, peer)
- Expect(err).NotTo(HaveOccurred())
-
- resp := &mgmtProto.SyncResponse{}
- err = pb.Unmarshal(decryptedBytes, resp)
- Expect(err).NotTo(HaveOccurred())
- if len(resp.GetRemotePeers()) > 0 {
- // only consider peer updates
- wg.Done()
- }
- }
- }()
- }
-
- time.Sleep(1 * time.Second)
- for i := 0; i < additionalPeers; i++ {
- key, _ := wgtypes.GenerateKey()
- loginPeerWithValidSetupKey(serverPubKey, key, client)
- r := rand.New(rand.NewSource(time.Now().UnixNano()))
- n := r.Intn(200)
- time.Sleep(time.Duration(n) * time.Millisecond)
- }
-
- wg.Wait()
-
- for _, syncClient := range clients {
- err := syncClient.CloseSend()
- Expect(err).NotTo(HaveOccurred())
- }
- })
- })
- })
-
- Context("when there are peers registered under one account concurrently", func() {
- Specify("then there are no duplicate IPs", func() {
- initialPeers := 30
-
- ipChannel := make(chan string, 20)
- for i := 0; i < initialPeers; i++ {
- go func() {
- defer GinkgoRecover()
- key, _ := wgtypes.GenerateKey()
- loginPeerWithValidSetupKey(serverPubKey, key, client)
- syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}
- encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq)
- Expect(err).NotTo(HaveOccurred())
-
- // open stream
- sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
- WgPubKey: key.PublicKey().String(),
- Body: encryptedBytes,
- })
- Expect(err).NotTo(HaveOccurred())
- encryptedResponse := &mgmtProto.EncryptedMessage{}
- err = sync.RecvMsg(encryptedResponse)
- Expect(err).NotTo(HaveOccurred())
-
- resp := &mgmtProto.SyncResponse{}
- err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp)
- Expect(err).NotTo(HaveOccurred())
-
- ipChannel <- resp.GetPeerConfig().Address
- }()
- }
-
- ips := make(map[string]struct{})
- for ip := range ipChannel {
- if _, ok := ips[ip]; ok {
- Fail("found duplicate IP: " + ip)
- }
- ips[ip] = struct{}{}
- if len(ips) == initialPeers {
- break
- }
- }
- close(ipChannel)
- })
- })
-
- Context("after login two peers", func() {
- Specify("then they receive the same network", func() {
- key, _ := wgtypes.GenerateKey()
- firstLogin := loginPeerWithValidSetupKey(serverPubKey, key, client)
- key, _ = wgtypes.GenerateKey()
- secondLogin := loginPeerWithValidSetupKey(serverPubKey, key, client)
-
- _, firstLoginNetwork, err := net.ParseCIDR(firstLogin.GetPeerConfig().GetAddress())
- Expect(err).NotTo(HaveOccurred())
- _, secondLoginNetwork, err := net.ParseCIDR(secondLogin.GetPeerConfig().GetAddress())
- Expect(err).NotTo(HaveOccurred())
-
- Expect(secondLoginNetwork.String()).To(BeEquivalentTo(firstLoginNetwork.String()))
- })
- })
-})
-
-func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, client mgmtProto.ManagementServiceClient) *mgmtProto.LoginResponse {
- defer GinkgoRecover()
-
- meta := &mgmtProto.PeerSystemMeta{
- Hostname: key.PublicKey().String(),
- GoOS: runtime.GOOS,
- OS: runtime.GOOS,
- Core: "core",
- Platform: "platform",
- Kernel: "kernel",
- WiretrusteeVersion: "",
+type testSuite struct {
+ t *testing.T
+ addr string
+ grpcServer *grpc.Server
+ dataDir string
+ client mgmtProto.ManagementServiceClient
+ serverPubKey wgtypes.Key
+ conn *grpc.ClientConn
+}
+
+func setupTest(t *testing.T) *testSuite {
+ t.Helper()
+ level, _ := log.ParseLevel("Debug")
+ log.SetLevel(level)
+
+ ts := &testSuite{t: t}
+
+ var err error
+ ts.dataDir, err = os.MkdirTemp("", "netbird_mgmt_test_tmp_*")
+ if err != nil {
+ t.Fatalf("failed to create temp directory: %v", err)
+ }
+
+ config := &server.Config{}
+ _, err = util.ReadJson("testdata/management.json", config)
+ if err != nil {
+ t.Fatalf("failed to read management.json: %v", err)
+ }
+ config.Datadir = ts.dataDir
+
+ var listener net.Listener
+ ts.grpcServer, listener = startServer(t, config, ts.dataDir, "testdata/store.sql")
+ ts.addr = listener.Addr().String()
+
+ ts.client, ts.conn = createRawClient(t, ts.addr)
+
+ resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{})
+ if err != nil {
+ t.Fatalf("failed to get server key: %v", err)
+ }
+
+ serverKey, err := wgtypes.ParseKey(resp.Key)
+ if err != nil {
+ t.Fatalf("failed to parse server key: %v", err)
+ }
+ ts.serverPubKey = serverKey
+
+ return ts
+}
+
+func tearDownTest(t *testing.T, ts *testSuite) {
+ t.Helper()
+ ts.grpcServer.Stop()
+ if err := ts.conn.Close(); err != nil {
+ t.Fatalf("failed to close client connection: %v", err)
+ }
+ time.Sleep(100 * time.Millisecond)
+ if err := os.RemoveAll(ts.dataDir); err != nil {
+ t.Fatalf("failed to remove data directory %s: %v", ts.dataDir, err)
+ }
+}
+
+func loginPeerWithValidSetupKey(
+ t *testing.T,
+ serverPubKey wgtypes.Key,
+ key wgtypes.Key,
+ client mgmtProto.ManagementServiceClient,
+) *mgmtProto.LoginResponse {
+ t.Helper()
+ meta := &mgmtProto.PeerSystemMeta{
+ Hostname: key.PublicKey().String(),
+ GoOS: runtime.GOOS,
+ OS: runtime.GOOS,
+ Core: "core",
+ Platform: "platform",
+ Kernel: "kernel",
+ NetbirdVersion: "",
+ }
+ msgToEncrypt := &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta}
+ message, err := encryption.EncryptMessage(serverPubKey, key, msgToEncrypt)
+ if err != nil {
+ t.Fatalf("failed to encrypt login request: %v", err)
}
- message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta})
- Expect(err).NotTo(HaveOccurred())
resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{
WgPubKey: key.PublicKey().String(),
Body: message,
})
-
- Expect(err).NotTo(HaveOccurred())
+ if err != nil {
+ t.Fatalf("login request failed: %v", err)
+ }
loginResp := &mgmtProto.LoginResponse{}
err = encryption.DecryptMessage(serverPubKey, key, resp.Body, loginResp)
- Expect(err).NotTo(HaveOccurred())
+ if err != nil {
+ t.Fatalf("failed to decrypt login response: %v", err)
+ }
return loginResp
}
-func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) {
+func createRawClient(t *testing.T, addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) {
+ t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -484,17 +144,27 @@ func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.Clie
Time: 10 * time.Second,
Timeout: 2 * time.Second,
}))
- Expect(err).NotTo(HaveOccurred())
+ if err != nil {
+ t.Fatalf("failed to dial gRPC server: %v", err)
+ }
return mgmtProto.NewManagementServiceClient(conn), conn
}
-func startServer(config *server.Config, dataDir string, testFile string) (*grpc.Server, net.Listener) {
+func startServer(
+ t *testing.T,
+ config *server.Config,
+ dataDir string,
+ testFile string,
+) (*grpc.Server, net.Listener) {
+ t.Helper()
lis, err := net.Listen("tcp", ":0")
- Expect(err).NotTo(HaveOccurred())
+ if err != nil {
+ t.Fatalf("failed to listen on a random port: %v", err)
+ }
s := grpc.NewServer()
- store, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir)
+ str, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir)
if err != nil {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
@@ -504,23 +174,530 @@ func startServer(config *server.Config, dataDir string, testFile string) (*grpc.
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
if err != nil {
- log.Fatalf("failed creating metrics: %v", err)
+ t.Fatalf("failed creating metrics: %v", err)
}
- accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, server.MocIntegratedValidator{}, metrics)
+ accountManager, err := server.BuildManager(
+ context.Background(),
+ str,
+ peersUpdateManager,
+ nil,
+ "",
+ "netbird.selfhosted",
+ eventStore,
+ nil,
+ false,
+ server.MocIntegratedValidator{},
+ metrics,
+ )
if err != nil {
- log.Fatalf("failed creating a manager: %v", err)
+ t.Fatalf("failed creating an account manager: %v", err)
}
secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay)
- mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil)
- Expect(err).NotTo(HaveOccurred())
+ mgmtServer, err := server.NewServer(
+ context.Background(),
+ config,
+ accountManager,
+ settings.NewManager(str),
+ peersUpdateManager,
+ secretsManager,
+ nil,
+ nil,
+ nil,
+ )
+ if err != nil {
+ t.Fatalf("failed creating management server: %v", err)
+ }
+
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
+
go func() {
if err := s.Serve(lis); err != nil {
- Expect(err).NotTo(HaveOccurred())
+ t.Errorf("failed to serve gRPC: %v", err)
+ return
}
}()
return s, lis
}
+
+func TestIsHealthy(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ healthy, err := ts.client.IsHealthy(context.TODO(), &mgmtProto.Empty{})
+ if err != nil {
+ t.Fatalf("IsHealthy call returned an error: %v", err)
+ }
+ if healthy == nil {
+ t.Fatal("IsHealthy returned a nil response")
+ }
+}
+
+func TestSyncNewPeerConfiguration(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ peerKey, _ := wgtypes.GenerateKey()
+ loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client)
+
+ syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}
+ encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, syncReq)
+ if err != nil {
+ t.Fatalf("failed to encrypt sync request: %v", err)
+ }
+
+ syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
+ WgPubKey: peerKey.PublicKey().String(),
+ Body: encryptedBytes,
+ })
+ if err != nil {
+ t.Fatalf("failed to call Sync: %v", err)
+ }
+
+ encryptedResponse := &mgmtProto.EncryptedMessage{}
+ err = syncStream.RecvMsg(encryptedResponse)
+ if err != nil {
+ t.Fatalf("failed to receive sync response message: %v", err)
+ }
+
+ resp := &mgmtProto.SyncResponse{}
+ err = encryption.DecryptMessage(ts.serverPubKey, peerKey, encryptedResponse.Body, resp)
+ if err != nil {
+ t.Fatalf("failed to decrypt sync response: %v", err)
+ }
+
+ expectedSignalConfig := &mgmtProto.HostConfig{
+ Uri: "signal.netbird.io:10000",
+ Protocol: mgmtProto.HostConfig_HTTP,
+ }
+ expectedStunsConfig := &mgmtProto.HostConfig{
+ Uri: "stun:stun.netbird.io:3468",
+ Protocol: mgmtProto.HostConfig_UDP,
+ }
+ expectedTRUNHost := &mgmtProto.HostConfig{
+ Uri: "turn:stun.netbird.io:3468",
+ Protocol: mgmtProto.HostConfig_UDP,
+ }
+
+ assert.NotNil(t, resp.NetbirdConfig)
+ assert.Equal(t, resp.NetbirdConfig.Signal, expectedSignalConfig)
+ assert.Contains(t, resp.NetbirdConfig.Stuns, expectedStunsConfig)
+ assert.Equal(t, len(resp.NetbirdConfig.Turns), 1)
+ actualTURN := resp.NetbirdConfig.Turns[0]
+ assert.Greater(t, len(actualTURN.User), 0)
+ assert.Equal(t, actualTURN.HostConfig, expectedTRUNHost)
+ assert.Equal(t, len(resp.NetworkMap.OfflinePeers), 0)
+}
+
+func TestSyncThreePeers(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ peerKey, _ := wgtypes.GenerateKey()
+ peerKey1, _ := wgtypes.GenerateKey()
+ peerKey2, _ := wgtypes.GenerateKey()
+
+ loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client)
+ loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey1, ts.client)
+ loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey2, ts.client)
+
+ syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}
+ syncBytes, err := pb.Marshal(syncReq)
+ if err != nil {
+ t.Fatalf("failed to marshal sync request: %v", err)
+ }
+ encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey)
+ if err != nil {
+ t.Fatalf("failed to encrypt sync request: %v", err)
+ }
+
+ syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
+ WgPubKey: peerKey.PublicKey().String(),
+ Body: encryptedBytes,
+ })
+ if err != nil {
+ t.Fatalf("failed to call Sync: %v", err)
+ }
+
+ encryptedResponse := &mgmtProto.EncryptedMessage{}
+ err = syncStream.RecvMsg(encryptedResponse)
+ if err != nil {
+ t.Fatalf("failed to receive sync response: %v", err)
+ }
+
+ decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey)
+ if err != nil {
+ t.Fatalf("failed to decrypt sync response: %v", err)
+ }
+
+ resp := &mgmtProto.SyncResponse{}
+ err = pb.Unmarshal(decryptedBytes, resp)
+ if err != nil {
+ t.Fatalf("failed to unmarshal sync response: %v", err)
+ }
+
+ if len(resp.GetRemotePeers()) != 2 {
+ t.Fatalf("expected 2 remote peers, got %d", len(resp.GetRemotePeers()))
+ }
+
+ var found1, found2 bool
+ for _, rp := range resp.GetRemotePeers() {
+ if rp.WgPubKey == peerKey1.PublicKey().String() {
+ found1 = true
+ } else if rp.WgPubKey == peerKey2.PublicKey().String() {
+ found2 = true
+ }
+ }
+ if !found1 || !found2 {
+ t.Fatalf("did not find the expected peer keys %s, %s among %v",
+ peerKey1.PublicKey().String(),
+ peerKey2.PublicKey().String(),
+ resp.GetRemotePeers())
+ }
+}
+
+func TestSyncNewPeerUpdate(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ peerKey, _ := wgtypes.GenerateKey()
+ loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client)
+
+ syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}
+ syncBytes, err := pb.Marshal(syncReq)
+ if err != nil {
+ t.Fatalf("failed to marshal sync request: %v", err)
+ }
+
+ encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey)
+ if err != nil {
+ t.Fatalf("failed to encrypt sync request: %v", err)
+ }
+
+ syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
+ WgPubKey: peerKey.PublicKey().String(),
+ Body: encryptedBytes,
+ })
+ if err != nil {
+ t.Fatalf("failed to call Sync: %v", err)
+ }
+
+ encryptedResponse := &mgmtProto.EncryptedMessage{}
+ err = syncStream.RecvMsg(encryptedResponse)
+ if err != nil {
+ t.Fatalf("failed to receive first sync response: %v", err)
+ }
+
+ decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey)
+ if err != nil {
+ t.Fatalf("failed to decrypt first sync response: %v", err)
+ }
+
+ resp := &mgmtProto.SyncResponse{}
+ if err := pb.Unmarshal(decryptedBytes, resp); err != nil {
+ t.Fatalf("failed to unmarshal first sync response: %v", err)
+ }
+
+ if len(resp.GetRemotePeers()) != 0 {
+ t.Fatalf("expected 0 remote peers at first sync, got %d", len(resp.GetRemotePeers()))
+ }
+
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ encryptedResponse := &mgmtProto.EncryptedMessage{}
+ err = syncStream.RecvMsg(encryptedResponse)
+ if err != nil {
+ t.Errorf("failed to receive second sync response: %v", err)
+ return
+ }
+
+ decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey)
+ if err != nil {
+ t.Errorf("failed to decrypt second sync response: %v", err)
+ return
+ }
+ err = pb.Unmarshal(decryptedBytes, resp)
+ if err != nil {
+ t.Errorf("failed to unmarshal second sync response: %v", err)
+ return
+ }
+ }()
+
+ newPeerKey, _ := wgtypes.GenerateKey()
+ loginPeerWithValidSetupKey(t, ts.serverPubKey, newPeerKey, ts.client)
+
+ wg.Wait()
+
+ if len(resp.GetRemotePeers()) != 1 {
+ t.Fatalf("expected exactly 1 remote peer update, got %d", len(resp.GetRemotePeers()))
+ }
+ if resp.GetRemotePeers()[0].WgPubKey != newPeerKey.PublicKey().String() {
+ t.Fatalf("expected new peer key %s, got %s",
+ newPeerKey.PublicKey().String(),
+ resp.GetRemotePeers()[0].WgPubKey)
+ }
+}
+
+func TestGetServerKey(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{})
+ if err != nil {
+ t.Fatalf("GetServerKey returned error: %v", err)
+ }
+ if resp == nil {
+ t.Fatal("GetServerKey returned nil response")
+ }
+ if resp.Key == "" {
+ t.Fatal("GetServerKey returned empty key")
+ }
+ if resp.ExpiresAt.AsTime().IsZero() {
+ t.Fatal("GetServerKey returned 0 for ExpiresAt")
+ }
+
+ _, err = wgtypes.ParseKey(resp.Key)
+ if err != nil {
+ t.Fatalf("GetServerKey returned an invalid WG key: %v", err)
+ }
+}
+
+func TestLoginInvalidSetupKey(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ peerKey, _ := wgtypes.GenerateKey()
+ request := &mgmtProto.LoginRequest{
+ SetupKey: "invalid setup key",
+ Meta: &mgmtProto.PeerSystemMeta{},
+ }
+ encryptedMsg, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, request)
+ if err != nil {
+ t.Fatalf("failed to encrypt login request: %v", err)
+ }
+
+ resp, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{
+ WgPubKey: peerKey.PublicKey().String(),
+ Body: encryptedMsg,
+ })
+ if err == nil {
+ t.Fatal("expected error for invalid setup key but got nil")
+ }
+ if resp != nil {
+ t.Fatalf("expected nil response for invalid setup key but got: %+v", resp)
+ }
+}
+
+func TestLoginValidSetupKey(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ peerKey, _ := wgtypes.GenerateKey()
+ resp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client)
+ if resp == nil {
+ t.Fatal("loginPeerWithValidSetupKey returned nil, expected a valid response")
+ }
+}
+
+func TestLoginRegisteredPeer(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ peerKey, _ := wgtypes.GenerateKey()
+ regResp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client)
+ if regResp == nil {
+ t.Fatal("registration with valid setup key failed")
+ }
+
+ loginReq := &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}}
+ encryptedLogin, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, loginReq)
+ if err != nil {
+ t.Fatalf("failed to encrypt login request: %v", err)
+ }
+ loginRespEnc, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{
+ WgPubKey: peerKey.PublicKey().String(),
+ Body: encryptedLogin,
+ })
+ if err != nil {
+ t.Fatalf("login call returned an error: %v", err)
+ }
+
+ loginResp := &mgmtProto.LoginResponse{}
+ err = encryption.DecryptMessage(ts.serverPubKey, peerKey, loginRespEnc.Body, loginResp)
+ if err != nil {
+ t.Fatalf("failed to decrypt login response: %v", err)
+ }
+
+ expectedSignalConfig := &mgmtProto.HostConfig{
+ Uri: "signal.netbird.io:10000",
+ Protocol: mgmtProto.HostConfig_HTTP,
+ }
+ expectedStunsConfig := &mgmtProto.HostConfig{
+ Uri: "stun:stun.netbird.io:3468",
+ Protocol: mgmtProto.HostConfig_UDP,
+ }
+ expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{
+ HostConfig: &mgmtProto.HostConfig{
+ Uri: "turn:stun.netbird.io:3468",
+ Protocol: mgmtProto.HostConfig_UDP,
+ },
+ User: "some_user",
+ Password: "some_password",
+ }
+
+ assert.NotNil(t, loginResp.GetNetbirdConfig())
+ assert.Equal(t, loginResp.GetNetbirdConfig().Signal, expectedSignalConfig)
+ assert.Contains(t, loginResp.GetNetbirdConfig().Stuns, expectedStunsConfig)
+ assert.Contains(t, loginResp.GetNetbirdConfig().Turns, expectedTurnsConfig)
+}
+
+func TestSync10PeersGetUpdates(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ initialPeers := 10
+ additionalPeers := 10
+
+ var peers []wgtypes.Key
+ for i := 0; i < initialPeers; i++ {
+ key, _ := wgtypes.GenerateKey()
+ loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client)
+ peers = append(peers, key)
+ }
+
+ var wg sync.WaitGroup
+ wg.Add(initialPeers + initialPeers*additionalPeers)
+
+ var syncClients []mgmtProto.ManagementService_SyncClient
+ for _, pk := range peers {
+ syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}
+ msgBytes, err := pb.Marshal(syncReq)
+ if err != nil {
+ t.Fatalf("failed to marshal SyncRequest: %v", err)
+ }
+ encBytes, err := encryption.Encrypt(msgBytes, ts.serverPubKey, pk)
+ if err != nil {
+ t.Fatalf("failed to encrypt SyncRequest: %v", err)
+ }
+
+ s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
+ WgPubKey: pk.PublicKey().String(),
+ Body: encBytes,
+ })
+ if err != nil {
+ t.Fatalf("failed to call Sync for peer: %v", err)
+ }
+ syncClients = append(syncClients, s)
+
+ go func(pk wgtypes.Key, syncStream mgmtProto.ManagementService_SyncClient) {
+ for {
+ encMsg := &mgmtProto.EncryptedMessage{}
+ err := syncStream.RecvMsg(encMsg)
+ if err != nil {
+ return
+ }
+ decryptedBytes, decErr := encryption.Decrypt(encMsg.Body, ts.serverPubKey, pk)
+ if decErr != nil {
+ t.Errorf("failed to decrypt SyncResponse for peer %s: %v", pk.PublicKey().String(), decErr)
+ return
+ }
+ resp := &mgmtProto.SyncResponse{}
+ umErr := pb.Unmarshal(decryptedBytes, resp)
+ if umErr != nil {
+ t.Errorf("failed to unmarshal SyncResponse for peer %s: %v", pk.PublicKey().String(), umErr)
+ return
+ }
+ // We only count if there's a new peer update
+ if len(resp.GetRemotePeers()) > 0 {
+ wg.Done()
+ }
+ }
+ }(pk, s)
+ }
+
+ time.Sleep(500 * time.Millisecond)
+ for i := 0; i < additionalPeers; i++ {
+ key, _ := wgtypes.GenerateKey()
+ loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client)
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ n := r.Intn(200)
+ time.Sleep(time.Duration(n) * time.Millisecond)
+ }
+
+ wg.Wait()
+
+ for _, sc := range syncClients {
+ err := sc.CloseSend()
+ if err != nil {
+ t.Fatalf("failed to close sync client: %v", err)
+ }
+ }
+}
+
+func TestConcurrentPeersNoDuplicateIPs(t *testing.T) {
+ ts := setupTest(t)
+ defer tearDownTest(t, ts)
+
+ initialPeers := 30
+ ipChan := make(chan string, initialPeers)
+
+ var wg sync.WaitGroup
+ wg.Add(initialPeers)
+
+ for i := 0; i < initialPeers; i++ {
+ go func() {
+ defer wg.Done()
+ key, _ := wgtypes.GenerateKey()
+ loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client)
+
+ syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}
+ encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, key, syncReq)
+ if err != nil {
+ t.Errorf("failed to encrypt sync request: %v", err)
+ return
+ }
+
+ s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{
+ WgPubKey: key.PublicKey().String(),
+ Body: encryptedBytes,
+ })
+ if err != nil {
+ t.Errorf("failed to call Sync: %v", err)
+ return
+ }
+
+ encResp := &mgmtProto.EncryptedMessage{}
+ if err = s.RecvMsg(encResp); err != nil {
+ t.Errorf("failed to receive sync response: %v", err)
+ return
+ }
+
+ resp := &mgmtProto.SyncResponse{}
+ if err = encryption.DecryptMessage(ts.serverPubKey, key, encResp.Body, resp); err != nil {
+ t.Errorf("failed to decrypt sync response: %v", err)
+ return
+ }
+ ipChan <- resp.GetPeerConfig().Address
+ }()
+ }
+
+ wg.Wait()
+ close(ipChan)
+
+ ipMap := make(map[string]bool)
+ for ip := range ipChan {
+ if ipMap[ip] {
+ t.Fatalf("found duplicate IP: %s", ip)
+ }
+ ipMap[ip] = true
+ }
+
+ // Ensure we collected all peers
+ if len(ipMap) != initialPeers {
+ t.Fatalf("expected %d unique IPs, got %d", initialPeers, len(ipMap))
+ }
+}
diff --git a/management/server/migration/migration.go b/management/server/migration/migration.go
index 8986d77b5..d7abbad47 100644
--- a/management/server/migration/migration.go
+++ b/management/server/migration/migration.go
@@ -330,10 +330,7 @@ func MigrateNewField[T any](ctx context.Context, db *gorm.DB, columnName string,
}
var rows []map[string]any
- if err := tx.Table(tableName).
- Select("id", columnName).
- Where(columnName + " IS NULL OR " + columnName + " = ''").
- Find(&rows).Error; err != nil {
+ if err := tx.Table(tableName).Select("id", columnName).Where(columnName + " IS NULL").Find(&rows).Error; err != nil {
return fmt.Errorf("failed to find rows with empty %s: %w", columnName, err)
}
diff --git a/management/server/migration/migration_test.go b/management/server/migration/migration_test.go
index a645ae325..e907d6853 100644
--- a/management/server/migration/migration_test.go
+++ b/management/server/migration/migration_test.go
@@ -21,9 +21,7 @@ import (
func setupDatabase(t *testing.T) *gorm.DB {
t.Helper()
- db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{
- PrepareStmt: true,
- })
+ db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
require.NoError(t, err, "Failed to open database")
return db
diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go
index bcb7f0642..67c23b95d 100644
--- a/management/server/mock_server/account_mock.go
+++ b/management/server/mock_server/account_mock.go
@@ -13,23 +13,25 @@ import (
"github.com/netbirdio/netbird/management/domain"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity"
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/idp"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/route"
)
+var _ server.AccountManager = (*MockAccountManager)(nil)
+
type MockAccountManager struct {
GetOrCreateAccountByUserFunc func(ctx context.Context, userId, domain string) (*types.Account, error)
GetAccountFunc func(ctx context.Context, accountID string) (*types.Account, error)
CreateSetupKeyFunc func(ctx context.Context, accountId string, keyName string, keyType types.SetupKeyType,
- expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error)
+ expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error)
GetSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) (*types.SetupKey, error)
AccountExistsFunc func(ctx context.Context, accountID string) (bool, error)
GetAccountIDByUserIdFunc func(ctx context.Context, userId, domain string) (string, error)
- GetUserFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error)
+ GetUserFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error)
ListUsersFunc func(ctx context.Context, accountID string) ([]*types.User, error)
GetPeersFunc func(ctx context.Context, accountID, userID string) ([]*nbpeer.Peer, error)
MarkPeerConnectedFunc func(ctx context.Context, peerKey string, connected bool, realIP net.IP) error
@@ -53,9 +55,7 @@ type MockAccountManager struct {
SavePolicyFunc func(ctx context.Context, accountID, userID string, policy *types.Policy) (*types.Policy, error)
DeletePolicyFunc func(ctx context.Context, accountID, policyID, userID string) error
ListPoliciesFunc func(ctx context.Context, accountID, userID string) ([]*types.Policy, error)
- GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error)
- GetAccountInfoFromPATFunc func(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, string, string, error)
- MarkPATUsedFunc func(ctx context.Context, pat string) error
+ GetUsersFromAccountFunc func(ctx context.Context, accountID, userID string) (map[string]*types.UserInfo, error)
UpdatePeerMetaFunc func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error
UpdatePeerFunc func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error)
CreateRouteFunc func(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peer string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error)
@@ -69,7 +69,7 @@ type MockAccountManager struct {
SaveOrAddUserFunc func(ctx context.Context, accountID, userID string, user *types.User, addIfNotExists bool) (*types.UserInfo, error)
SaveOrAddUsersFunc func(ctx context.Context, accountID, initiatorUserID string, update []*types.User, addIfNotExists bool) ([]*types.UserInfo, error)
DeleteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserID string) error
- DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error
+ DeleteRegularUsersFunc func(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error
CreatePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error)
DeletePATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) error
GetPATFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserId string, tokenID string) (*types.PersonalAccessToken, error)
@@ -80,8 +80,7 @@ type MockAccountManager struct {
DeleteNameServerGroupFunc func(ctx context.Context, accountID, nsGroupID, userID string) error
ListNameServerGroupsFunc func(ctx context.Context, accountID string, userID string) ([]*nbdns.NameServerGroup, error)
CreateUserFunc func(ctx context.Context, accountID, userID string, key *types.UserInfo) (*types.UserInfo, error)
- GetAccountIDFromTokenFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error)
- CheckUserAccessByJWTGroupsFunc func(ctx context.Context, claims jwtclaims.AuthorizationClaims) error
+ GetAccountIDFromUserAuthFunc func(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error)
DeleteAccountFunc func(ctx context.Context, accountID, userID string) error
GetDNSDomainFunc func() string
StoreEventFunc func(ctx context.Context, initiatorID, targetID, accountID string, activityID activity.ActivityDescriber, meta map[string]any)
@@ -110,6 +109,7 @@ type MockAccountManager struct {
GetUserByIDFunc func(ctx context.Context, id string) (*types.User, error)
GetAccountSettingsFunc func(ctx context.Context, accountID string, userID string) (*types.Settings, error)
DeleteSetupKeyFunc func(ctx context.Context, accountID, userID, keyID string) error
+ BuildUserInfosForAccountFunc func(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error)
}
func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) {
@@ -165,7 +165,7 @@ func (am *MockAccountManager) GetAllGroups(ctx context.Context, accountID, userI
}
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
-func (am *MockAccountManager) GetUsersFromAccount(ctx context.Context, accountID string, userID string) ([]*types.UserInfo, error) {
+func (am *MockAccountManager) GetUsersFromAccount(ctx context.Context, accountID string, userID string) (map[string]*types.UserInfo, error) {
if am.GetUsersFromAccountFunc != nil {
return am.GetUsersFromAccountFunc(ctx, accountID, userID)
}
@@ -204,9 +204,10 @@ func (am *MockAccountManager) CreateSetupKey(
usageLimit int,
userID string,
ephemeral bool,
+ allowExtraDNSLabels bool,
) (*types.SetupKey, error) {
if am.CreateSetupKeyFunc != nil {
- return am.CreateSetupKeyFunc(ctx, accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID, ephemeral)
+ return am.CreateSetupKeyFunc(ctx, accountID, keyName, keyType, expiresIn, autoGroups, usageLimit, userID, ephemeral, allowExtraDNSLabels)
}
return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented")
}
@@ -238,14 +239,6 @@ func (am *MockAccountManager) MarkPeerConnected(ctx context.Context, peerKey str
return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented")
}
-// GetAccountInfoFromPAT mock implementation of GetAccountInfoFromPAT from server.AccountManager interface
-func (am *MockAccountManager) GetAccountInfoFromPAT(ctx context.Context, pat string) (*types.User, *types.PersonalAccessToken, string, string, error) {
- if am.GetAccountInfoFromPATFunc != nil {
- return am.GetAccountInfoFromPATFunc(ctx, pat)
- }
- return nil, nil, "", "", status.Errorf(codes.Unimplemented, "method GetAccountInfoFromPAT is not implemented")
-}
-
// DeleteAccount mock implementation of DeleteAccount from server.AccountManager interface
func (am *MockAccountManager) DeleteAccount(ctx context.Context, accountID, userID string) error {
if am.DeleteAccountFunc != nil {
@@ -254,14 +247,6 @@ func (am *MockAccountManager) DeleteAccount(ctx context.Context, accountID, user
return status.Errorf(codes.Unimplemented, "method DeleteAccount is not implemented")
}
-// MarkPATUsed mock implementation of MarkPATUsed from server.AccountManager interface
-func (am *MockAccountManager) MarkPATUsed(ctx context.Context, pat string) error {
- if am.MarkPATUsedFunc != nil {
- return am.MarkPATUsedFunc(ctx, pat)
- }
- return status.Errorf(codes.Unimplemented, "method MarkPATUsed is not implemented")
-}
-
// CreatePAT mock implementation of GetPAT from server.AccountManager interface
func (am *MockAccountManager) CreatePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, name string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) {
if am.CreatePATFunc != nil {
@@ -428,11 +413,11 @@ func (am *MockAccountManager) UpdatePeerMeta(ctx context.Context, peerID string,
}
// GetUser mock implementation of GetUser from server.AccountManager interface
-func (am *MockAccountManager) GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) {
- if am.GetUserFunc != nil {
- return am.GetUserFunc(ctx, claims)
+func (am *MockAccountManager) GetUserFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (*types.User, error) {
+ if am.GetUserFromUserAuthFunc != nil {
+ return am.GetUserFromUserAuthFunc(ctx, userAuth)
}
- return nil, status.Errorf(codes.Unimplemented, "method GetUser is not implemented")
+ return nil, status.Errorf(codes.Unimplemented, "method GetUserFromUserAuth is not implemented")
}
func (am *MockAccountManager) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) {
@@ -550,9 +535,9 @@ func (am *MockAccountManager) DeleteUser(ctx context.Context, accountID string,
}
// DeleteRegularUsers mocks DeleteRegularUsers of the AccountManager interface
-func (am *MockAccountManager) DeleteRegularUsers(ctx context.Context, accountID string, initiatorUserID string, targetUserIDs []string) error {
+func (am *MockAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error {
if am.DeleteRegularUsersFunc != nil {
- return am.DeleteRegularUsersFunc(ctx, accountID, initiatorUserID, targetUserIDs)
+ return am.DeleteRegularUsersFunc(ctx, accountID, initiatorUserID, targetUserIDs, userInfos)
}
return status.Errorf(codes.Unimplemented, "method DeleteRegularUsers is not implemented")
}
@@ -612,19 +597,11 @@ func (am *MockAccountManager) CreateUser(ctx context.Context, accountID, userID
return nil, status.Errorf(codes.Unimplemented, "method CreateUser is not implemented")
}
-// GetAccountIDFromToken mocks GetAccountIDFromToken of the AccountManager interface
-func (am *MockAccountManager) GetAccountIDFromToken(ctx context.Context, claims jwtclaims.AuthorizationClaims) (string, string, error) {
- if am.GetAccountIDFromTokenFunc != nil {
- return am.GetAccountIDFromTokenFunc(ctx, claims)
+func (am *MockAccountManager) GetAccountIDFromUserAuth(ctx context.Context, userAuth nbcontext.UserAuth) (string, string, error) {
+ if am.GetAccountIDFromUserAuthFunc != nil {
+ return am.GetAccountIDFromUserAuthFunc(ctx, userAuth)
}
- return "", "", status.Errorf(codes.Unimplemented, "method GetAccountIDFromToken is not implemented")
-}
-
-func (am *MockAccountManager) CheckUserAccessByJWTGroups(ctx context.Context, claims jwtclaims.AuthorizationClaims) error {
- if am.CheckUserAccessByJWTGroupsFunc != nil {
- return am.CheckUserAccessByJWTGroupsFunc(ctx, claims)
- }
- return status.Errorf(codes.Unimplemented, "method CheckUserAccessByJWTGroups is not implemented")
+ return "", "", status.Errorf(codes.Unimplemented, "method GetAccountIDFromUserAuth is not implemented")
}
// GetPeers mocks GetPeers of the AccountManager interface
@@ -849,3 +826,15 @@ func (am *MockAccountManager) GetPeerGroups(ctx context.Context, accountID, peer
}
return nil, status.Errorf(codes.Unimplemented, "method GetPeerGroups is not implemented")
}
+
+// BuildUserInfosForAccount mocks BuildUserInfosForAccount of the AccountManager interface
+func (am *MockAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) {
+ if am.BuildUserInfosForAccountFunc != nil {
+ return am.BuildUserInfosForAccountFunc(ctx, accountID, initiatorUserID, accountUsers)
+ }
+ return nil, status.Errorf(codes.Unimplemented, "method BuildUserInfosForAccount is not implemented")
+}
+
+func (am *MockAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error {
+ return status.Errorf(codes.Unimplemented, "method SyncUserJWTGroups is not implemented")
+}
diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go
index 0743db513..497d9af4f 100644
--- a/management/server/nameserver_test.go
+++ b/management/server/nameserver_test.go
@@ -379,12 +379,12 @@ func TestCreateNameServerGroup(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
am, err := createNSManager(t)
if err != nil {
- t.Error("failed to create account manager")
+ t.Fatalf("failed to create account manager: %s", err)
}
account, err := initTestNSAccount(t, am)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
outNSGroup, err := am.CreateNameServerGroup(
@@ -607,12 +607,12 @@ func TestSaveNameServerGroup(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
am, err := createNSManager(t)
if err != nil {
- t.Error("failed to create account manager")
+ t.Fatalf("failed to create account manager: %s", err)
}
account, err := initTestNSAccount(t, am)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
account.NameServerGroups[testCase.existingNSGroup.ID] = testCase.existingNSGroup
@@ -706,7 +706,7 @@ func TestDeleteNameServerGroup(t *testing.T) {
account, err := initTestNSAccount(t, am)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
account.NameServerGroups[testingNSGroup.ID] = testingNSGroup
@@ -741,7 +741,7 @@ func TestGetNameServerGroup(t *testing.T) {
account, err := initTestNSAccount(t, am)
if err != nil {
- t.Error("failed to init testing account")
+ t.Fatalf("failed to init testing account: %s", err)
}
foundGroup, err := am.GetNameServerGroup(context.Background(), account.Id, testUserID, existingNSGroupID)
@@ -761,6 +761,7 @@ func TestGetNameServerGroup(t *testing.T) {
func createNSManager(t *testing.T) (*DefaultAccountManager, error) {
t.Helper()
+
store, err := createNSStore(t)
if err != nil {
return nil, err
diff --git a/management/server/networks/resources/manager.go b/management/server/networks/resources/manager.go
index 725d15496..5b542d886 100644
--- a/management/server/networks/resources/manager.go
+++ b/management/server/networks/resources/manager.go
@@ -113,7 +113,7 @@ func (m *managerImpl) CreateResource(ctx context.Context, userID string, resourc
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
_, err = transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name)
if err == nil {
- return errors.New("resource already exists")
+ return status.Errorf(status.InvalidArgument, "resource with name %s already exists", resource.Name)
}
network, err := transaction.GetNetworkByID(ctx, store.LockingStrengthUpdate, resource.AccountID, resource.NetworkID)
@@ -223,7 +223,7 @@ func (m *managerImpl) UpdateResource(ctx context.Context, userID string, resourc
oldResource, err := transaction.GetNetworkResourceByName(ctx, store.LockingStrengthShare, resource.AccountID, resource.Name)
if err == nil && oldResource.ID != resource.ID {
- return errors.New("new resource name already exists")
+ return status.Errorf(status.InvalidArgument, "new resource name already exists")
}
oldResource, err = transaction.GetNetworkResourceByID(ctx, store.LockingStrengthShare, resource.AccountID, resource.ID)
diff --git a/management/server/peer.go b/management/server/peer.go
index f48ba2d15..c9b0fcfee 100644
--- a/management/server/peer.go
+++ b/management/server/peer.go
@@ -11,11 +11,13 @@ import (
"sync"
"time"
- "github.com/netbirdio/netbird/management/server/geolocation"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
+ "github.com/netbirdio/netbird/management/domain"
+ "github.com/netbirdio/netbird/management/server/geolocation"
+
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/store"
@@ -52,6 +54,9 @@ type PeerLogin struct {
SetupKey string
// ConnectionIP is the real IP of the peer
ConnectionIP net.IP
+
+ // ExtraDNSLabels is a list of extra DNS labels that the peer wants to use
+ ExtraDNSLabels []string
}
// GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if
@@ -119,6 +124,11 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
// MarkPeerConnected marks peer as connected (true) or disconnected (false)
func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubKey string, connected bool, realIP net.IP, accountID string) error {
+ start := time.Now()
+ defer func() {
+ log.WithContext(ctx).Debugf("MarkPeerConnected: took %v", time.Since(start))
+ }()
+
var peer *nbpeer.Peer
var settings *types.Settings
var expired bool
@@ -130,11 +140,6 @@ func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubK
return err
}
- settings, err = transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
- if err != nil {
- return err
- }
-
expired, err = updatePeerStatusAndLocation(ctx, am.geo, transaction, peer, connected, realIP, accountID)
return err
})
@@ -143,6 +148,11 @@ func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubK
}
if peer.AddedWithSSOLogin() {
+ settings, err = am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+ if err != nil {
+ return err
+ }
+
if peer.LoginExpirationEnabled && settings.PeerLoginExpirationEnabled {
am.checkAndSchedulePeerLoginExpiration(ctx, accountID)
}
@@ -374,6 +384,19 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer
return err
}
+ groups, err := transaction.GetPeerGroups(ctx, store.LockingStrengthUpdate, accountID, peerID)
+ if err != nil {
+ return fmt.Errorf("failed to get peer groups: %w", err)
+ }
+
+ for _, group := range groups {
+ group.RemovePeer(peerID)
+ err = transaction.SaveGroup(ctx, store.LockingStrengthUpdate, group)
+ if err != nil {
+ return fmt.Errorf("failed to save group: %w", err)
+ }
+ }
+
eventsToStore, err = deletePeers(ctx, am, transaction, accountID, userID, []*nbpeer.Peer{peer})
return err
})
@@ -483,6 +506,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
var setupKeyName string
var ephemeral bool
var groupsToAdd []string
+ var allowExtraDNSLabels bool
if addedByUser {
user, err := transaction.GetUserByUserID(ctx, store.LockingStrengthUpdate, userID)
if err != nil {
@@ -508,6 +532,11 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
ephemeral = sk.Ephemeral
setupKeyID = sk.Id
setupKeyName = sk.Name
+ allowExtraDNSLabels = sk.AllowExtraDNSLabels
+
+ if !sk.AllowExtraDNSLabels && len(peer.ExtraDNSLabels) > 0 {
+ return status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key doesn't allow extra DNS labels")
+ }
}
if (strings.ToLower(peer.Meta.Hostname) == "iphone" || strings.ToLower(peer.Meta.Hostname) == "ipad") && userID != "" {
@@ -548,6 +577,8 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
Ephemeral: ephemeral,
Location: peer.Location,
InactivityExpirationEnabled: addedByUser,
+ ExtraDNSLabels: peer.ExtraDNSLabels,
+ AllowExtraDNSLabels: allowExtraDNSLabels,
}
opEvent.TargetID = newPeer.ID
opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
@@ -658,6 +689,11 @@ func getFreeIP(ctx context.Context, transaction store.Store, accountID string) (
// SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible
func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) {
+ start := time.Now()
+ defer func() {
+ log.WithContext(ctx).Debugf("SyncPeer: took %v", time.Since(start))
+ }()
+
var peer *nbpeer.Peer
var peerNotValid bool
var isStatusChanged bool
@@ -665,6 +701,11 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, ac
var err error
var postureChecks []*posture.Checks
+ settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
peer, err = transaction.GetPeerByPeerPubKey(ctx, store.LockingStrengthUpdate, sync.WireGuardPubKey)
if err != nil {
@@ -682,11 +723,6 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, ac
}
}
- settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
- if err != nil {
- return err
- }
-
if peerLoginExpired(ctx, peer, settings) {
return status.NewPeerLoginExpiredError()
}
@@ -779,16 +815,17 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login PeerLogin)
var isPeerUpdated bool
var postureChecks []*posture.Checks
+ settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
peer, err = transaction.GetPeerByPeerPubKey(ctx, store.LockingStrengthUpdate, login.WireGuardPubKey)
if err != nil {
return err
}
- settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
- if err != nil {
- return err
- }
// this flag prevents unnecessary calls to the persistent store.
shouldStorePeer := false
@@ -835,6 +872,20 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login PeerLogin)
shouldStorePeer = true
}
+ if !peer.AllowExtraDNSLabels && len(login.ExtraDNSLabels) > 0 {
+ return status.Errorf(status.PreconditionFailed, "couldn't login peer: setup key doesn't allow extra DNS labels")
+ }
+
+ extraLabels, err := domain.ValidateDomainsStrSlice(login.ExtraDNSLabels)
+ if err != nil {
+ return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err)
+ }
+
+ if !slices.Equal(peer.ExtraDNSLabels, extraLabels) {
+ peer.ExtraDNSLabels = extraLabels
+ shouldStorePeer = true
+ }
+
if shouldStorePeer {
if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil {
return err
@@ -947,6 +998,11 @@ func (am *DefaultAccountManager) checkIFPeerNeedsLoginWithoutLock(ctx context.Co
}
func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) {
+ start := time.Now()
+ defer func() {
+ log.WithContext(ctx).Debugf("getValidatedPeerWithMap: took %s", time.Since(start))
+ }()
+
if isRequiresApproval {
network, err := am.Store.GetAccountNetwork(ctx, store.LockingStrengthShare, accountID)
if err != nil {
@@ -959,7 +1015,7 @@ func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, is
return peer, emptyMap, nil, nil
}
- account, err := am.Store.GetAccount(ctx, accountID)
+ account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
if err != nil {
return nil, nil, nil, err
}
@@ -1100,11 +1156,6 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account
}
start := time.Now()
- defer func() {
- if am.metrics != nil {
- am.metrics.AccountManagerMetrics().CountUpdateAccountPeersDuration(time.Since(start))
- }
- }()
approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
if err != nil {
@@ -1145,6 +1196,9 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account
}
wg.Wait()
+ if am.metrics != nil {
+ am.metrics.AccountManagerMetrics().CountUpdateAccountPeersDuration(time.Since(start))
+ }
}
// UpdateAccountPeer updates a single peer that belongs to an account.
diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go
index 199c7c89d..afda55d17 100644
--- a/management/server/peer/peer.go
+++ b/management/server/peer/peer.go
@@ -49,6 +49,11 @@ type Peer struct {
Ephemeral bool `gorm:"index"`
// Geo location based on connection IP
Location Location `gorm:"embedded;embeddedPrefix:location_"`
+
+ // ExtraDNSLabels is a list of additional DNS labels that can be used to resolve the peer
+ ExtraDNSLabels []string `gorm:"serializer:json"`
+ // AllowExtraDNSLabels indicates whether the peer allows extra DNS labels to be used for resolving the peer
+ AllowExtraDNSLabels bool
}
type PeerStatus struct { //nolint:revive
@@ -202,6 +207,8 @@ func (p *Peer) Copy() *Peer {
Ephemeral: p.Ephemeral,
Location: p.Location,
InactivityExpirationEnabled: p.InactivityExpirationEnabled,
+ ExtraDNSLabels: slices.Clone(p.ExtraDNSLabels),
+ AllowExtraDNSLabels: p.AllowExtraDNSLabels,
}
}
diff --git a/management/server/peer_test.go b/management/server/peer_test.go
index 0ecd635ba..9deb8e456 100644
--- a/management/server/peer_test.go
+++ b/management/server/peer_test.go
@@ -168,7 +168,7 @@ func TestAccountManager_GetNetworkMap(t *testing.T) {
t.Fatal(err)
}
- setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false)
+ setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false, false)
if err != nil {
t.Fatal("error creating setup key")
return
@@ -417,7 +417,7 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) {
t.Fatal(err)
}
- setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false)
+ setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userId, false, false)
if err != nil {
t.Fatal("error creating setup key")
return
@@ -489,7 +489,7 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
}
// two peers one added by a regular user and one with a setup key
- setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, adminUser, false)
+ setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, adminUser, false, false)
if err != nil {
t.Fatal("error creating setup key")
return
@@ -1099,13 +1099,13 @@ func TestToSyncResponse(t *testing.T) {
assert.Equal(t, "192.168.1.1/24", response.PeerConfig.Address)
assert.Equal(t, "peer1.example.com", response.PeerConfig.Fqdn)
assert.Equal(t, true, response.PeerConfig.SshConfig.SshEnabled)
- // assert wiretrustee config
- assert.Equal(t, "signal.uri", response.WiretrusteeConfig.Signal.Uri)
- assert.Equal(t, proto.HostConfig_HTTPS, response.WiretrusteeConfig.Signal.GetProtocol())
- assert.Equal(t, "stun.uri", response.WiretrusteeConfig.Stuns[0].Uri)
- assert.Equal(t, "turn.uri", response.WiretrusteeConfig.Turns[0].HostConfig.GetUri())
- assert.Equal(t, "turn-user", response.WiretrusteeConfig.Turns[0].User)
- assert.Equal(t, "turn-pass", response.WiretrusteeConfig.Turns[0].Password)
+ // assert netbird config
+ assert.Equal(t, "signal.uri", response.NetbirdConfig.Signal.Uri)
+ assert.Equal(t, proto.HostConfig_HTTPS, response.NetbirdConfig.Signal.GetProtocol())
+ assert.Equal(t, "stun.uri", response.NetbirdConfig.Stuns[0].Uri)
+ assert.Equal(t, "turn.uri", response.NetbirdConfig.Turns[0].HostConfig.GetUri())
+ assert.Equal(t, "turn-user", response.NetbirdConfig.Turns[0].User)
+ assert.Equal(t, "turn-pass", response.NetbirdConfig.Turns[0].Password)
// assert RemotePeers
assert.Equal(t, 1, len(response.RemotePeers))
assert.Equal(t, "192.168.1.2/32", response.RemotePeers[0].AllowedIps[0])
@@ -1729,3 +1729,52 @@ func TestPeerAccountPeersUpdate(t *testing.T) {
}
})
}
+
+func Test_DeletePeer(t *testing.T) {
+ manager, err := createManager(t)
+ if err != nil {
+ t.Fatal(err)
+ return
+ }
+
+ // account with an admin and a regular user
+ accountID := "test_account"
+ adminUser := "account_creator"
+ account := newAccountWithId(context.Background(), accountID, adminUser, "")
+ account.Peers = map[string]*nbpeer.Peer{
+ "peer1": {
+ ID: "peer1",
+ AccountID: accountID,
+ },
+ "peer2": {
+ ID: "peer2",
+ AccountID: accountID,
+ },
+ }
+ account.Groups = map[string]*types.Group{
+ "group1": {
+ ID: "group1",
+ Name: "Group1",
+ Peers: []string{"peer1", "peer2"},
+ },
+ }
+
+ err = manager.Store.SaveAccount(context.Background(), account)
+ if err != nil {
+ t.Fatal(err)
+ return
+ }
+
+ err = manager.DeletePeer(context.Background(), accountID, "peer1", adminUser)
+ if err != nil {
+ t.Fatalf("DeletePeer failed: %v", err)
+ }
+
+ _, err = manager.GetPeer(context.Background(), accountID, "peer1", adminUser)
+ assert.Error(t, err)
+
+ group, err := manager.GetGroup(context.Background(), accountID, "group1", adminUser)
+ assert.NoError(t, err)
+ assert.NotContains(t, group.Peers, "peer1")
+
+}
diff --git a/management/server/route_test.go b/management/server/route_test.go
index 1c5c56f60..40e0f41b0 100644
--- a/management/server/route_test.go
+++ b/management/server/route_test.go
@@ -13,12 +13,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/netbirdio/netbird/management/domain"
+ "github.com/netbirdio/netbird/management/server/activity"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
-
- "github.com/netbirdio/netbird/management/domain"
- "github.com/netbirdio/netbird/management/server/activity"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/telemetry"
diff --git a/management/server/setupkey.go b/management/server/setupkey.go
index f2f1aad45..b0bdad4e5 100644
--- a/management/server/setupkey.go
+++ b/management/server/setupkey.go
@@ -52,7 +52,7 @@ type SetupKeyUpdateOperation struct {
// CreateSetupKey generates a new setup key with a given name, type, list of groups IDs to auto-assign to peers registered with this key,
// and adds it to the specified account. A list of autoGroups IDs can be empty.
func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType,
- expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool) (*types.SetupKey, error) {
+ expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error) {
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
defer unlock()
@@ -78,7 +78,7 @@ func (am *DefaultAccountManager) CreateSetupKey(ctx context.Context, accountID s
return status.Errorf(status.InvalidArgument, "invalid auto groups: %v", err)
}
- setupKey, plainKey = types.GenerateSetupKey(keyName, keyType, expiresIn, autoGroups, usageLimit, ephemeral)
+ setupKey, plainKey = types.GenerateSetupKey(keyName, keyType, expiresIn, autoGroups, usageLimit, ephemeral, allowExtraDNSLabels)
setupKey.AccountID = accountID
events := am.prepareSetupKeyEvents(ctx, transaction, accountID, userID, autoGroups, nil, setupKey)
diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go
index e225ec54b..6e1e1cf7d 100644
--- a/management/server/setupkey_test.go
+++ b/management/server/setupkey_test.go
@@ -50,7 +50,7 @@ func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
keyName := "my-test-key"
key, err := manager.CreateSetupKey(context.Background(), account.Id, keyName, types.SetupKeyReusable, expiresIn, []string{},
- types.SetupKeyUnlimitedUsage, userID, false)
+ types.SetupKeyUnlimitedUsage, userID, false, false)
if err != nil {
t.Fatal(err)
}
@@ -168,7 +168,7 @@ func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
for _, tCase := range []testCase{testCase1, testCase2, testCase3} {
t.Run(tCase.name, func(t *testing.T) {
key, err := manager.CreateSetupKey(context.Background(), account.Id, tCase.expectedKeyName, types.SetupKeyReusable, expiresIn,
- tCase.expectedGroups, types.SetupKeyUnlimitedUsage, userID, false)
+ tCase.expectedGroups, types.SetupKeyUnlimitedUsage, userID, false, false)
if tCase.expectedFailure {
if err == nil {
@@ -210,7 +210,7 @@ func TestGetSetupKeys(t *testing.T) {
t.Fatal(err)
}
- plainKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false)
+ plainKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false, false)
if err != nil {
t.Fatal(err)
}
@@ -275,7 +275,7 @@ func TestGenerateSetupKey(t *testing.T) {
expectedUpdatedAt := time.Now().UTC()
var expectedAutoGroups []string
- key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
+ key, plain := types.GenerateSetupKey(expectedName, types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt,
expectedExpiresAt, strconv.Itoa(int(types.Hash(plain))), expectedUpdatedAt, expectedAutoGroups, true)
@@ -283,33 +283,33 @@ func TestGenerateSetupKey(t *testing.T) {
}
func TestSetupKey_IsValid(t *testing.T) {
- validKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
+ validKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
if !validKey.IsValid() {
t.Errorf("expected key to be valid, got invalid %v", validKey)
}
// expired
- expiredKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, -time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
+ expiredKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, -time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
if expiredKey.IsValid() {
t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey)
}
// revoked
- revokedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
+ revokedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
revokedKey.Revoked = true
if revokedKey.IsValid() {
t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey)
}
// overused
- overUsedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
+ overUsedKey, _ := types.GenerateSetupKey("invalid key", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
overUsedKey.UsedTimes = 1
if overUsedKey.IsValid() {
t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey)
}
// overused
- reusableKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyReusable, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
+ reusableKey, _ := types.GenerateSetupKey("valid key", types.SetupKeyReusable, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
reusableKey.UsedTimes = 99
if !reusableKey.IsValid() {
t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey)
@@ -388,7 +388,7 @@ func isValidBase64SHA256(encodedKey string) bool {
func TestSetupKey_Copy(t *testing.T) {
- key, _ := types.GenerateSetupKey("key name", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false)
+ key, _ := types.GenerateSetupKey("key name", types.SetupKeyOneOff, time.Hour, []string{}, types.SetupKeyUnlimitedUsage, false, false)
keyCopy := key.Copy()
assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.GetExpiresAt(), key.Id,
@@ -436,7 +436,7 @@ func TestSetupKeyAccountPeersUpdate(t *testing.T) {
close(done)
}()
- setupKey, err = manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, 999, userID, false)
+ setupKey, err = manager.CreateSetupKey(context.Background(), account.Id, "key1", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
assert.NoError(t, err)
select {
@@ -477,7 +477,7 @@ func TestDefaultAccountManager_CreateSetupKey_ShouldNotAllowToUpdateRevokedKey(t
t.Fatal(err)
}
- key, err := manager.CreateSetupKey(context.Background(), account.Id, "testName", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false)
+ key, err := manager.CreateSetupKey(context.Background(), account.Id, "testName", types.SetupKeyReusable, time.Hour, nil, types.SetupKeyUnlimitedUsage, userID, false, false)
assert.NoError(t, err)
// revoke the key
diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go
index 7f0b3979e..43ad80023 100644
--- a/management/server/store/sql_store.go
+++ b/management/server/store/sql_store.go
@@ -15,7 +15,6 @@ import (
"sync"
"time"
- "github.com/netbirdio/netbird/management/server/util"
log "github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
@@ -34,6 +33,7 @@ import (
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/netbirdio/netbird/management/server/types"
+ "github.com/netbirdio/netbird/management/server/util"
"github.com/netbirdio/netbird/route"
)
@@ -420,7 +420,7 @@ func (s *SqlStore) SaveUsers(ctx context.Context, lockStrength LockingStrength,
return nil
}
- result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&users)
+ result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&users)
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to save users to store: %s", result.Error)
return status.Errorf(status.Internal, "failed to save users to store")
@@ -444,7 +444,7 @@ func (s *SqlStore) SaveGroups(ctx context.Context, lockStrength LockingStrength,
return nil
}
- result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Save(&groups)
+ result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&groups)
if result.Error != nil {
return status.Errorf(status.Internal, "failed to save groups to store: %v", result.Error)
}
@@ -615,6 +615,16 @@ func (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingSt
return groups, nil
}
+func (s *SqlStore) GetAccountsCounter(ctx context.Context) (int64, error) {
+ var count int64
+ result := s.db.Model(&types.Account{}).Count(&count)
+ if result.Error != nil {
+ return 0, fmt.Errorf("failed to get all accounts counter: %w", result.Error)
+ }
+
+ return count, nil
+}
+
func (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) {
var accounts []types.Account
result := s.db.Find(&accounts)
@@ -1001,7 +1011,6 @@ func getGormConfig() *gorm.Config {
return &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
CreateBatchSize: 400,
- PrepareStmt: true,
}
}
@@ -1036,6 +1045,13 @@ func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, data
}
for _, account := range fileStore.GetAllAccounts(ctx) {
+ _, err = account.GetGroupAll()
+ if err != nil {
+ if err := account.AddAllGroup(); err != nil {
+ return nil, err
+ }
+ }
+
err := store.SaveAccount(ctx, account)
if err != nil {
return nil, err
diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go
index 7efc68745..46ef0eaa3 100644
--- a/management/server/store/sql_store_test.go
+++ b/management/server/store/sql_store_test.go
@@ -10,17 +10,19 @@ import (
"net/netip"
"os"
"runtime"
+ "sync"
"testing"
"time"
"github.com/google/uuid"
- "github.com/netbirdio/netbird/management/server/util"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
nbdns "github.com/netbirdio/netbird/dns"
+ "github.com/netbirdio/netbird/management/server/util"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
@@ -35,40 +37,44 @@ import (
nbroute "github.com/netbirdio/netbird/route"
)
-func TestSqlite_NewStore(t *testing.T) {
+func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) {
+ t.Helper()
+ for _, engine := range supportedEngines {
+ if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) {
+ continue
+ }
+ t.Setenv("NETBIRD_STORE_ENGINE", string(engine))
+ store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir())
+ t.Cleanup(cleanUp)
+ assert.NoError(t, err)
+ t.Run(string(engine), func(t *testing.T) {
+ f(t, store)
+ })
+ os.Unsetenv("NETBIRD_STORE_ENGINE")
+ }
+}
+
+func Test_NewStore(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("The SQLite store is not properly supported by Windows yet")
}
- t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine))
- store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir())
- t.Cleanup(cleanUp)
- assert.NoError(t, err)
-
- if len(store.GetAllAccounts(context.Background())) != 0 {
- t.Errorf("expected to create a new empty Accounts map when creating a new FileStore")
- }
+ runTestForAllEngines(t, "", func(t *testing.T, store Store) {
+ if store == nil {
+ t.Errorf("expected to create a new Store")
+ }
+ if len(store.GetAllAccounts(context.Background())) != 0 {
+ t.Errorf("expected to create a new empty Accounts map when creating a new FileStore")
+ }
+ })
}
-func TestSqlite_SaveAccount_Large(t *testing.T) {
+func Test_SaveAccount_Large(t *testing.T) {
if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" {
t.Skip("skip CI tests on darwin and windows")
}
- t.Run("SQLite", func(t *testing.T) {
- t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine))
- store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir())
- t.Cleanup(cleanUp)
- assert.NoError(t, err)
- runLargeTest(t, store)
- })
-
- // create store outside to have a better time counter for the test
- t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine))
- store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir())
- t.Cleanup(cleanUp)
- assert.NoError(t, err)
- t.Run("PostgreSQL", func(t *testing.T) {
+ runTestForAllEngines(t, "", func(t *testing.T, store Store) {
runLargeTest(t, store)
})
}
@@ -213,77 +219,74 @@ func randomIPv4() net.IP {
return net.IP(b)
}
-func TestSqlite_SaveAccount(t *testing.T) {
+func Test_SaveAccount(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("The SQLite store is not properly supported by Windows yet")
}
- t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine))
- store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir())
- t.Cleanup(cleanUp)
- assert.NoError(t, err)
+ runTestForAllEngines(t, "", func(t *testing.T, store Store) {
+ account := newAccountWithId(context.Background(), "account_id", "testuser", "")
+ setupKey, _ := types.GenerateDefaultSetupKey()
+ account.SetupKeys[setupKey.Key] = setupKey
+ account.Peers["testpeer"] = &nbpeer.Peer{
+ Key: "peerkey",
+ IP: net.IP{127, 0, 0, 1},
+ Meta: nbpeer.PeerSystemMeta{},
+ Name: "peer name",
+ Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
+ }
- account := newAccountWithId(context.Background(), "account_id", "testuser", "")
- setupKey, _ := types.GenerateDefaultSetupKey()
- account.SetupKeys[setupKey.Key] = setupKey
- account.Peers["testpeer"] = &nbpeer.Peer{
- Key: "peerkey",
- IP: net.IP{127, 0, 0, 1},
- Meta: nbpeer.PeerSystemMeta{},
- Name: "peer name",
- Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
- }
+ err := store.SaveAccount(context.Background(), account)
+ require.NoError(t, err)
- err = store.SaveAccount(context.Background(), account)
- require.NoError(t, err)
+ account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "")
+ setupKey, _ = types.GenerateDefaultSetupKey()
+ account2.SetupKeys[setupKey.Key] = setupKey
+ account2.Peers["testpeer2"] = &nbpeer.Peer{
+ Key: "peerkey2",
+ IP: net.IP{127, 0, 0, 2},
+ Meta: nbpeer.PeerSystemMeta{},
+ Name: "peer name 2",
+ Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
+ }
- account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "")
- setupKey, _ = types.GenerateDefaultSetupKey()
- account2.SetupKeys[setupKey.Key] = setupKey
- account2.Peers["testpeer2"] = &nbpeer.Peer{
- Key: "peerkey2",
- IP: net.IP{127, 0, 0, 2},
- Meta: nbpeer.PeerSystemMeta{},
- Name: "peer name 2",
- Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
- }
+ err = store.SaveAccount(context.Background(), account2)
+ require.NoError(t, err)
- err = store.SaveAccount(context.Background(), account2)
- require.NoError(t, err)
+ if len(store.GetAllAccounts(context.Background())) != 2 {
+ t.Errorf("expecting 2 Accounts to be stored after SaveAccount()")
+ }
- if len(store.GetAllAccounts(context.Background())) != 2 {
- t.Errorf("expecting 2 Accounts to be stored after SaveAccount()")
- }
+ a, err := store.GetAccount(context.Background(), account.Id)
+ if a == nil {
+ t.Errorf("expecting Account to be stored after SaveAccount(): %v", err)
+ }
- a, err := store.GetAccount(context.Background(), account.Id)
- if a == nil {
- t.Errorf("expecting Account to be stored after SaveAccount(): %v", err)
- }
+ if a != nil && len(a.Policies) != 1 {
+ t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies))
+ }
- if a != nil && len(a.Policies) != 1 {
- t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies))
- }
+ if a != nil && len(a.Policies[0].Rules) != 1 {
+ t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules))
+ return
+ }
- if a != nil && len(a.Policies[0].Rules) != 1 {
- t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules))
- return
- }
+ if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil {
+ t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err)
+ }
- if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil {
- t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err)
- }
+ if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil {
+ t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err)
+ }
- if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil {
- t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err)
- }
+ if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil {
+ t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err)
+ }
- if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil {
- t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err)
- }
-
- if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil {
- t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err)
- }
+ if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil {
+ t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err)
+ }
+ })
}
func TestSqlite_DeleteAccount(t *testing.T) {
@@ -400,27 +403,24 @@ func TestSqlite_DeleteAccount(t *testing.T) {
}
}
-func TestSqlite_GetAccount(t *testing.T) {
+func Test_GetAccount(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("The SQLite store is not properly supported by Windows yet")
}
- t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine))
- store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
- t.Cleanup(cleanUp)
- assert.NoError(t, err)
+ runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) {
+ id := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
- id := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
+ account, err := store.GetAccount(context.Background(), id)
+ require.NoError(t, err)
+ require.Equal(t, id, account.Id, "account id should match")
- account, err := store.GetAccount(context.Background(), id)
- require.NoError(t, err)
- require.Equal(t, id, account.Id, "account id should match")
-
- _, err = store.GetAccount(context.Background(), "non-existing-account")
- assert.Error(t, err)
- parsedErr, ok := status.FromError(err)
- require.True(t, ok)
- require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
+ _, err = store.GetAccount(context.Background(), "non-existing-account")
+ assert.Error(t, err)
+ parsedErr, ok := status.FromError(err)
+ require.True(t, ok)
+ require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
+ })
}
func TestSqlStore_SavePeer(t *testing.T) {
@@ -578,51 +578,45 @@ func TestSqlStore_SavePeerLocation(t *testing.T) {
require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
}
-func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) {
+func Test_TestGetAccountByPrivateDomain(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("The SQLite store is not properly supported by Windows yet")
}
- t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine))
- store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
- t.Cleanup(cleanUp)
- assert.NoError(t, err)
+ runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) {
+ existingDomain := "test.com"
- existingDomain := "test.com"
+ account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain)
+ require.NoError(t, err, "should found account")
+ require.Equal(t, existingDomain, account.Domain, "domains should match")
- account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain)
- require.NoError(t, err, "should found account")
- require.Equal(t, existingDomain, account.Domain, "domains should match")
-
- _, err = store.GetAccountByPrivateDomain(context.Background(), "missing-domain.com")
- require.Error(t, err, "should return error on domain lookup")
- parsedErr, ok := status.FromError(err)
- require.True(t, ok)
- require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
+ _, err = store.GetAccountByPrivateDomain(context.Background(), "missing-domain.com")
+ require.Error(t, err, "should return error on domain lookup")
+ parsedErr, ok := status.FromError(err)
+ require.True(t, ok)
+ require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
+ })
}
-func TestSqlite_GetTokenIDByHashedToken(t *testing.T) {
+func Test_GetTokenIDByHashedToken(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("The SQLite store is not properly supported by Windows yet")
}
- t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine))
- store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
- t.Cleanup(cleanUp)
- assert.NoError(t, err)
+ runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) {
+ hashed := "SoMeHaShEdToKeN"
+ id := "9dj38s35-63fb-11ec-90d6-0242ac120003"
- hashed := "SoMeHaShEdToKeN"
- id := "9dj38s35-63fb-11ec-90d6-0242ac120003"
+ token, err := store.GetTokenIDByHashedToken(context.Background(), hashed)
+ require.NoError(t, err)
+ require.Equal(t, id, token)
- token, err := store.GetTokenIDByHashedToken(context.Background(), hashed)
- require.NoError(t, err)
- require.Equal(t, id, token)
-
- _, err = store.GetTokenIDByHashedToken(context.Background(), "non-existing-hash")
- require.Error(t, err)
- parsedErr, ok := status.FromError(err)
- require.True(t, ok)
- require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
+ _, err = store.GetTokenIDByHashedToken(context.Background(), "non-existing-hash")
+ require.Error(t, err)
+ parsedErr, ok := status.FromError(err)
+ require.True(t, ok)
+ require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
+ })
}
func TestMigrate(t *testing.T) {
@@ -1329,6 +1323,14 @@ func TestSqlStore_SaveGroups(t *testing.T) {
}
err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups)
require.NoError(t, err)
+
+ groups[1].Peers = []string{}
+ err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups)
+ require.NoError(t, err)
+
+ group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groups[1].ID)
+ require.NoError(t, err)
+ require.Equal(t, groups[1], group)
}
func TestSqlStore_DeleteGroup(t *testing.T) {
@@ -2043,52 +2045,12 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty
},
}
- if err := addAllGroup(acc); err != nil {
+ if err := acc.AddAllGroup(); err != nil {
log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err)
}
return acc
}
-// addAllGroup to account object if it doesn't exist
-func addAllGroup(account *types.Account) error {
- if len(account.Groups) == 0 {
- allGroup := &types.Group{
- ID: xid.New().String(),
- Name: "All",
- Issued: types.GroupIssuedAPI,
- }
- for _, peer := range account.Peers {
- allGroup.Peers = append(allGroup.Peers, peer.ID)
- }
- account.Groups = map[string]*types.Group{allGroup.ID: allGroup}
-
- id := xid.New().String()
-
- defaultPolicy := &types.Policy{
- ID: id,
- Name: types.DefaultRuleName,
- Description: types.DefaultRuleDescription,
- Enabled: true,
- Rules: []*types.PolicyRule{
- {
- ID: id,
- Name: types.DefaultRuleName,
- Description: types.DefaultRuleDescription,
- Enabled: true,
- Sources: []string{allGroup.ID},
- Destinations: []string{allGroup.ID},
- Bidirectional: true,
- Protocol: types.PolicyRuleProtocolALL,
- Action: types.PolicyTrafficActionAccept,
- },
- },
- }
-
- account.Policies = []*types.Policy{defaultPolicy}
- }
- return nil
-}
-
func TestSqlStore_GetAccountNetworks(t *testing.T) {
store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
t.Cleanup(cleanup)
@@ -2478,6 +2440,465 @@ func TestSqlStore_AddAndRemoveResourceFromGroup(t *testing.T) {
require.NotContains(t, group.Resources, *res)
}
+func TestSqlStore_DatabaseBlocking(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ concurrentReads := 40
+
+ testRunSuccessful := false
+ wgSuccess := sync.WaitGroup{}
+ wgSuccess.Add(concurrentReads)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+
+ start := make(chan struct{})
+
+ for i := 0; i < concurrentReads/2; i++ {
+ go func() {
+ t.Logf("Entered routine 1-%d", i)
+
+ <-start
+ err := store.ExecuteInTransaction(context.Background(), func(tx Store) error {
+ _, err := tx.GetAccountIDByPeerID(context.Background(), LockingStrengthShare, "cfvprsrlo1hqoo49ohog")
+ return err
+ })
+ if err != nil {
+ t.Errorf("Failed, got error: %v", err)
+ return
+ }
+
+ t.Log("Got User from routine 1")
+ wgSuccess.Done()
+ }()
+ }
+
+ for i := 0; i < concurrentReads/2; i++ {
+ go func() {
+ t.Logf("Entered routine 2-%d", i)
+
+ <-start
+ _, err := store.GetAccountIDByPeerID(context.Background(), LockingStrengthShare, "cfvprsrlo1hqoo49ohog")
+ if err != nil {
+ t.Errorf("Failed, got error: %v", err)
+ return
+ }
+
+ t.Log("Got User from routine 2")
+ wgSuccess.Done()
+ }()
+ }
+
+ time.Sleep(200 * time.Millisecond)
+ close(start)
+ t.Log("Started routines")
+
+ go func() {
+ wgSuccess.Wait()
+ testRunSuccessful = true
+ }()
+
+ <-ctx.Done()
+ if !testRunSuccessful {
+ t.Fatalf("Test failed")
+ }
+
+ t.Logf("Test completed")
+}
+
+func TestSqlStore_GetAccountCreatedBy(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ accountID string
+ expectError bool
+ createdBy string
+ }{
+ {
+ name: "existing account ID",
+ accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b",
+ expectError: false,
+ createdBy: "edafee4e-63fb-11ec-90d6-0242ac120003",
+ },
+ {
+ name: "non-existing account ID",
+ accountID: "nonexistent",
+ expectError: true,
+ },
+ {
+ name: "empty account ID",
+ accountID: "",
+ expectError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ createdBy, err := store.GetAccountCreatedBy(context.Background(), LockingStrengthShare, tt.accountID)
+ if tt.expectError {
+ require.Error(t, err)
+ sErr, ok := status.FromError(err)
+ require.True(t, ok)
+ require.Equal(t, sErr.Type(), status.NotFound)
+ require.Empty(t, createdBy)
+ } else {
+ require.NoError(t, err)
+ require.NotNil(t, createdBy)
+ require.Equal(t, tt.createdBy, createdBy)
+ }
+ })
+ }
+
+}
+
+func TestSqlStore_GetUserByUserID(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ userID string
+ expectError bool
+ }{
+ {
+ name: "retrieve existing user",
+ userID: "edafee4e-63fb-11ec-90d6-0242ac120003",
+ expectError: false,
+ },
+ {
+ name: "retrieve non-existing user",
+ userID: "non-existing",
+ expectError: true,
+ },
+ {
+ name: "retrieve with empty user ID",
+ userID: "",
+ expectError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, tt.userID)
+ if tt.expectError {
+ require.Error(t, err)
+ sErr, ok := status.FromError(err)
+ require.True(t, ok)
+ require.Equal(t, sErr.Type(), status.NotFound)
+ require.Nil(t, user)
+ } else {
+ require.NoError(t, err)
+ require.NotNil(t, user)
+ require.Equal(t, tt.userID, user.Id)
+ }
+ })
+ }
+}
+
+func TestSqlStore_GetUserByPATID(t *testing.T) {
+ store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
+ t.Cleanup(cleanUp)
+ assert.NoError(t, err)
+
+ id := "9dj38s35-63fb-11ec-90d6-0242ac120003"
+
+ user, err := store.GetUserByPATID(context.Background(), LockingStrengthShare, id)
+ require.NoError(t, err)
+ require.Equal(t, "f4f6d672-63fb-11ec-90d6-0242ac120003", user.Id)
+}
+
+func TestSqlStore_SaveUser(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
+
+ user := &types.User{
+ Id: "user-id",
+ AccountID: accountID,
+ Role: types.UserRoleAdmin,
+ IsServiceUser: false,
+ AutoGroups: []string{"groupA", "groupB"},
+ Blocked: false,
+ LastLogin: util.ToPtr(time.Now().UTC()),
+ CreatedAt: time.Now().UTC().Add(-time.Hour),
+ Issued: types.UserIssuedIntegration,
+ }
+ err = store.SaveUser(context.Background(), LockingStrengthUpdate, user)
+ require.NoError(t, err)
+
+ saveUser, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, user.Id)
+ require.NoError(t, err)
+ require.Equal(t, user.Id, saveUser.Id)
+ require.Equal(t, user.AccountID, saveUser.AccountID)
+ require.Equal(t, user.Role, saveUser.Role)
+ require.Equal(t, user.AutoGroups, saveUser.AutoGroups)
+ require.WithinDurationf(t, user.GetLastLogin(), saveUser.LastLogin.UTC(), time.Millisecond, "LastLogin should be equal")
+ require.WithinDurationf(t, user.CreatedAt, saveUser.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal")
+ require.Equal(t, user.Issued, saveUser.Issued)
+ require.Equal(t, user.Blocked, saveUser.Blocked)
+ require.Equal(t, user.IsServiceUser, saveUser.IsServiceUser)
+}
+
+func TestSqlStore_SaveUsers(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
+
+ accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID)
+ require.NoError(t, err)
+ require.Len(t, accountUsers, 2)
+
+ users := []*types.User{
+ {
+ Id: "user-1",
+ AccountID: accountID,
+ Issued: "api",
+ AutoGroups: []string{"groupA", "groupB"},
+ },
+ {
+ Id: "user-2",
+ AccountID: accountID,
+ Issued: "integration",
+ AutoGroups: []string{"groupA"},
+ },
+ }
+ err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users)
+ require.NoError(t, err)
+
+ accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID)
+ require.NoError(t, err)
+ require.Len(t, accountUsers, 4)
+
+ users[1].AutoGroups = []string{"groupA", "groupC"}
+ err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users)
+ require.NoError(t, err)
+
+ user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, users[1].Id)
+ require.NoError(t, err)
+ require.Equal(t, users[1].AutoGroups, user.AutoGroups)
+}
+
+func TestSqlStore_DeleteUser(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
+ userID := "f4f6d672-63fb-11ec-90d6-0242ac120003"
+
+ err = store.DeleteUser(context.Background(), LockingStrengthUpdate, accountID, userID)
+ require.NoError(t, err)
+
+ user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, userID)
+ require.Error(t, err)
+ require.Nil(t, user)
+
+ userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, userID)
+ require.NoError(t, err)
+ require.Len(t, userPATs, 0)
+}
+
+func TestSqlStore_GetPATByID(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ userID := "f4f6d672-63fb-11ec-90d6-0242ac120003"
+
+ tests := []struct {
+ name string
+ patID string
+ expectError bool
+ }{
+ {
+ name: "retrieve existing PAT",
+ patID: "9dj38s35-63fb-11ec-90d6-0242ac120003",
+ expectError: false,
+ },
+ {
+ name: "retrieve non-existing PAT",
+ patID: "non-existing",
+ expectError: true,
+ },
+ {
+ name: "retrieve with empty PAT ID",
+ patID: "",
+ expectError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, tt.patID)
+ if tt.expectError {
+ require.Error(t, err)
+ sErr, ok := status.FromError(err)
+ require.True(t, ok)
+ require.Equal(t, sErr.Type(), status.NotFound)
+ require.Nil(t, pat)
+ } else {
+ require.NoError(t, err)
+ require.NotNil(t, pat)
+ require.Equal(t, tt.patID, pat.ID)
+ }
+ })
+ }
+}
+
+func TestSqlStore_GetUserPATs(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, "f4f6d672-63fb-11ec-90d6-0242ac120003")
+ require.NoError(t, err)
+ require.Len(t, userPATs, 1)
+}
+
+func TestSqlStore_GetPATByHashedToken(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ pat, err := store.GetPATByHashedToken(context.Background(), LockingStrengthShare, "SoMeHaShEdToKeN")
+ require.NoError(t, err)
+ require.Equal(t, "9dj38s35-63fb-11ec-90d6-0242ac120003", pat.ID)
+}
+
+func TestSqlStore_MarkPATUsed(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ userID := "f4f6d672-63fb-11ec-90d6-0242ac120003"
+ patID := "9dj38s35-63fb-11ec-90d6-0242ac120003"
+
+ err = store.MarkPATUsed(context.Background(), LockingStrengthUpdate, patID)
+ require.NoError(t, err)
+
+ pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID)
+ require.NoError(t, err)
+ now := time.Now().UTC()
+ require.WithinRange(t, pat.LastUsed.UTC(), now.Add(-15*time.Second), now, "LastUsed should be within 1 second of now")
+}
+
+func TestSqlStore_SavePAT(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ userID := "edafee4e-63fb-11ec-90d6-0242ac120003"
+
+ pat := &types.PersonalAccessToken{
+ ID: "pat-id",
+ UserID: userID,
+ Name: "token",
+ HashedToken: "SoMeHaShEdToKeN",
+ ExpirationDate: util.ToPtr(time.Now().UTC().Add(12 * time.Hour)),
+ CreatedBy: userID,
+ CreatedAt: time.Now().UTC().Add(time.Hour),
+ LastUsed: util.ToPtr(time.Now().UTC().Add(-15 * time.Minute)),
+ }
+ err = store.SavePAT(context.Background(), LockingStrengthUpdate, pat)
+ require.NoError(t, err)
+
+ savePAT, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, pat.ID)
+ require.NoError(t, err)
+ require.Equal(t, pat.ID, savePAT.ID)
+ require.Equal(t, pat.UserID, savePAT.UserID)
+ require.Equal(t, pat.HashedToken, savePAT.HashedToken)
+ require.Equal(t, pat.CreatedBy, savePAT.CreatedBy)
+ require.WithinDurationf(t, pat.GetExpirationDate(), savePAT.ExpirationDate.UTC(), time.Millisecond, "ExpirationDate should be equal")
+ require.WithinDurationf(t, pat.CreatedAt, savePAT.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal")
+ require.WithinDurationf(t, pat.GetLastUsed(), savePAT.LastUsed.UTC(), time.Millisecond, "LastUsed should be equal")
+}
+
+func TestSqlStore_DeletePAT(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ userID := "f4f6d672-63fb-11ec-90d6-0242ac120003"
+ patID := "9dj38s35-63fb-11ec-90d6-0242ac120003"
+
+ err = store.DeletePAT(context.Background(), LockingStrengthUpdate, userID, patID)
+ require.NoError(t, err)
+
+ pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID)
+ require.Error(t, err)
+ require.Nil(t, pat)
+}
+
+func TestSqlStore_SaveUsers_LargeBatch(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
+
+ accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID)
+ require.NoError(t, err)
+ require.Len(t, accountUsers, 2)
+
+ usersToSave := make([]*types.User, 0)
+
+ for i := 1; i <= 8000; i++ {
+ usersToSave = append(usersToSave, &types.User{
+ Id: fmt.Sprintf("user-%d", i),
+ AccountID: accountID,
+ Role: types.UserRoleUser,
+ })
+ }
+
+ err = store.SaveUsers(context.Background(), LockingStrengthUpdate, usersToSave)
+ require.NoError(t, err)
+
+ accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID)
+ require.NoError(t, err)
+ require.Equal(t, 8002, len(accountUsers))
+}
+
+func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) {
+ store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
+ t.Cleanup(cleanup)
+ require.NoError(t, err)
+
+ accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
+
+ accountGroups, err := store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID)
+ require.NoError(t, err)
+ require.Len(t, accountGroups, 3)
+
+ groupsToSave := make([]*types.Group, 0)
+
+ for i := 1; i <= 8000; i++ {
+ groupsToSave = append(groupsToSave, &types.Group{
+ ID: fmt.Sprintf("%d", i),
+ AccountID: accountID,
+ Name: fmt.Sprintf("group-%d", i),
+ })
+ }
+
+ err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groupsToSave)
+ require.NoError(t, err)
+
+ accountGroups, err = store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID)
+ require.NoError(t, err)
+ require.Equal(t, 8003, len(accountGroups))
+}
+
func TestSqlStore_AddPeerToGroup(t *testing.T) {
store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_policy_migrate.sql", t.TempDir())
t.Cleanup(cleanup)
@@ -2804,329 +3225,6 @@ func TestSqlStore_DeletePeer(t *testing.T) {
require.Nil(t, peer)
}
-func TestSqlStore_GetAccountCreatedBy(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- tests := []struct {
- name string
- accountID string
- expectError bool
- createdBy string
- }{
- {
- name: "existing account ID",
- accountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b",
- expectError: false,
- createdBy: "edafee4e-63fb-11ec-90d6-0242ac120003",
- },
- {
- name: "non-existing account ID",
- accountID: "nonexistent",
- expectError: true,
- },
- {
- name: "empty account ID",
- accountID: "",
- expectError: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- createdBy, err := store.GetAccountCreatedBy(context.Background(), LockingStrengthShare, tt.accountID)
- if tt.expectError {
- require.Error(t, err)
- sErr, ok := status.FromError(err)
- require.True(t, ok)
- require.Equal(t, sErr.Type(), status.NotFound)
- require.Empty(t, createdBy)
- } else {
- require.NoError(t, err)
- require.NotNil(t, createdBy)
- require.Equal(t, tt.createdBy, createdBy)
- }
- })
- }
-
-}
-
-func TestSqlStore_GetUserByUserID(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- tests := []struct {
- name string
- userID string
- expectError bool
- }{
- {
- name: "retrieve existing user",
- userID: "edafee4e-63fb-11ec-90d6-0242ac120003",
- expectError: false,
- },
- {
- name: "retrieve non-existing user",
- userID: "non-existing",
- expectError: true,
- },
- {
- name: "retrieve with empty user ID",
- userID: "",
- expectError: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, tt.userID)
- if tt.expectError {
- require.Error(t, err)
- sErr, ok := status.FromError(err)
- require.True(t, ok)
- require.Equal(t, sErr.Type(), status.NotFound)
- require.Nil(t, user)
- } else {
- require.NoError(t, err)
- require.NotNil(t, user)
- require.Equal(t, tt.userID, user.Id)
- }
- })
- }
-}
-
-func TestSqlStore_GetUserByPATID(t *testing.T) {
- store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
- t.Cleanup(cleanUp)
- assert.NoError(t, err)
-
- id := "9dj38s35-63fb-11ec-90d6-0242ac120003"
-
- user, err := store.GetUserByPATID(context.Background(), LockingStrengthShare, id)
- require.NoError(t, err)
- require.Equal(t, "f4f6d672-63fb-11ec-90d6-0242ac120003", user.Id)
-}
-
-func TestSqlStore_SaveUser(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
-
- user := &types.User{
- Id: "user-id",
- AccountID: accountID,
- Role: types.UserRoleAdmin,
- IsServiceUser: false,
- AutoGroups: []string{"groupA", "groupB"},
- Blocked: false,
- LastLogin: util.ToPtr(time.Now().UTC()),
- CreatedAt: time.Now().UTC().Add(-time.Hour),
- Issued: types.UserIssuedIntegration,
- }
- err = store.SaveUser(context.Background(), LockingStrengthUpdate, user)
- require.NoError(t, err)
-
- saveUser, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, user.Id)
- require.NoError(t, err)
- require.Equal(t, user.Id, saveUser.Id)
- require.Equal(t, user.AccountID, saveUser.AccountID)
- require.Equal(t, user.Role, saveUser.Role)
- require.Equal(t, user.AutoGroups, saveUser.AutoGroups)
- require.WithinDurationf(t, user.GetLastLogin(), saveUser.LastLogin.UTC(), time.Millisecond, "LastLogin should be equal")
- require.WithinDurationf(t, user.CreatedAt, saveUser.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal")
- require.Equal(t, user.Issued, saveUser.Issued)
- require.Equal(t, user.Blocked, saveUser.Blocked)
- require.Equal(t, user.IsServiceUser, saveUser.IsServiceUser)
-}
-
-func TestSqlStore_SaveUsers(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
-
- accountUsers, err := store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID)
- require.NoError(t, err)
- require.Len(t, accountUsers, 2)
-
- users := []*types.User{
- {
- Id: "user-1",
- AccountID: accountID,
- Issued: "api",
- AutoGroups: []string{"groupA", "groupB"},
- },
- {
- Id: "user-2",
- AccountID: accountID,
- Issued: "integration",
- AutoGroups: []string{"groupA"},
- },
- }
- err = store.SaveUsers(context.Background(), LockingStrengthUpdate, users)
- require.NoError(t, err)
-
- accountUsers, err = store.GetAccountUsers(context.Background(), LockingStrengthShare, accountID)
- require.NoError(t, err)
- require.Len(t, accountUsers, 4)
-}
-
-func TestSqlStore_DeleteUser(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
- userID := "f4f6d672-63fb-11ec-90d6-0242ac120003"
-
- err = store.DeleteUser(context.Background(), LockingStrengthUpdate, accountID, userID)
- require.NoError(t, err)
-
- user, err := store.GetUserByUserID(context.Background(), LockingStrengthShare, userID)
- require.Error(t, err)
- require.Nil(t, user)
-
- userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, userID)
- require.NoError(t, err)
- require.Len(t, userPATs, 0)
-}
-
-func TestSqlStore_GetPATByID(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- userID := "f4f6d672-63fb-11ec-90d6-0242ac120003"
-
- tests := []struct {
- name string
- patID string
- expectError bool
- }{
- {
- name: "retrieve existing PAT",
- patID: "9dj38s35-63fb-11ec-90d6-0242ac120003",
- expectError: false,
- },
- {
- name: "retrieve non-existing PAT",
- patID: "non-existing",
- expectError: true,
- },
- {
- name: "retrieve with empty PAT ID",
- patID: "",
- expectError: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, tt.patID)
- if tt.expectError {
- require.Error(t, err)
- sErr, ok := status.FromError(err)
- require.True(t, ok)
- require.Equal(t, sErr.Type(), status.NotFound)
- require.Nil(t, pat)
- } else {
- require.NoError(t, err)
- require.NotNil(t, pat)
- require.Equal(t, tt.patID, pat.ID)
- }
- })
- }
-}
-
-func TestSqlStore_GetUserPATs(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- userPATs, err := store.GetUserPATs(context.Background(), LockingStrengthShare, "f4f6d672-63fb-11ec-90d6-0242ac120003")
- require.NoError(t, err)
- require.Len(t, userPATs, 1)
-}
-
-func TestSqlStore_GetPATByHashedToken(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- pat, err := store.GetPATByHashedToken(context.Background(), LockingStrengthShare, "SoMeHaShEdToKeN")
- require.NoError(t, err)
- require.Equal(t, "9dj38s35-63fb-11ec-90d6-0242ac120003", pat.ID)
-}
-
-func TestSqlStore_MarkPATUsed(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- userID := "f4f6d672-63fb-11ec-90d6-0242ac120003"
- patID := "9dj38s35-63fb-11ec-90d6-0242ac120003"
-
- err = store.MarkPATUsed(context.Background(), LockingStrengthUpdate, patID)
- require.NoError(t, err)
-
- pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID)
- require.NoError(t, err)
- now := time.Now().UTC()
- require.WithinRange(t, pat.LastUsed.UTC(), now.Add(-15*time.Second), now, "LastUsed should be within 1 second of now")
-}
-
-func TestSqlStore_SavePAT(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- userID := "edafee4e-63fb-11ec-90d6-0242ac120003"
-
- pat := &types.PersonalAccessToken{
- ID: "pat-id",
- UserID: userID,
- Name: "token",
- HashedToken: "SoMeHaShEdToKeN",
- ExpirationDate: util.ToPtr(time.Now().UTC().Add(12 * time.Hour)),
- CreatedBy: userID,
- CreatedAt: time.Now().UTC().Add(time.Hour),
- LastUsed: util.ToPtr(time.Now().UTC().Add(-15 * time.Minute)),
- }
- err = store.SavePAT(context.Background(), LockingStrengthUpdate, pat)
- require.NoError(t, err)
-
- savePAT, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, pat.ID)
- require.NoError(t, err)
- require.Equal(t, pat.ID, savePAT.ID)
- require.Equal(t, pat.UserID, savePAT.UserID)
- require.Equal(t, pat.HashedToken, savePAT.HashedToken)
- require.Equal(t, pat.CreatedBy, savePAT.CreatedBy)
- require.WithinDurationf(t, pat.GetExpirationDate(), savePAT.ExpirationDate.UTC(), time.Millisecond, "ExpirationDate should be equal")
- require.WithinDurationf(t, pat.CreatedAt, savePAT.CreatedAt.UTC(), time.Millisecond, "CreatedAt should be equal")
- require.WithinDurationf(t, pat.GetLastUsed(), savePAT.LastUsed.UTC(), time.Millisecond, "LastUsed should be equal")
-}
-
-func TestSqlStore_DeletePAT(t *testing.T) {
- store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
- t.Cleanup(cleanup)
- require.NoError(t, err)
-
- userID := "f4f6d672-63fb-11ec-90d6-0242ac120003"
- patID := "9dj38s35-63fb-11ec-90d6-0242ac120003"
-
- err = store.DeletePAT(context.Background(), LockingStrengthUpdate, userID, patID)
- require.NoError(t, err)
-
- pat, err := store.GetPATByID(context.Background(), LockingStrengthShare, userID, patID)
- require.Error(t, err)
- require.Nil(t, pat)
-}
-
func TestSqlStore_GetAccountRoutes(t *testing.T) {
store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir())
t.Cleanup(cleanup)
diff --git a/management/server/store/store.go b/management/server/store/store.go
index e05fe57c8..0acb74123 100644
--- a/management/server/store/store.go
+++ b/management/server/store/store.go
@@ -9,11 +9,16 @@ import (
"os"
"path"
"path/filepath"
+ "regexp"
"runtime"
+ "slices"
"strings"
"time"
+ "github.com/google/uuid"
log "github.com/sirupsen/logrus"
+ "gorm.io/driver/mysql"
+ "gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@@ -43,6 +48,7 @@ const (
)
type Store interface {
+ GetAccountsCounter(ctx context.Context) (int64, error)
GetAllAccounts(ctx context.Context) []*types.Account
GetAccount(ctx context.Context, accountID string) (*types.Account, error)
AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error)
@@ -195,6 +201,8 @@ const (
mysqlDsnEnv = "NETBIRD_STORE_ENGINE_MYSQL_DSN"
)
+var supportedEngines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine}
+
func getStoreEngineFromEnv() Engine {
// NETBIRD_STORE_ENGINE supposed to be used in tests. Otherwise, rely on the config file.
kind, ok := os.LookupEnv("NETBIRD_STORE_ENGINE")
@@ -203,7 +211,7 @@ func getStoreEngineFromEnv() Engine {
}
value := Engine(strings.ToLower(kind))
- if value == SqliteStoreEngine || value == PostgresStoreEngine || value == MysqlStoreEngine {
+ if slices.Contains(supportedEngines, value) {
return value
}
@@ -347,55 +355,158 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) (
return nil, nil, fmt.Errorf("failed to create test store: %v", err)
}
+ err = addAllGroupToAccount(ctx, store)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to add all group to account: %v", err)
+ }
+
return getSqlStoreEngine(ctx, store, kind)
}
-func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) {
- if kind == PostgresStoreEngine {
- cleanUp, err := testutil.CreatePostgresTestContainer()
+func addAllGroupToAccount(ctx context.Context, store Store) error {
+ allAccounts := store.GetAllAccounts(ctx)
+ for _, account := range allAccounts {
+ shouldSave := false
+
+ _, err := account.GetGroupAll()
if err != nil {
- return nil, nil, err
+ if err := account.AddAllGroup(); err != nil {
+ return err
+ }
+ shouldSave = true
}
- dsn, ok := os.LookupEnv(postgresDsnEnv)
- if !ok {
- return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv)
+ if shouldSave {
+ err = store.SaveAccount(ctx, account)
+ if err != nil {
+ return err
+ }
}
-
- store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil)
- if err != nil {
- return nil, nil, err
- }
-
- return store, cleanUp, nil
}
+ return nil
+}
- if kind == MysqlStoreEngine {
- cleanUp, err := testutil.CreateMysqlTestContainer()
- if err != nil {
- return nil, nil, err
+func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) {
+ var cleanup func()
+ var err error
+ switch kind {
+ case PostgresStoreEngine:
+ store, cleanup, err = newReusedPostgresStore(ctx, store, kind)
+ case MysqlStoreEngine:
+ store, cleanup, err = newReusedMysqlStore(ctx, store, kind)
+ default:
+ cleanup = func() {
+ // sqlite doesn't need to be cleaned up
}
-
- dsn, ok := os.LookupEnv(mysqlDsnEnv)
- if !ok {
- return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
- }
-
- store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil)
- if err != nil {
- return nil, nil, err
- }
-
- return store, cleanUp, nil
+ }
+ if err != nil {
+ return nil, cleanup, fmt.Errorf("failed to create test store: %v", err)
}
closeConnection := func() {
+ cleanup()
store.Close(ctx)
}
return store, closeConnection, nil
}
+func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind Engine) (*SqlStore, func(), error) {
+ if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" {
+ var err error
+ _, err = testutil.CreatePostgresTestContainer()
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ dsn, ok := os.LookupEnv(postgresDsnEnv)
+ if !ok {
+ return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv)
+ }
+
+ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to open postgres connection: %v", err)
+ }
+
+ dsn, cleanup, err := createRandomDB(dsn, db, kind)
+ if err != nil {
+ return nil, cleanup, err
+ }
+
+ store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil)
+ if err != nil {
+ return nil, cleanup, err
+ }
+
+ return store, cleanup, nil
+}
+
+func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind Engine) (*SqlStore, func(), error) {
+ if envDsn, ok := os.LookupEnv(mysqlDsnEnv); !ok || envDsn == "" {
+ var err error
+ _, err = testutil.CreateMysqlTestContainer()
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ dsn, ok := os.LookupEnv(mysqlDsnEnv)
+ if !ok {
+ return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
+ }
+
+ db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{})
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to open mysql connection: %v", err)
+ }
+
+ dsn, cleanup, err := createRandomDB(dsn, db, kind)
+ if err != nil {
+ return nil, cleanup, err
+ }
+
+ store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return store, cleanup, nil
+}
+
+func createRandomDB(dsn string, db *gorm.DB, engine Engine) (string, func(), error) {
+ dbName := fmt.Sprintf("test_db_%s", strings.ReplaceAll(uuid.New().String(), "-", "_"))
+
+ if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil {
+ return "", nil, fmt.Errorf("failed to create database: %v", err)
+ }
+
+ var err error
+ cleanup := func() {
+ switch engine {
+ case PostgresStoreEngine:
+ err = db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)).Error
+ case MysqlStoreEngine:
+ // err = killMySQLConnections(dsn, dbName)
+ err = db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)).Error
+ }
+ if err != nil {
+ log.Errorf("failed to drop database %s: %v", dbName, err)
+ panic(err)
+ }
+ sqlDB, _ := db.DB()
+ _ = sqlDB.Close()
+ }
+
+ return replaceDBName(dsn, dbName), cleanup, nil
+}
+
+func replaceDBName(dsn, newDBName string) string {
+ re := regexp.MustCompile(`(?P[:/@])(?P[^/?]+)(?P\?|$)`)
+ return re.ReplaceAllString(dsn, `${pre}`+newDBName+`${post}`)
+}
+
func loadSQL(db *gorm.DB, filepath string) error {
sqlContent, err := os.ReadFile(filepath)
if err != nil {
diff --git a/management/server/telemetry/accountmanager_metrics.go b/management/server/telemetry/accountmanager_metrics.go
index 4a5a31e2d..3b1e078eb 100644
--- a/management/server/telemetry/accountmanager_metrics.go
+++ b/management/server/telemetry/accountmanager_metrics.go
@@ -22,7 +22,8 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account
metric.WithUnit("milliseconds"),
metric.WithExplicitBucketBoundaries(
0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000,
- ))
+ ),
+ metric.WithDescription("Duration of triggering the account peers update and preparing the required data for the network map being sent to the clients"))
if err != nil {
return nil, err
}
@@ -31,7 +32,8 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account
metric.WithUnit("milliseconds"),
metric.WithExplicitBucketBoundaries(
0.1, 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000,
- ))
+ ),
+ metric.WithDescription("Duration of calculating the peer network map that is sent to the clients"))
if err != nil {
return nil, err
}
@@ -40,12 +42,15 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account
metric.WithUnit("objects"),
metric.WithExplicitBucketBoundaries(
50, 100, 200, 500, 1000, 2500, 5000, 10000,
- ))
+ ),
+ metric.WithDescription("Number of objects in the network map like peers, routes, firewall rules, etc. that are sent to the clients"))
if err != nil {
return nil, err
}
- peerMetaUpdateCount, err := meter.Int64Counter("management.account.peer.meta.update.counter", metric.WithUnit("1"))
+ peerMetaUpdateCount, err := meter.Int64Counter("management.account.peer.meta.update.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of updates with new meta data from the peers"))
if err != nil {
return nil, err
}
diff --git a/management/server/telemetry/grpc_metrics.go b/management/server/telemetry/grpc_metrics.go
index acbe1281c..ac6ff2ea8 100644
--- a/management/server/telemetry/grpc_metrics.go
+++ b/management/server/telemetry/grpc_metrics.go
@@ -22,32 +22,50 @@ type GRPCMetrics struct {
// NewGRPCMetrics creates new GRPCMetrics struct and registers common metrics of the gRPC server
func NewGRPCMetrics(ctx context.Context, meter metric.Meter) (*GRPCMetrics, error) {
- syncRequestsCounter, err := meter.Int64Counter("management.grpc.sync.request.counter", metric.WithUnit("1"))
+ syncRequestsCounter, err := meter.Int64Counter("management.grpc.sync.request.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of sync gRPC requests from the peers to establish a connection and receive network map updates (update channel)"),
+ )
if err != nil {
return nil, err
}
- loginRequestsCounter, err := meter.Int64Counter("management.grpc.login.request.counter", metric.WithUnit("1"))
+ loginRequestsCounter, err := meter.Int64Counter("management.grpc.login.request.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of login gRPC requests from the peers to authenticate and receive initial configuration and relay credentials"),
+ )
if err != nil {
return nil, err
}
- getKeyRequestsCounter, err := meter.Int64Counter("management.grpc.key.request.counter", metric.WithUnit("1"))
+ getKeyRequestsCounter, err := meter.Int64Counter("management.grpc.key.request.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of key gRPC requests from the peers to get the server's public WireGuard key"),
+ )
if err != nil {
return nil, err
}
- activeStreamsGauge, err := meter.Int64ObservableGauge("management.grpc.connected.streams", metric.WithUnit("1"))
+ activeStreamsGauge, err := meter.Int64ObservableGauge("management.grpc.connected.streams",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of active peer streams connected to the gRPC server"),
+ )
if err != nil {
return nil, err
}
- syncRequestDuration, err := meter.Int64Histogram("management.grpc.sync.request.duration.ms", metric.WithUnit("milliseconds"))
+ syncRequestDuration, err := meter.Int64Histogram("management.grpc.sync.request.duration.ms",
+ metric.WithUnit("milliseconds"),
+ metric.WithDescription("Duration of the sync gRPC requests from the peers to establish a connection and receive network map updates (update channel)"),
+ )
if err != nil {
return nil, err
}
- loginRequestDuration, err := meter.Int64Histogram("management.grpc.login.request.duration.ms", metric.WithUnit("milliseconds"))
+ loginRequestDuration, err := meter.Int64Histogram("management.grpc.login.request.duration.ms",
+ metric.WithUnit("milliseconds"),
+ metric.WithDescription("Duration of the login gRPC requests from the peers to authenticate and receive initial configuration and relay credentials"),
+ )
if err != nil {
return nil, err
}
@@ -57,7 +75,7 @@ func NewGRPCMetrics(ctx context.Context, meter metric.Meter) (*GRPCMetrics, erro
// TODO(yury): This needs custom bucketing as we are interested in the values from 0 to server.channelBufferSize (100)
channelQueue, err := meter.Int64Histogram(
"management.grpc.updatechannel.queue",
- metric.WithDescription("Number of update messages in the channel queue"),
+ metric.WithDescription("Number of update messages piling up in the update channel queue"),
metric.WithUnit("length"),
)
if err != nil {
diff --git a/management/server/telemetry/http_api_metrics.go b/management/server/telemetry/http_api_metrics.go
index 357f019c7..5ef9e6d02 100644
--- a/management/server/telemetry/http_api_metrics.go
+++ b/management/server/telemetry/http_api_metrics.go
@@ -74,37 +74,58 @@ type HTTPMiddleware struct {
// NewMetricsMiddleware creates a new HTTPMiddleware
func NewMetricsMiddleware(ctx context.Context, meter metric.Meter) (*HTTPMiddleware, error) {
- httpRequestCounter, err := meter.Int64Counter(httpRequestCounterPrefix, metric.WithUnit("1"))
+ httpRequestCounter, err := meter.Int64Counter(httpRequestCounterPrefix,
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of incoming HTTP requests by endpoint and method"),
+ )
if err != nil {
return nil, err
}
- httpResponseCounter, err := meter.Int64Counter(httpResponseCounterPrefix, metric.WithUnit("1"))
+ httpResponseCounter, err := meter.Int64Counter(httpResponseCounterPrefix,
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of outgoing HTTP responses by endpoint, method and returned status code"),
+ )
if err != nil {
return nil, err
}
- totalHTTPRequestsCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpRequestCounterPrefix), metric.WithUnit("1"))
+ totalHTTPRequestsCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpRequestCounterPrefix),
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of incoming HTTP requests"),
+ )
if err != nil {
return nil, err
}
- totalHTTPResponseCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpResponseCounterPrefix), metric.WithUnit("1"))
+ totalHTTPResponseCounter, err := meter.Int64Counter(fmt.Sprintf("%s.total", httpResponseCounterPrefix),
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of outgoing HTTP responses"),
+ )
if err != nil {
return nil, err
}
- totalHTTPResponseCodeCounter, err := meter.Int64Counter(fmt.Sprintf("%s.code.total", httpResponseCounterPrefix), metric.WithUnit("1"))
+ totalHTTPResponseCodeCounter, err := meter.Int64Counter(fmt.Sprintf("%s.code.total", httpResponseCounterPrefix),
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of outgoing HTTP responses by status code"),
+ )
if err != nil {
return nil, err
}
- httpRequestDuration, err := meter.Int64Histogram(httpRequestDurationPrefix, metric.WithUnit("milliseconds"))
+ httpRequestDuration, err := meter.Int64Histogram(httpRequestDurationPrefix,
+ metric.WithUnit("milliseconds"),
+ metric.WithDescription("Duration of incoming HTTP requests by endpoint and method"),
+ )
if err != nil {
return nil, err
}
- totalHTTPRequestDuration, err := meter.Int64Histogram(fmt.Sprintf("%s.total", httpRequestDurationPrefix), metric.WithUnit("milliseconds"))
+ totalHTTPRequestDuration, err := meter.Int64Histogram(fmt.Sprintf("%s.total", httpRequestDurationPrefix),
+ metric.WithUnit("milliseconds"),
+ metric.WithDescription("Duration of incoming HTTP requests"),
+ )
if err != nil {
return nil, err
}
diff --git a/management/server/telemetry/idp_metrics.go b/management/server/telemetry/idp_metrics.go
index 0bcd5d432..5337c91c2 100644
--- a/management/server/telemetry/idp_metrics.go
+++ b/management/server/telemetry/idp_metrics.go
@@ -23,43 +23,73 @@ type IDPMetrics struct {
// NewIDPMetrics creates new IDPMetrics struct and registers common
func NewIDPMetrics(ctx context.Context, meter metric.Meter) (*IDPMetrics, error) {
- metaUpdateCounter, err := meter.Int64Counter("management.idp.update.user.meta.counter", metric.WithUnit("1"))
+ metaUpdateCounter, err := meter.Int64Counter("management.idp.update.user.meta.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of updates of user metadata sent to the configured identity provider"),
+ )
if err != nil {
return nil, err
}
- getUserByEmailCounter, err := meter.Int64Counter("management.idp.get.user.by.email.counter", metric.WithUnit("1"))
+ getUserByEmailCounter, err := meter.Int64Counter("management.idp.get.user.by.email.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of requests to get a user by email from the configured identity provider"),
+ )
if err != nil {
return nil, err
}
- getAllAccountsCounter, err := meter.Int64Counter("management.idp.get.accounts.counter", metric.WithUnit("1"))
+ getAllAccountsCounter, err := meter.Int64Counter("management.idp.get.accounts.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of requests to get all accounts from the configured identity provider"),
+ )
if err != nil {
return nil, err
}
- createUserCounter, err := meter.Int64Counter("management.idp.create.user.counter", metric.WithUnit("1"))
+ createUserCounter, err := meter.Int64Counter("management.idp.create.user.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of requests to create a new user in the configured identity provider"),
+ )
if err != nil {
return nil, err
}
- deleteUserCounter, err := meter.Int64Counter("management.idp.delete.user.counter", metric.WithUnit("1"))
+ deleteUserCounter, err := meter.Int64Counter("management.idp.delete.user.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of requests to delete a user from the configured identity provider"),
+ )
if err != nil {
return nil, err
}
- getAccountCounter, err := meter.Int64Counter("management.idp.get.account.counter", metric.WithUnit("1"))
+ getAccountCounter, err := meter.Int64Counter("management.idp.get.account.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of requests to get all users in an account from the configured identity provider"),
+ )
if err != nil {
return nil, err
}
- getUserByIDCounter, err := meter.Int64Counter("management.idp.get.user.by.id.counter", metric.WithUnit("1"))
+ getUserByIDCounter, err := meter.Int64Counter("management.idp.get.user.by.id.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of requests to get a user by ID from the configured identity provider"),
+ )
if err != nil {
return nil, err
}
- authenticateRequestCounter, err := meter.Int64Counter("management.idp.authenticate.request.counter", metric.WithUnit("1"))
+ authenticateRequestCounter, err := meter.Int64Counter("management.idp.authenticate.request.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of requests to authenticate the server with the configured identity provider"),
+ )
if err != nil {
return nil, err
}
- requestErrorCounter, err := meter.Int64Counter("management.idp.request.error.counter", metric.WithUnit("1"))
+ requestErrorCounter, err := meter.Int64Counter("management.idp.request.error.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of errors that happened when doing http request to the configured identity provider"),
+ )
if err != nil {
return nil, err
}
- requestStatusErrorCounter, err := meter.Int64Counter("management.idp.request.status.error.counter", metric.WithUnit("1"))
+ requestStatusErrorCounter, err := meter.Int64Counter("management.idp.request.status.error.counter",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of responses that came from the configured identity provider with non success status code"),
+ )
if err != nil {
return nil, err
}
diff --git a/management/server/telemetry/store_metrics.go b/management/server/telemetry/store_metrics.go
index bb3745b5a..f035ce847 100644
--- a/management/server/telemetry/store_metrics.go
+++ b/management/server/telemetry/store_metrics.go
@@ -20,28 +20,41 @@ type StoreMetrics struct {
// NewStoreMetrics creates an instance of StoreMetrics
func NewStoreMetrics(ctx context.Context, meter metric.Meter) (*StoreMetrics, error) {
globalLockAcquisitionDurationMicro, err := meter.Int64Histogram("management.store.global.lock.acquisition.duration.micro",
- metric.WithUnit("microseconds"))
+ metric.WithUnit("microseconds"),
+ metric.WithDescription("Duration of how long it takes to acquire the global lock in the store to block all other requests to the store"),
+ )
if err != nil {
return nil, err
}
- globalLockAcquisitionDurationMs, err := meter.Int64Histogram("management.store.global.lock.acquisition.duration.ms")
+ globalLockAcquisitionDurationMs, err := meter.Int64Histogram("management.store.global.lock.acquisition.duration.ms",
+ metric.WithUnit("milliseconds"),
+ metric.WithDescription("Duration of how long a process holds the acquired global lock in the store"),
+ )
if err != nil {
return nil, err
}
persistenceDurationMicro, err := meter.Int64Histogram("management.store.persistence.duration.micro",
- metric.WithUnit("microseconds"))
+ metric.WithUnit("microseconds"),
+ metric.WithDescription("Duration of how long it takes to save or delete an account in the store"),
+ )
if err != nil {
return nil, err
}
- persistenceDurationMs, err := meter.Int64Histogram("management.store.persistence.duration.ms")
+ persistenceDurationMs, err := meter.Int64Histogram("management.store.persistence.duration.ms",
+ metric.WithUnit("milliseconds"),
+ metric.WithDescription("Duration of how long it takes to save or delete an account in the store"),
+ )
if err != nil {
return nil, err
}
- transactionDurationMs, err := meter.Int64Histogram("management.store.transaction.duration.ms")
+ transactionDurationMs, err := meter.Int64Histogram("management.store.transaction.duration.ms",
+ metric.WithUnit("milliseconds"),
+ metric.WithDescription("Duration of how long it takes to execute a transaction in the store"),
+ )
if err != nil {
return nil, err
}
diff --git a/management/server/telemetry/updatechannel_metrics.go b/management/server/telemetry/updatechannel_metrics.go
index 2582006e5..584b9ec20 100644
--- a/management/server/telemetry/updatechannel_metrics.go
+++ b/management/server/telemetry/updatechannel_metrics.go
@@ -23,42 +23,68 @@ type UpdateChannelMetrics struct {
// NewUpdateChannelMetrics creates an instance of UpdateChannel
func NewUpdateChannelMetrics(ctx context.Context, meter metric.Meter) (*UpdateChannelMetrics, error) {
- createChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.create.duration.micro")
+ createChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.create.duration.micro",
+ metric.WithUnit("microseconds"),
+ metric.WithDescription("Duration of how long it takes to create a new peer update channel"),
+ )
if err != nil {
return nil, err
}
- closeChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.one.duration.micro")
+ closeChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.one.duration.micro",
+ metric.WithUnit("microseconds"),
+ metric.WithDescription("Duration of how long it takes to close a peer update channel"),
+ )
if err != nil {
return nil, err
}
- closeChannelsDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.multiple.duration.micro")
+ closeChannelsDurationMicro, err := meter.Int64Histogram("management.updatechannel.close.multiple.duration.micro",
+ metric.WithUnit("microseconds"),
+ metric.WithDescription("Duration of how long it takes to close a set of peer update channels"),
+ )
+
if err != nil {
return nil, err
}
- closeChannels, err := meter.Int64Histogram("management.updatechannel.close.multiple.channels")
+ closeChannels, err := meter.Int64Histogram("management.updatechannel.close.multiple.channels",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of peer update channels that have been closed"),
+ )
+
if err != nil {
return nil, err
}
- sendUpdateDurationMicro, err := meter.Int64Histogram("management.updatechannel.send.duration.micro")
+ sendUpdateDurationMicro, err := meter.Int64Histogram("management.updatechannel.send.duration.micro",
+ metric.WithUnit("microseconds"),
+ metric.WithDescription("Duration of how long it takes to send an network map update to a peer"),
+ )
if err != nil {
return nil, err
}
- getAllConnectedPeersDurationMicro, err := meter.Int64Histogram("management.updatechannel.get.all.duration.micro")
+ getAllConnectedPeersDurationMicro, err := meter.Int64Histogram("management.updatechannel.get.all.duration.micro",
+ metric.WithUnit("microseconds"),
+ metric.WithDescription("Duration of how long it takes to get all connected peers"),
+ )
if err != nil {
return nil, err
}
- getAllConnectedPeers, err := meter.Int64Histogram("management.updatechannel.get.all.peers")
+ getAllConnectedPeers, err := meter.Int64Histogram("management.updatechannel.get.all.peers",
+ metric.WithUnit("1"),
+ metric.WithDescription("Number of connected peers"),
+ )
if err != nil {
return nil, err
}
- hasChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.haschannel.duration.micro")
+ hasChannelDurationMicro, err := meter.Int64Histogram("management.updatechannel.haschannel.duration.micro",
+ metric.WithUnit("microseconds"),
+ metric.WithDescription("Duration of how long it takes to check if a peer has a channel"),
+ )
if err != nil {
return nil, err
}
diff --git a/management/server/testdata/management.json b/management/server/testdata/management.json
index d29491118..f797a7d2b 100644
--- a/management/server/testdata/management.json
+++ b/management/server/testdata/management.json
@@ -2,7 +2,7 @@
"Stuns": [
{
"Proto": "udp",
- "URI": "stun:stun.wiretrustee.com:3468",
+ "URI": "stun:stun.netbird.io:3468",
"Username": "",
"Password": null
}
@@ -11,7 +11,7 @@
"Turns": [
{
"Proto": "udp",
- "URI": "turn:stun.wiretrustee.com:3468",
+ "URI": "turn:stun.netbird.io:3468",
"Username": "some_user",
"Password": "some_password"
}
@@ -22,7 +22,7 @@
},
"Signal": {
"Proto": "http",
- "URI": "signal.wiretrustee.com:10000",
+ "URI": "signal.netbird.io:10000",
"Username": "",
"Password": null
},
@@ -44,4 +44,4 @@
"GrantType": "client_credentials"
}
}
-}
\ No newline at end of file
+}
diff --git a/management/server/testdata/store.sql b/management/server/testdata/store.sql
index 84524127f..41b8fa2f7 100644
--- a/management/server/testdata/store.sql
+++ b/management/server/testdata/store.sql
@@ -19,6 +19,7 @@ CREATE INDEX `idx_accounts_domain` ON `accounts`(`domain`);
CREATE INDEX `idx_setup_keys_account_id` ON `setup_keys`(`account_id`);
CREATE INDEX `idx_peers_key` ON `peers`(`key`);
CREATE INDEX `idx_peers_account_id` ON `peers`(`account_id`);
+CREATE INDEX `idx_peers_account_id_ip` ON `peers`(`account_id`,`ip`);
CREATE INDEX `idx_users_account_id` ON `users`(`account_id`);
CREATE INDEX `idx_personal_access_tokens_user_id` ON `personal_access_tokens`(`user_id`);
CREATE INDEX `idx_groups_account_id` ON `groups`(`account_id`);
@@ -39,8 +40,11 @@ CREATE INDEX `idx_networks_account_id` ON `networks`(`account_id`);
INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
INSERT INTO "groups" VALUES('cs1tnh0hhcjnqoiuebeg','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,'');
INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cs1tnh0hhcjnqoiuebeg"]',0,0);
+INSERT INTO users VALUES('a23efe53-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','owner',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,'');
INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,'');
INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,'');
+-- Unhashed PAT is "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp"
+INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120004','a23efe53-63fb-11ec-90d6-0242ac120003','','smJvzexPcQ3NRezrVDUmF++0XqvFvXzx8Rsn2y9r1z0=','5023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00');
INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120003','f4f6d672-63fb-11ec-90d6-0242ac120003','','SoMeHaShEdToKeN','2023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00');
INSERT INTO installations VALUES(1,'');
INSERT INTO policies VALUES('cs1tnh0hhcjnqoiuebf0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Default','This is a default rule that allows connections between all the resources',1,'[]');
@@ -48,3 +52,4 @@ INSERT INTO policy_rules VALUES('cs387mkv2d4bgq41b6n0','cs1tnh0hhcjnqoiuebf0','D
INSERT INTO network_routers VALUES('ctc20ji7qv9ck2sebc80','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','cs1tnh0hhcjnqoiuebeg',NULL,0,0);
INSERT INTO network_resources VALUES ('ctc4nci7qv9061u6ilfg','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Host','192.168.1.1');
INSERT INTO networks VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Test Network','Test Network');
+INSERT INTO peers VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','','','192.168.0.0','','','','','','','','','','','','','','','','','test','test','2023-01-01 00:00:00+00:00',0,0,0,'a23efe53-63fb-11ec-90d6-0242ac120003','',0,0,'2023-01-01 00:00:00+00:00','2023-01-01 00:00:00+00:00',0,'','','',0);
diff --git a/management/server/testdata/storev1.sql b/management/server/testdata/storev1.sql
index cda333d4f..8b09ec2be 100644
--- a/management/server/testdata/storev1.sql
+++ b/management/server/testdata/storev1.sql
@@ -36,4 +36,3 @@ INSERT INTO peers VALUES('xlx9/9D8+ibnRiIIB8nHGMxGOzxV17r8ShPHgi4aYSM=','auth0|6
INSERT INTO peers VALUES('6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','google-oauth2|103201118415301331038','6kjbmVq1hmucVzvBXo5OucY5OYv+jSsB1jUTLq291Dw=','5AFB60DB-61F2-4251-8E11-494847EE88E9','"100.64.0.2"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini','2021-12-24 16:12:05.994305438+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',0,'""','','',0);
INSERT INTO peers VALUES('Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','google-oauth2|103201118415301331038','Ok+5QMdt/UjoktNOvicGYj+IX2g98p+0N2PJ3vJ45RI=','A72E4DC2-00DE-4542-8A24-62945438104E','"100.64.0.1"','braginini','linux','Linux','21.04','x86_64','Ubuntu','','','','',NULL,'','','','{"Cloud":"","Platform":""}',NULL,'braginini','braginini-1','2021-12-24 16:11:27.015739803+01:00',0,0,0,'','',0,0,NULL,'2024-10-02 17:00:54.228182+02:00',1,'""','','',0);
INSERT INTO installations VALUES(1,'');
-
diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go
index 16438cab8..8672efa7f 100644
--- a/management/server/testutil/store.go
+++ b/management/server/testutil/store.go
@@ -22,7 +22,7 @@ func CreateMysqlTestContainer() (func(), error) {
myContainer, err := mysql.RunContainer(ctx,
testcontainers.WithImage("mlsmaycon/warmed-mysql:8"),
mysql.WithDatabase("testing"),
- mysql.WithUsername("testing"),
+ mysql.WithUsername("root"),
mysql.WithPassword("testing"),
testcontainers.WithWaitStrategy(
wait.ForLog("/usr/sbin/mysqld: ready for connections").
@@ -34,6 +34,7 @@ func CreateMysqlTestContainer() (func(), error) {
}
cleanup := func() {
+ os.Unsetenv("NETBIRD_STORE_ENGINE_MYSQL_DSN")
timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
defer cancelFunc()
if err = myContainer.Terminate(timeoutCtx); err != nil {
@@ -68,6 +69,7 @@ func CreatePostgresTestContainer() (func(), error) {
}
cleanup := func() {
+ os.Unsetenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN")
timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
defer cancelFunc()
if err = pgContainer.Terminate(timeoutCtx); err != nil {
diff --git a/management/server/token_mgr.go b/management/server/token_mgr.go
index fd67fa3e3..ec8aae47e 100644
--- a/management/server/token_mgr.go
+++ b/management/server/token_mgr.go
@@ -199,7 +199,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Cont
}
update := &proto.SyncResponse{
- WiretrusteeConfig: &proto.WiretrusteeConfig{
+ NetbirdConfig: &proto.NetbirdConfig{
Turns: turns,
},
}
@@ -208,7 +208,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewTURNAndRelayTokens(ctx context.Cont
if m.relayCfg != nil {
token, err := m.GenerateRelayToken()
if err == nil {
- update.WiretrusteeConfig.Relay = &proto.RelayConfig{
+ update.NetbirdConfig.Relay = &proto.RelayConfig{
Urls: m.relayCfg.Addresses,
TokenPayload: token.Payload,
TokenSignature: token.Signature,
@@ -228,7 +228,7 @@ func (m *TimeBasedAuthSecretsManager) pushNewRelayTokens(ctx context.Context, pe
}
update := &proto.SyncResponse{
- WiretrusteeConfig: &proto.WiretrusteeConfig{
+ NetbirdConfig: &proto.NetbirdConfig{
Relay: &proto.RelayConfig{
Urls: m.relayCfg.Addresses,
TokenPayload: string(relayToken.Payload),
diff --git a/management/server/token_mgr_test.go b/management/server/token_mgr_test.go
index 2aafb9f68..f2b056d8f 100644
--- a/management/server/token_mgr_test.go
+++ b/management/server/token_mgr_test.go
@@ -18,7 +18,7 @@ import (
var TurnTestHost = &Host{
Proto: UDP,
- URI: "turn:turn.wiretrustee.com:77777",
+ URI: "turn:turn.netbird.io:77777",
Username: "username",
Password: "",
}
@@ -124,7 +124,7 @@ loop:
var firstRelayUpdate, secondRelayUpdate *proto.RelayConfig
for _, update := range updates {
- if turns := update.Update.GetWiretrusteeConfig().GetTurns(); len(turns) > 0 {
+ if turns := update.Update.GetNetbirdConfig().GetTurns(); len(turns) > 0 {
turnUpdates++
if turnUpdates == 1 {
firstTurnUpdate = turns[0]
@@ -132,9 +132,9 @@ loop:
secondTurnUpdate = turns[0]
}
}
- if relay := update.Update.GetWiretrusteeConfig().GetRelay(); relay != nil {
+ if relay := update.Update.GetNetbirdConfig().GetRelay(); relay != nil {
// avoid updating on turn updates since they also send relay credentials
- if update.Update.GetWiretrusteeConfig().GetTurns() == nil {
+ if update.Update.GetNetbirdConfig().GetTurns() == nil {
relayUpdates++
if relayUpdates == 1 {
firstRelayUpdate = relay
diff --git a/management/server/types/account.go b/management/server/types/account.go
index f74d38cb6..c890a7730 100644
--- a/management/server/types/account.go
+++ b/management/server/types/account.go
@@ -12,6 +12,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/miekg/dns"
+ "github.com/rs/xid"
log "github.com/sirupsen/logrus"
nbdns "github.com/netbirdio/netbird/dns"
@@ -289,14 +290,14 @@ func (a *Account) GetPeerNetworkMap(
}
if metrics != nil {
- objectCount := int64(len(peersToConnect) + len(expiredPeers) + len(routesUpdate) + len(firewallRules))
+ objectCount := int64(len(peersToConnectIncludingRouters) + len(expiredPeers) + len(routesUpdate) + len(networkResourcesRoutes) + len(firewallRules) + +len(networkResourcesFirewallRules) + len(routesFirewallRules))
metrics.CountNetworkMapObjects(objectCount)
metrics.CountGetPeerNetworkMapDuration(time.Since(start))
if objectCount > 5000 {
log.WithContext(ctx).Tracef("account: %s has a total resource count of %d objects, "+
- "peers to connect: %d, expired peers: %d, routes: %d, firewall rules: %d",
- a.Id, objectCount, len(peersToConnect), len(expiredPeers), len(routesUpdate), len(firewallRules))
+ "peers to connect: %d, expired peers: %d, routes: %d, firewall rules: %d, network resources routes: %d, network resources firewall rules: %d, routes firewall rules: %d",
+ a.Id, objectCount, len(peersToConnectIncludingRouters), len(expiredPeers), len(routesUpdate), len(firewallRules), len(networkResourcesRoutes), len(networkResourcesFirewallRules), len(routesFirewallRules))
}
}
@@ -459,8 +460,23 @@ func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdn
TTL: defaultTTL,
RData: peer.IP.String(),
})
-
sb.Reset()
+
+ for _, extraLabel := range peer.ExtraDNSLabels {
+ sb.Grow(len(extraLabel) + len(domainSuffix))
+ sb.WriteString(extraLabel)
+ sb.WriteString(domainSuffix)
+
+ customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
+ Name: sb.String(),
+ Type: int(dns.TypeA),
+ Class: nbdns.DefaultClass,
+ TTL: defaultTTL,
+ RData: peer.IP.String(),
+ })
+ sb.Reset()
+ }
+
}
go func() {
@@ -1510,3 +1526,43 @@ func getPoliciesSourcePeers(policies []*Policy, groups map[string]*Group) map[st
return sourcePeers
}
+
+// AddAllGroup to account object if it doesn't exist
+func (a *Account) AddAllGroup() error {
+ if len(a.Groups) == 0 {
+ allGroup := &Group{
+ ID: xid.New().String(),
+ Name: "All",
+ Issued: GroupIssuedAPI,
+ }
+ for _, peer := range a.Peers {
+ allGroup.Peers = append(allGroup.Peers, peer.ID)
+ }
+ a.Groups = map[string]*Group{allGroup.ID: allGroup}
+
+ id := xid.New().String()
+
+ defaultPolicy := &Policy{
+ ID: id,
+ Name: DefaultRuleName,
+ Description: DefaultRuleDescription,
+ Enabled: true,
+ Rules: []*PolicyRule{
+ {
+ ID: id,
+ Name: DefaultRuleName,
+ Description: DefaultRuleDescription,
+ Enabled: true,
+ Sources: []string{allGroup.ID},
+ Destinations: []string{allGroup.ID},
+ Bidirectional: true,
+ Protocol: PolicyRuleProtocolALL,
+ Action: PolicyTrafficActionAccept,
+ },
+ },
+ }
+
+ a.Policies = []*Policy{defaultPolicy}
+ }
+ return nil
+}
diff --git a/management/server/types/policyrule.go b/management/server/types/policyrule.go
index bd9a99292..721621a4b 100644
--- a/management/server/types/policyrule.go
+++ b/management/server/types/policyrule.go
@@ -66,18 +66,20 @@ type PolicyRule struct {
// Copy returns a copy of a policy rule
func (pm *PolicyRule) Copy() *PolicyRule {
rule := &PolicyRule{
- ID: pm.ID,
- PolicyID: pm.PolicyID,
- Name: pm.Name,
- Description: pm.Description,
- Enabled: pm.Enabled,
- Action: pm.Action,
- Destinations: make([]string, len(pm.Destinations)),
- Sources: make([]string, len(pm.Sources)),
- Bidirectional: pm.Bidirectional,
- Protocol: pm.Protocol,
- Ports: make([]string, len(pm.Ports)),
- PortRanges: make([]RulePortRange, len(pm.PortRanges)),
+ ID: pm.ID,
+ PolicyID: pm.PolicyID,
+ Name: pm.Name,
+ Description: pm.Description,
+ Enabled: pm.Enabled,
+ Action: pm.Action,
+ Destinations: make([]string, len(pm.Destinations)),
+ DestinationResource: pm.DestinationResource,
+ Sources: make([]string, len(pm.Sources)),
+ SourceResource: pm.SourceResource,
+ Bidirectional: pm.Bidirectional,
+ Protocol: pm.Protocol,
+ Ports: make([]string, len(pm.Ports)),
+ PortRanges: make([]RulePortRange, len(pm.PortRanges)),
}
copy(rule.Destinations, pm.Destinations)
copy(rule.Sources, pm.Sources)
diff --git a/management/server/types/setupkey.go b/management/server/types/setupkey.go
index 2cd835289..ab8e46bea 100644
--- a/management/server/types/setupkey.go
+++ b/management/server/types/setupkey.go
@@ -10,6 +10,7 @@ import (
"unicode/utf8"
"github.com/google/uuid"
+
"github.com/netbirdio/netbird/management/server/util"
)
@@ -54,6 +55,8 @@ type SetupKey struct {
UsageLimit int
// Ephemeral indicate if the peers will be ephemeral or not
Ephemeral bool
+ // AllowExtraDNSLabels indicates if the key allows extra DNS labels
+ AllowExtraDNSLabels bool
}
// Copy copies SetupKey to a new object
@@ -64,21 +67,22 @@ func (key *SetupKey) Copy() *SetupKey {
key.UpdatedAt = key.CreatedAt
}
return &SetupKey{
- Id: key.Id,
- AccountID: key.AccountID,
- Key: key.Key,
- KeySecret: key.KeySecret,
- Name: key.Name,
- Type: key.Type,
- CreatedAt: key.CreatedAt,
- ExpiresAt: key.ExpiresAt,
- UpdatedAt: key.UpdatedAt,
- Revoked: key.Revoked,
- UsedTimes: key.UsedTimes,
- LastUsed: key.LastUsed,
- AutoGroups: autoGroups,
- UsageLimit: key.UsageLimit,
- Ephemeral: key.Ephemeral,
+ Id: key.Id,
+ AccountID: key.AccountID,
+ Key: key.Key,
+ KeySecret: key.KeySecret,
+ Name: key.Name,
+ Type: key.Type,
+ CreatedAt: key.CreatedAt,
+ ExpiresAt: key.ExpiresAt,
+ UpdatedAt: key.UpdatedAt,
+ Revoked: key.Revoked,
+ UsedTimes: key.UsedTimes,
+ LastUsed: key.LastUsed,
+ AutoGroups: autoGroups,
+ UsageLimit: key.UsageLimit,
+ Ephemeral: key.Ephemeral,
+ AllowExtraDNSLabels: key.AllowExtraDNSLabels,
}
}
@@ -150,7 +154,7 @@ func (key *SetupKey) IsOverUsed() bool {
// GenerateSetupKey generates a new setup key
func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoGroups []string,
- usageLimit int, ephemeral bool) (*SetupKey, string) {
+ usageLimit int, ephemeral bool, allowExtraDNSLabels bool) (*SetupKey, string) {
key := strings.ToUpper(uuid.New().String())
limit := usageLimit
if t == SetupKeyOneOff {
@@ -166,26 +170,27 @@ func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoG
encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
return &SetupKey{
- Id: strconv.Itoa(int(Hash(key))),
- Key: encodedHashedKey,
- KeySecret: HiddenKey(key, 4),
- Name: name,
- Type: t,
- CreatedAt: time.Now().UTC(),
- ExpiresAt: expiresAt,
- UpdatedAt: time.Now().UTC(),
- Revoked: false,
- UsedTimes: 0,
- AutoGroups: autoGroups,
- UsageLimit: limit,
- Ephemeral: ephemeral,
+ Id: strconv.Itoa(int(Hash(key))),
+ Key: encodedHashedKey,
+ KeySecret: HiddenKey(key, 4),
+ Name: name,
+ Type: t,
+ CreatedAt: time.Now().UTC(),
+ ExpiresAt: expiresAt,
+ UpdatedAt: time.Now().UTC(),
+ Revoked: false,
+ UsedTimes: 0,
+ AutoGroups: autoGroups,
+ UsageLimit: limit,
+ Ephemeral: ephemeral,
+ AllowExtraDNSLabels: allowExtraDNSLabels,
}, key
}
// GenerateDefaultSetupKey generates a default reusable setup key with an unlimited usage and 30 days expiration
func GenerateDefaultSetupKey() (*SetupKey, string) {
return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{},
- SetupKeyUnlimitedUsage, false)
+ SetupKeyUnlimitedUsage, false, false)
}
func Hash(s string) uint32 {
diff --git a/management/server/types/user.go b/management/server/types/user.go
index 348fbfb22..5f7a4f2cb 100644
--- a/management/server/types/user.go
+++ b/management/server/types/user.go
@@ -80,7 +80,7 @@ type User struct {
// AutoGroups is a list of Group IDs to auto-assign to peers registered by this user
AutoGroups []string `gorm:"serializer:json"`
PATs map[string]*PersonalAccessToken `gorm:"-"`
- PATsG []PersonalAccessToken `json:"-" gorm:"foreignKey:UserID;references:id"`
+ 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 bool
// LastLogin is the last time the user logged in to IdP
diff --git a/management/server/user.go b/management/server/user.go
index 7a3e04a1d..381879ae6 100644
--- a/management/server/user.go
+++ b/management/server/user.go
@@ -8,16 +8,16 @@ import (
"time"
"github.com/google/uuid"
+ log "github.com/sirupsen/logrus"
+
"github.com/netbirdio/netbird/management/server/activity"
nbContext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/idp"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/status"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/util"
- log "github.com/sirupsen/logrus"
)
// createServiceUser creates a new service user under the given account.
@@ -143,7 +143,7 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
// createNewIdpUser validates the invite and creates a new user in the IdP
func (am *DefaultAccountManager) createNewIdpUser(ctx context.Context, accountID string, inviterID string, invite *types.UserInfo) (*idp.UserData, error) {
// inviterUser is the one who is inviting the new user
- inviterUser, err := am.lookupUserInCache(ctx, am.Store, inviterID, accountID)
+ inviterUser, err := am.lookupUserInCache(ctx, inviterID, accountID)
if err != nil {
return nil, status.Errorf(status.NotFound, "inviter user with ID %s doesn't exist in IdP", inviterID)
}
@@ -174,31 +174,26 @@ func (am *DefaultAccountManager) GetUserByID(ctx context.Context, id string) (*t
return am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, id)
}
-// GetUser looks up a user by provided authorization claims.
-// It will also create an account if didn't exist for this user before.
-func (am *DefaultAccountManager) GetUser(ctx context.Context, claims jwtclaims.AuthorizationClaims) (*types.User, error) {
- accountID, userID, err := am.GetAccountIDFromToken(ctx, claims)
- if err != nil {
- return nil, fmt.Errorf("failed to get account with token claims %v", err)
- }
-
- user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
+// GetUser looks up a user by provided nbContext.UserAuths.
+// Expects account to have been created already.
+func (am *DefaultAccountManager) GetUserFromUserAuth(ctx context.Context, userAuth nbContext.UserAuth) (*types.User, error) {
+ user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userAuth.UserId)
if err != nil {
return nil, err
}
// this code should be outside of the am.GetAccountIDFromToken(claims) because this method is called also by the gRPC
// server when user authenticates a device. And we need to separate the Dashboard login event from the Device login event.
- newLogin := user.LastDashboardLoginChanged(claims.LastLogin)
+ newLogin := user.LastDashboardLoginChanged(userAuth.LastLogin)
- err = am.Store.SaveUserLastLogin(ctx, accountID, userID, claims.LastLogin)
+ err = am.Store.SaveUserLastLogin(ctx, userAuth.AccountId, userAuth.UserId, userAuth.LastLogin)
if err != nil {
log.WithContext(ctx).Errorf("failed saving user last login: %v", err)
}
if newLogin {
- meta := map[string]any{"timestamp": claims.LastLogin}
- am.StoreEvent(ctx, claims.UserId, claims.UserId, accountID, activity.DashboardLogin, meta)
+ meta := map[string]any{"timestamp": userAuth.LastLogin}
+ am.StoreEvent(ctx, userAuth.UserId, userAuth.UserId, userAuth.AccountId, activity.DashboardLogin, meta)
}
return user, nil
@@ -207,8 +202,6 @@ func (am *DefaultAccountManager) GetUser(ctx context.Context, claims jwtclaims.A
// ListUsers returns lists of all users under the account.
// It doesn't populate user information such as email or name.
func (am *DefaultAccountManager) ListUsers(ctx context.Context, accountID string) ([]*types.User, error) {
- unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
- defer unlock()
return am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID)
}
@@ -266,7 +259,12 @@ func (am *DefaultAccountManager) DeleteUser(ctx context.Context, accountID, init
return am.deleteServiceUser(ctx, accountID, initiatorUserID, targetUser)
}
- updateAccountPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, targetUserID)
+ userInfo, err := am.getUserInfo(ctx, targetUser, accountID)
+ if err != nil {
+ return err
+ }
+
+ updateAccountPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, userInfo)
if err != nil {
return err
}
@@ -297,7 +295,7 @@ func (am *DefaultAccountManager) InviteUser(ctx context.Context, accountID strin
}
// check if the user is already registered with this ID
- user, err := am.lookupUserInCache(ctx, am.Store, targetUserID, accountID)
+ user, err := am.lookupUserInCache(ctx, targetUserID, accountID)
if err != nil {
return err
}
@@ -495,7 +493,6 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID,
var peersToExpire []*nbpeer.Peer
var addUserEvents []func()
var usersToSave = make([]*types.User, 0, len(updates))
- var updatedUsersInfo = make([]*types.UserInfo, 0, len(updates))
groups, err := am.Store.GetAccountGroups(ctx, store.LockingStrengthShare, accountID)
if err != nil {
@@ -526,20 +523,28 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID,
if userHadPeers {
updateAccountPeers = true
}
-
- updatedUserInfo, err := am.getUserInfo(ctx, transaction, updatedUser, accountID)
- if err != nil {
- return fmt.Errorf("failed to get user info: %w", err)
- }
- updatedUsersInfo = append(updatedUsersInfo, updatedUserInfo)
}
-
return transaction.SaveUsers(ctx, store.LockingStrengthUpdate, usersToSave)
})
if err != nil {
return nil, err
}
+ var updatedUsersInfo = make([]*types.UserInfo, 0, len(updates))
+
+ userInfos, err := am.GetUsersFromAccount(ctx, accountID, initiatorUserID)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, updatedUser := range usersToSave {
+ updatedUserInfo, ok := userInfos[updatedUser.Id]
+ if !ok || updatedUserInfo == nil {
+ return nil, fmt.Errorf("failed to get user: %s updated user info", updatedUser.Id)
+ }
+ updatedUsersInfo = append(updatedUsersInfo, updatedUserInfo)
+ }
+
for _, addUserEvent := range addUserEvents {
addUserEvent()
}
@@ -682,14 +687,14 @@ func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initi
// getUserInfo retrieves the UserInfo for a given User and Account.
// If the AccountManager has a non-nil idpManager and the User is not a service user,
// it will attempt to look up the UserData from the cache.
-func (am *DefaultAccountManager) getUserInfo(ctx context.Context, transaction store.Store, user *types.User, accountID string) (*types.UserInfo, error) {
- settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.User, accountID string) (*types.UserInfo, error) {
+ settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
if !isNil(am.idpManager) && !user.IsServiceUser {
- userData, err := am.lookupUserInCache(ctx, transaction, user.Id, accountID)
+ userData, err := am.lookupUserInCache(ctx, user.Id, accountID)
if err != nil {
return nil, err
}
@@ -774,22 +779,34 @@ func (am *DefaultAccountManager) GetOrCreateAccountByUser(ctx context.Context, u
// GetUsersFromAccount performs a batched request for users from IDP by account ID apply filter on what data to return
// based on provided user role.
-func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accountID, userID string) ([]*types.UserInfo, error) {
+func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accountID, initiatorUserID string) (map[string]*types.UserInfo, error) {
accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
- user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
+ initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
if err != nil {
return nil, err
}
- if user.AccountID != accountID {
+ if initiatorUser.AccountID != accountID {
return nil, status.NewUserNotPartOfAccountError()
}
- queriedUsers := make([]*idp.UserData, 0)
+ return am.BuildUserInfosForAccount(ctx, accountID, initiatorUserID, accountUsers)
+}
+
+// BuildUserInfosForAccount builds user info for the given account.
+func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error) {
+ var queriedUsers []*idp.UserData
+ var err error
+
+ initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
+ if err != nil {
+ return nil, err
+ }
+
if !isNil(am.idpManager) {
users := make(map[string]userLoggedInOnce, len(accountUsers))
usersFromIntegration := make([]*idp.UserData, 0)
@@ -818,31 +835,33 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun
queriedUsers = append(queriedUsers, usersFromIntegration...)
}
- userInfos := make([]*types.UserInfo, 0)
-
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return nil, err
}
+ userInfosMap := make(map[string]*types.UserInfo)
+
// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
if len(queriedUsers) == 0 {
for _, accountUser := range accountUsers {
- if user.IsRegularUser() && user.Id != accountUser.Id {
+ if initiatorUser.IsRegularUser() && initiatorUser.Id != accountUser.Id {
// if user is not an admin then show only current user and do not show other users
continue
}
+
info, err := accountUser.ToUserInfo(nil, settings)
if err != nil {
return nil, err
}
- userInfos = append(userInfos, info)
+ userInfosMap[accountUser.Id] = info
}
- return userInfos, nil
+
+ return userInfosMap, nil
}
for _, localUser := range accountUsers {
- if user.IsRegularUser() && user.Id != localUser.Id {
+ if initiatorUser.IsRegularUser() && initiatorUser.Id != localUser.Id {
// if user is not an admin then show only current user and do not show other users
continue
}
@@ -879,10 +898,10 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun
Permissions: types.UserPermissions{DashboardView: dashboardViewPermissions},
}
}
- userInfos = append(userInfos, info)
+ userInfosMap[info.ID] = info
}
- return userInfos, nil
+ return userInfosMap, nil
}
// expireAndUpdatePeers expires all peers of the given user and updates them in the account
@@ -936,27 +955,13 @@ func (am *DefaultAccountManager) deleteUserFromIDP(ctx context.Context, targetUs
return nil
}
-func (am *DefaultAccountManager) getEmailAndNameOfTargetUser(ctx context.Context, accountId, initiatorId, targetId string) (string, string, error) {
- userInfos, err := am.GetUsersFromAccount(ctx, accountId, initiatorId)
- if err != nil {
- return "", "", err
- }
- for _, ui := range userInfos {
- if ui.ID == targetId {
- return ui.Email, ui.Name, nil
- }
- }
-
- return "", "", fmt.Errorf("user info not found for user: %s", targetId)
-}
-
// DeleteRegularUsers deletes regular users from an account.
// Note: This function does not acquire the global lock.
// It is the caller's responsibility to ensure proper locking is in place before invoking this method.
//
// If an error occurs while deleting the user, the function skips it and continues deleting other users.
// Errors are collected and returned at the end.
-func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string) error {
+func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, accountID, initiatorUserID string, targetUserIDs []string, userInfos map[string]*types.UserInfo) error {
initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
if err != nil {
return err
@@ -992,7 +997,13 @@ func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, account
continue
}
- userHadPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, targetUserID)
+ userInfo, ok := userInfos[targetUserID]
+ if !ok || userInfo == nil {
+ allErrors = errors.Join(allErrors, fmt.Errorf("user info not found for user: %s", targetUserID))
+ continue
+ }
+
+ userHadPeers, err := am.deleteRegularUser(ctx, accountID, initiatorUserID, userInfo)
if err != nil {
allErrors = errors.Join(allErrors, err)
continue
@@ -1011,53 +1022,48 @@ func (am *DefaultAccountManager) DeleteRegularUsers(ctx context.Context, account
}
// deleteRegularUser deletes a specified user and their related peers from the account.
-func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountID, initiatorUserID, targetUserID string) (bool, error) {
- tuEmail, tuName, err := am.getEmailAndNameOfTargetUser(ctx, accountID, initiatorUserID, targetUserID)
- if err != nil {
- log.WithContext(ctx).Errorf("failed to resolve email address: %s", err)
- return false, err
- }
-
+func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountID, initiatorUserID string, targetUserInfo *types.UserInfo) (bool, error) {
if !isNil(am.idpManager) {
// Delete if the user already exists in the IdP. Necessary in cases where a user account
// was created where a user account was provisioned but the user did not sign in
- _, err = am.idpManager.GetUserDataByID(ctx, targetUserID, idp.AppMetadata{WTAccountID: accountID})
+ _, err := am.idpManager.GetUserDataByID(ctx, targetUserInfo.ID, idp.AppMetadata{WTAccountID: accountID})
if err == nil {
- err = am.deleteUserFromIDP(ctx, targetUserID, accountID)
+ err = am.deleteUserFromIDP(ctx, targetUserInfo.ID, accountID)
if err != nil {
- log.WithContext(ctx).Debugf("failed to delete user from IDP: %s", targetUserID)
+ log.WithContext(ctx).Debugf("failed to delete user from IDP: %s", targetUserInfo.ID)
return false, err
}
} else {
- log.WithContext(ctx).Debugf("skipped deleting user %s from IDP, error: %v", targetUserID, err)
+ log.WithContext(ctx).Debugf("skipped deleting user %s from IDP, error: %v", targetUserInfo.ID, err)
}
}
var addPeerRemovedEvents []func()
var updateAccountPeers bool
var targetUser *types.User
+ var err error
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
- targetUser, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserID)
+ targetUser, err = transaction.GetUserByUserID(ctx, store.LockingStrengthShare, targetUserInfo.ID)
if err != nil {
return fmt.Errorf("failed to get user to delete: %w", err)
}
- userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, targetUserID)
+ userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthShare, accountID, targetUserInfo.ID)
if err != nil {
return fmt.Errorf("failed to get user peers: %w", err)
}
if len(userPeers) > 0 {
updateAccountPeers = true
- addPeerRemovedEvents, err = deletePeers(ctx, am, transaction, accountID, targetUserID, userPeers)
+ addPeerRemovedEvents, err = deletePeers(ctx, am, transaction, accountID, targetUserInfo.ID, userPeers)
if err != nil {
return fmt.Errorf("failed to delete user peers: %w", err)
}
}
- if err = transaction.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUserID); err != nil {
- return fmt.Errorf("failed to delete user: %s %w", targetUserID, err)
+ if err = transaction.DeleteUser(ctx, store.LockingStrengthUpdate, accountID, targetUserInfo.ID); err != nil {
+ return fmt.Errorf("failed to delete user: %s %w", targetUserInfo.ID, err)
}
return nil
@@ -1069,7 +1075,7 @@ func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountI
for _, addPeerRemovedEvent := range addPeerRemovedEvents {
addPeerRemovedEvent()
}
- meta := map[string]any{"name": tuName, "email": tuEmail, "created_at": targetUser.CreatedAt}
+ meta := map[string]any{"name": targetUserInfo.Name, "email": targetUserInfo.Email, "created_at": targetUser.CreatedAt}
am.StoreEvent(ctx, initiatorUserID, targetUser.Id, accountID, activity.UserDeleted, meta)
return updateAccountPeers, nil
diff --git a/management/server/user_test.go b/management/server/user_test.go
index 5c4b1e2cb..a180a761a 100644
--- a/management/server/user_test.go
+++ b/management/server/user_test.go
@@ -10,7 +10,10 @@ import (
"github.com/eko/gocache/v3/cache"
cacheStore "github.com/eko/gocache/v3/store"
"github.com/google/go-cmp/cmp"
+
+ nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/util"
+ "golang.org/x/exp/maps"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/store"
@@ -24,7 +27,6 @@ import (
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/integration_reference"
- "github.com/netbirdio/netbird/management/server/jwtclaims"
)
const (
@@ -867,7 +869,10 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- err = am.DeleteRegularUsers(context.Background(), mockAccountID, mockUserID, tc.userIDs)
+ userInfos, err := am.BuildUserInfosForAccount(context.Background(), mockAccountID, mockUserID, maps.Values(account.Users))
+ assert.NoError(t, err)
+
+ err = am.DeleteRegularUsers(context.Background(), mockAccountID, mockUserID, tc.userIDs, userInfos)
if len(tc.expectedReasons) > 0 {
assert.Error(t, err)
var foundExpectedErrors int
@@ -921,11 +926,12 @@ func TestDefaultAccountManager_GetUser(t *testing.T) {
eventStore: &activity.InMemoryEventStore{},
}
- claims := jwtclaims.AuthorizationClaims{
- UserId: mockUserID,
+ claims := nbcontext.UserAuth{
+ UserId: mockUserID,
+ AccountId: mockAccountID,
}
- user, err := am.GetUser(context.Background(), claims)
+ user, err := am.GetUserFromUserAuth(context.Background(), claims)
if err != nil {
t.Fatalf("Error when checking user role: %s", err)
}
diff --git a/relay/client/client.go b/relay/client/client.go
index db5252f50..9e7e54393 100644
--- a/relay/client/client.go
+++ b/relay/client/client.go
@@ -10,6 +10,8 @@ import (
log "github.com/sirupsen/logrus"
auth "github.com/netbirdio/netbird/relay/auth/hmac"
+ "github.com/netbirdio/netbird/relay/client/dialer"
+ "github.com/netbirdio/netbird/relay/client/dialer/quic"
"github.com/netbirdio/netbird/relay/client/dialer/ws"
"github.com/netbirdio/netbird/relay/healthcheck"
"github.com/netbirdio/netbird/relay/messages"
@@ -95,8 +97,6 @@ func (cc *connContainer) writeMsg(msg Msg) {
msg.Free()
default:
msg.Free()
- cc.log.Infof("message queue is full")
- // todo consider to close the connection
}
}
@@ -141,7 +141,6 @@ type Client struct {
muInstanceURL sync.Mutex
onDisconnectListener func(string)
- onConnectedListener func()
listenerMutex sync.Mutex
}
@@ -179,8 +178,7 @@ func (c *Client) Connect() error {
return nil
}
- err := c.connect()
- if err != nil {
+ if err := c.connect(); err != nil {
return err
}
@@ -191,7 +189,6 @@ func (c *Client) Connect() error {
c.wgReadLoop.Add(1)
go c.readLoop(c.relayConn)
- go c.notifyConnected()
return nil
}
@@ -239,12 +236,6 @@ func (c *Client) SetOnDisconnectListener(fn func(string)) {
c.onDisconnectListener = fn
}
-func (c *Client) SetOnConnectedListener(fn func()) {
- c.listenerMutex.Lock()
- defer c.listenerMutex.Unlock()
- c.onConnectedListener = fn
-}
-
// HasConns returns true if there are connections.
func (c *Client) HasConns() bool {
c.mu.Lock()
@@ -264,14 +255,14 @@ func (c *Client) Close() error {
}
func (c *Client) connect() error {
- conn, err := ws.Dial(c.connectionURL)
+ rd := dialer.NewRaceDial(c.log, c.connectionURL, quic.Dialer{}, ws.Dialer{})
+ conn, err := rd.Dial()
if err != nil {
return err
}
c.relayConn = conn
- err = c.handShake()
- if err != nil {
+ if err = c.handShake(); err != nil {
cErr := conn.Close()
if cErr != nil {
c.log.Errorf("failed to close connection: %s", cErr)
@@ -306,7 +297,7 @@ func (c *Client) handShake() error {
return fmt.Errorf("validate version: %w", err)
}
- msgType, err := messages.DetermineServerMessageType(buf[messages.SizeOfVersionByte:n])
+ msgType, err := messages.DetermineServerMessageType(buf[:n])
if err != nil {
c.log.Errorf("failed to determine message type: %s", err)
return err
@@ -317,7 +308,7 @@ func (c *Client) handShake() error {
return fmt.Errorf("unexpected message type")
}
- addr, err := messages.UnmarshalAuthResponse(buf[messages.SizeOfProtoHeader:n])
+ addr, err := messages.UnmarshalAuthResponse(buf[:n])
if err != nil {
return err
}
@@ -345,27 +336,30 @@ func (c *Client) readLoop(relayConn net.Conn) {
c.log.Infof("start to Relay read loop exit")
c.mu.Lock()
if c.serviceIsRunning && !internallyStoppedFlag.isSet() {
- c.log.Debugf("failed to read message from relay server: %s", errExit)
+ c.log.Errorf("failed to read message from relay server: %s", errExit)
}
c.mu.Unlock()
+ c.bufPool.Put(bufPtr)
break
}
- _, err := messages.ValidateVersion(buf[:n])
+ buf = buf[:n]
+
+ _, err := messages.ValidateVersion(buf)
if err != nil {
c.log.Errorf("failed to validate protocol version: %s", err)
c.bufPool.Put(bufPtr)
continue
}
- msgType, err := messages.DetermineServerMessageType(buf[messages.SizeOfVersionByte:n])
+ msgType, err := messages.DetermineServerMessageType(buf)
if err != nil {
c.log.Errorf("failed to determine message type: %s", err)
c.bufPool.Put(bufPtr)
continue
}
- if !c.handleMsg(msgType, buf[messages.SizeOfProtoHeader:n], bufPtr, hc, internallyStoppedFlag) {
+ if !c.handleMsg(msgType, buf, bufPtr, hc, internallyStoppedFlag) {
break
}
}
@@ -557,16 +551,6 @@ func (c *Client) notifyDisconnected() {
go c.onDisconnectListener(c.connectionURL)
}
-func (c *Client) notifyConnected() {
- c.listenerMutex.Lock()
- defer c.listenerMutex.Unlock()
-
- if c.onConnectedListener == nil {
- return
- }
- go c.onConnectedListener()
-}
-
func (c *Client) writeCloseMsg() {
msg := messages.MarshalCloseMsg()
_, err := c.relayConn.Write(msg)
diff --git a/relay/client/dialer/net/err.go b/relay/client/dialer/net/err.go
new file mode 100644
index 000000000..fee844963
--- /dev/null
+++ b/relay/client/dialer/net/err.go
@@ -0,0 +1,7 @@
+package net
+
+import "errors"
+
+var (
+ ErrClosedByServer = errors.New("closed by server")
+)
diff --git a/relay/client/dialer/quic/conn.go b/relay/client/dialer/quic/conn.go
new file mode 100644
index 000000000..d64633c8c
--- /dev/null
+++ b/relay/client/dialer/quic/conn.go
@@ -0,0 +1,97 @@
+package quic
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "time"
+
+ "github.com/quic-go/quic-go"
+ log "github.com/sirupsen/logrus"
+
+ netErr "github.com/netbirdio/netbird/relay/client/dialer/net"
+)
+
+const (
+ Network = "quic"
+)
+
+type Addr struct {
+ addr string
+}
+
+func (a Addr) Network() string {
+ return Network
+}
+
+func (a Addr) String() string {
+ return a.addr
+}
+
+type Conn struct {
+ session quic.Connection
+ ctx context.Context
+}
+
+func NewConn(session quic.Connection) net.Conn {
+ return &Conn{
+ session: session,
+ ctx: context.Background(),
+ }
+}
+
+func (c *Conn) Read(b []byte) (n int, err error) {
+ dgram, err := c.session.ReceiveDatagram(c.ctx)
+ if err != nil {
+ return 0, c.remoteCloseErrHandling(err)
+ }
+
+ n = copy(b, dgram)
+ return n, nil
+}
+
+func (c *Conn) Write(b []byte) (int, error) {
+ err := c.session.SendDatagram(b)
+ if err != nil {
+ err = c.remoteCloseErrHandling(err)
+ log.Errorf("failed to write to QUIC stream: %v", err)
+ return 0, err
+ }
+ return len(b), nil
+}
+
+func (c *Conn) RemoteAddr() net.Addr {
+ return c.session.RemoteAddr()
+}
+
+func (c *Conn) LocalAddr() net.Addr {
+ if c.session != nil {
+ return c.session.LocalAddr()
+ }
+ return Addr{addr: "unknown"}
+}
+
+func (c *Conn) SetReadDeadline(t time.Time) error {
+ return fmt.Errorf("SetReadDeadline is not implemented")
+}
+
+func (c *Conn) SetWriteDeadline(t time.Time) error {
+ return fmt.Errorf("SetWriteDeadline is not implemented")
+}
+
+func (c *Conn) SetDeadline(t time.Time) error {
+ return nil
+}
+
+func (c *Conn) Close() error {
+ return c.session.CloseWithError(0, "normal closure")
+}
+
+func (c *Conn) remoteCloseErrHandling(err error) error {
+ var appErr *quic.ApplicationError
+ if errors.As(err, &appErr) && appErr.ErrorCode == 0x0 {
+ return netErr.ErrClosedByServer
+ }
+ return err
+}
diff --git a/relay/client/dialer/quic/quic.go b/relay/client/dialer/quic/quic.go
new file mode 100644
index 000000000..7fd486f87
--- /dev/null
+++ b/relay/client/dialer/quic/quic.go
@@ -0,0 +1,72 @@
+package quic
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "strings"
+ "time"
+
+ "github.com/quic-go/quic-go"
+ log "github.com/sirupsen/logrus"
+
+ quictls "github.com/netbirdio/netbird/relay/tls"
+ nbnet "github.com/netbirdio/netbird/util/net"
+)
+
+type Dialer struct {
+}
+
+func (d Dialer) Protocol() string {
+ return Network
+}
+
+func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) {
+ quicURL, err := prepareURL(address)
+ if err != nil {
+ return nil, err
+ }
+
+ quicConfig := &quic.Config{
+ KeepAlivePeriod: 30 * time.Second,
+ MaxIdleTimeout: 4 * time.Minute,
+ EnableDatagrams: true,
+ InitialPacketSize: 1452,
+ }
+
+ udpConn, err := nbnet.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
+ if err != nil {
+ log.Errorf("failed to listen on UDP: %s", err)
+ return nil, err
+ }
+
+ udpAddr, err := net.ResolveUDPAddr("udp", quicURL)
+ if err != nil {
+ log.Errorf("failed to resolve UDP address: %s", err)
+ return nil, err
+ }
+
+ session, err := quic.Dial(ctx, udpConn, udpAddr, quictls.ClientQUICTLSConfig(), quicConfig)
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ return nil, err
+ }
+ log.Errorf("failed to dial to Relay server via QUIC '%s': %s", quicURL, err)
+ return nil, err
+ }
+
+ conn := NewConn(session)
+ return conn, nil
+}
+
+func prepareURL(address string) (string, error) {
+ if !strings.HasPrefix(address, "rel://") && !strings.HasPrefix(address, "rels://") {
+ return "", fmt.Errorf("unsupported scheme: %s", address)
+ }
+
+ if strings.HasPrefix(address, "rels://") {
+ return address[7:], nil
+ }
+ return address[6:], nil
+}
diff --git a/relay/client/dialer/race_dialer.go b/relay/client/dialer/race_dialer.go
new file mode 100644
index 000000000..11dba5799
--- /dev/null
+++ b/relay/client/dialer/race_dialer.go
@@ -0,0 +1,96 @@
+package dialer
+
+import (
+ "context"
+ "errors"
+ "net"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+var (
+ connectionTimeout = 30 * time.Second
+)
+
+type DialeFn interface {
+ Dial(ctx context.Context, address string) (net.Conn, error)
+ Protocol() string
+}
+
+type dialResult struct {
+ Conn net.Conn
+ Protocol string
+ Err error
+}
+
+type RaceDial struct {
+ log *log.Entry
+ serverURL string
+ dialerFns []DialeFn
+}
+
+func NewRaceDial(log *log.Entry, serverURL string, dialerFns ...DialeFn) *RaceDial {
+ return &RaceDial{
+ log: log,
+ serverURL: serverURL,
+ dialerFns: dialerFns,
+ }
+}
+
+func (r *RaceDial) Dial() (net.Conn, error) {
+ connChan := make(chan dialResult, len(r.dialerFns))
+ winnerConn := make(chan net.Conn, 1)
+ abortCtx, abort := context.WithCancel(context.Background())
+ defer abort()
+
+ for _, dfn := range r.dialerFns {
+ go r.dial(dfn, abortCtx, connChan)
+ }
+
+ go r.processResults(connChan, winnerConn, abort)
+
+ conn, ok := <-winnerConn
+ if !ok {
+ return nil, errors.New("failed to dial to Relay server on any protocol")
+ }
+ return conn, nil
+}
+
+func (r *RaceDial) dial(dfn DialeFn, abortCtx context.Context, connChan chan dialResult) {
+ ctx, cancel := context.WithTimeout(abortCtx, connectionTimeout)
+ defer cancel()
+
+ r.log.Infof("dialing Relay server via %s", dfn.Protocol())
+ conn, err := dfn.Dial(ctx, r.serverURL)
+ connChan <- dialResult{Conn: conn, Protocol: dfn.Protocol(), Err: err}
+}
+
+func (r *RaceDial) processResults(connChan chan dialResult, winnerConn chan net.Conn, abort context.CancelFunc) {
+ var hasWinner bool
+ for i := 0; i < len(r.dialerFns); i++ {
+ dr := <-connChan
+ if dr.Err != nil {
+ if errors.Is(dr.Err, context.Canceled) {
+ r.log.Infof("connection attempt aborted via: %s", dr.Protocol)
+ } else {
+ r.log.Errorf("failed to dial via %s: %s", dr.Protocol, dr.Err)
+ }
+ continue
+ }
+
+ if hasWinner {
+ if cerr := dr.Conn.Close(); cerr != nil {
+ r.log.Warnf("failed to close connection via %s: %s", dr.Protocol, cerr)
+ }
+ continue
+ }
+
+ r.log.Infof("successfully dialed via: %s", dr.Protocol)
+
+ abort()
+ hasWinner = true
+ winnerConn <- dr.Conn
+ }
+ close(winnerConn)
+}
diff --git a/relay/client/dialer/race_dialer_test.go b/relay/client/dialer/race_dialer_test.go
new file mode 100644
index 000000000..989abb0a6
--- /dev/null
+++ b/relay/client/dialer/race_dialer_test.go
@@ -0,0 +1,252 @@
+package dialer
+
+import (
+ "context"
+ "errors"
+ "net"
+ "testing"
+ "time"
+
+ "github.com/sirupsen/logrus"
+)
+
+type MockAddr struct {
+ network string
+}
+
+func (m *MockAddr) Network() string {
+ return m.network
+}
+
+func (m *MockAddr) String() string {
+ return "1.2.3.4"
+}
+
+// MockDialer is a mock implementation of DialeFn
+type MockDialer struct {
+ dialFunc func(ctx context.Context, address string) (net.Conn, error)
+ protocolStr string
+}
+
+func (m *MockDialer) Dial(ctx context.Context, address string) (net.Conn, error) {
+ return m.dialFunc(ctx, address)
+}
+
+func (m *MockDialer) Protocol() string {
+ return m.protocolStr
+}
+
+// MockConn implements net.Conn for testing
+type MockConn struct {
+ remoteAddr net.Addr
+}
+
+func (m *MockConn) Read(b []byte) (n int, err error) {
+ return 0, nil
+}
+
+func (m *MockConn) Write(b []byte) (n int, err error) {
+ return 0, nil
+}
+
+func (m *MockConn) Close() error {
+ return nil
+}
+
+func (m *MockConn) LocalAddr() net.Addr {
+ return nil
+}
+
+func (m *MockConn) RemoteAddr() net.Addr {
+ return m.remoteAddr
+}
+
+func (m *MockConn) SetDeadline(t time.Time) error {
+ return nil
+}
+
+func (m *MockConn) SetReadDeadline(t time.Time) error {
+ return nil
+}
+
+func (m *MockConn) SetWriteDeadline(t time.Time) error {
+ return nil
+}
+
+func TestRaceDialEmptyDialers(t *testing.T) {
+ logger := logrus.NewEntry(logrus.New())
+ serverURL := "test.server.com"
+
+ rd := NewRaceDial(logger, serverURL)
+ conn, err := rd.Dial()
+ if err == nil {
+ t.Errorf("Expected an error with empty dialers, got nil")
+ }
+ if conn != nil {
+ t.Errorf("Expected nil connection with empty dialers, got %v", conn)
+ }
+}
+
+func TestRaceDialSingleSuccessfulDialer(t *testing.T) {
+ logger := logrus.NewEntry(logrus.New())
+ serverURL := "test.server.com"
+ proto := "test-protocol"
+
+ mockConn := &MockConn{
+ remoteAddr: &MockAddr{network: proto},
+ }
+
+ mockDialer := &MockDialer{
+ dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
+ return mockConn, nil
+ },
+ protocolStr: proto,
+ }
+
+ rd := NewRaceDial(logger, serverURL, mockDialer)
+ conn, err := rd.Dial()
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+ if conn == nil {
+ t.Errorf("Expected non-nil connection")
+ }
+}
+
+func TestRaceDialMultipleDialersWithOneSuccess(t *testing.T) {
+ logger := logrus.NewEntry(logrus.New())
+ serverURL := "test.server.com"
+ proto2 := "protocol2"
+
+ mockConn2 := &MockConn{
+ remoteAddr: &MockAddr{network: proto2},
+ }
+
+ mockDialer1 := &MockDialer{
+ dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
+ return nil, errors.New("first dialer failed")
+ },
+ protocolStr: "proto1",
+ }
+
+ mockDialer2 := &MockDialer{
+ dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
+ return mockConn2, nil
+ },
+ protocolStr: "proto2",
+ }
+
+ rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2)
+ conn, err := rd.Dial()
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+ if conn.RemoteAddr().Network() != proto2 {
+ t.Errorf("Expected connection with protocol %s, got %s", proto2, conn.RemoteAddr().Network())
+ }
+}
+
+func TestRaceDialTimeout(t *testing.T) {
+ logger := logrus.NewEntry(logrus.New())
+ serverURL := "test.server.com"
+
+ connectionTimeout = 3 * time.Second
+ mockDialer := &MockDialer{
+ dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
+ <-ctx.Done()
+ return nil, ctx.Err()
+ },
+ protocolStr: "proto1",
+ }
+
+ rd := NewRaceDial(logger, serverURL, mockDialer)
+ conn, err := rd.Dial()
+ if err == nil {
+ t.Errorf("Expected an error, got nil")
+ }
+ if conn != nil {
+ t.Errorf("Expected nil connection, got %v", conn)
+ }
+}
+
+func TestRaceDialAllDialersFail(t *testing.T) {
+ logger := logrus.NewEntry(logrus.New())
+ serverURL := "test.server.com"
+
+ mockDialer1 := &MockDialer{
+ dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
+ return nil, errors.New("first dialer failed")
+ },
+ protocolStr: "protocol1",
+ }
+
+ mockDialer2 := &MockDialer{
+ dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
+ return nil, errors.New("second dialer failed")
+ },
+ protocolStr: "protocol2",
+ }
+
+ rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2)
+ conn, err := rd.Dial()
+ if err == nil {
+ t.Errorf("Expected an error, got nil")
+ }
+ if conn != nil {
+ t.Errorf("Expected nil connection, got %v", conn)
+ }
+}
+
+func TestRaceDialFirstSuccessfulDialerWins(t *testing.T) {
+ logger := logrus.NewEntry(logrus.New())
+ serverURL := "test.server.com"
+ proto1 := "protocol1"
+ proto2 := "protocol2"
+
+ mockConn1 := &MockConn{
+ remoteAddr: &MockAddr{network: proto1},
+ }
+
+ mockConn2 := &MockConn{
+ remoteAddr: &MockAddr{network: proto2},
+ }
+
+ mockDialer1 := &MockDialer{
+ dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
+ time.Sleep(1 * time.Second)
+ return mockConn1, nil
+ },
+ protocolStr: proto1,
+ }
+
+ mock2err := make(chan error)
+ mockDialer2 := &MockDialer{
+ dialFunc: func(ctx context.Context, address string) (net.Conn, error) {
+ <-ctx.Done()
+ mock2err <- ctx.Err()
+ return mockConn2, ctx.Err()
+ },
+ protocolStr: proto2,
+ }
+
+ rd := NewRaceDial(logger, serverURL, mockDialer1, mockDialer2)
+ conn, err := rd.Dial()
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+ if conn == nil {
+ t.Errorf("Expected non-nil connection")
+ }
+ if conn != mockConn1 {
+ t.Errorf("Expected first connection, got %v", conn)
+ }
+
+ select {
+ case <-time.After(3 * time.Second):
+ t.Errorf("Timed out waiting for second dialer to finish")
+ case err := <-mock2err:
+ if !errors.Is(err, context.Canceled) {
+ t.Errorf("Expected context.Canceled error, got %v", err)
+ }
+ }
+}
diff --git a/relay/client/dialer/ws/addr.go b/relay/client/dialer/ws/addr.go
index 43f5dd6af..11158cfbd 100644
--- a/relay/client/dialer/ws/addr.go
+++ b/relay/client/dialer/ws/addr.go
@@ -1,11 +1,15 @@
package ws
+const (
+ Network = "ws"
+)
+
type WebsocketAddr struct {
addr string
}
func (a WebsocketAddr) Network() string {
- return "websocket"
+ return Network
}
func (a WebsocketAddr) String() string {
diff --git a/relay/client/dialer/ws/conn.go b/relay/client/dialer/ws/conn.go
index e7f771b8d..0086b702b 100644
--- a/relay/client/dialer/ws/conn.go
+++ b/relay/client/dialer/ws/conn.go
@@ -6,7 +6,7 @@ import (
"net"
"time"
- "nhooyr.io/websocket"
+ "github.com/coder/websocket"
)
type Conn struct {
@@ -26,6 +26,7 @@ func NewConn(wsConn *websocket.Conn, serverAddress string) net.Conn {
func (c *Conn) Read(b []byte) (n int, err error) {
t, ioReader, err := c.Conn.Reader(c.ctx)
if err != nil {
+ // todo use ErrClosedByServer
return 0, err
}
diff --git a/relay/client/dialer/ws/ws.go b/relay/client/dialer/ws/ws.go
index d9388aafd..cb525865b 100644
--- a/relay/client/dialer/ws/ws.go
+++ b/relay/client/dialer/ws/ws.go
@@ -2,20 +2,31 @@ package ws
import (
"context"
+ "crypto/tls"
+ "crypto/x509"
+ "errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
+ "github.com/coder/websocket"
log "github.com/sirupsen/logrus"
- "nhooyr.io/websocket"
"github.com/netbirdio/netbird/relay/server/listener/ws"
+ "github.com/netbirdio/netbird/util/embeddedroots"
nbnet "github.com/netbirdio/netbird/util/net"
)
-func Dial(address string) (net.Conn, error) {
+type Dialer struct {
+}
+
+func (d Dialer) Protocol() string {
+ return "WS"
+}
+
+func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) {
wsURL, err := prepareURL(address)
if err != nil {
return nil, err
@@ -31,8 +42,11 @@ func Dial(address string) (net.Conn, error) {
}
parsedURL.Path = ws.URLPath
- wsConn, resp, err := websocket.Dial(context.Background(), parsedURL.String(), opts)
+ wsConn, resp, err := websocket.Dial(ctx, parsedURL.String(), opts)
if err != nil {
+ if errors.Is(err, context.Canceled) {
+ return nil, err
+ }
log.Errorf("failed to dial to Relay server '%s': %s", wsURL, err)
return nil, err
}
@@ -55,10 +69,19 @@ func prepareURL(address string) (string, error) {
func httpClientNbDialer() *http.Client {
customDialer := nbnet.NewDialer()
+ certPool, err := x509.SystemCertPool()
+ if err != nil || certPool == nil {
+ log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
+ certPool = embeddedroots.Get()
+ }
+
customTransport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return customDialer.DialContext(ctx, network, addr)
},
+ TLSClientConfig: &tls.Config{
+ RootCAs: certPool,
+ },
}
return &http.Client{
diff --git a/relay/client/guard.go b/relay/client/guard.go
index b971363a8..554330ea3 100644
--- a/relay/client/guard.go
+++ b/relay/client/guard.go
@@ -14,8 +14,9 @@ var (
// Guard manage the reconnection tries to the Relay server in case of disconnection event.
type Guard struct {
- // OnNewRelayClient is a channel that is used to notify the relay client about a new relay client instance.
+ // OnNewRelayClient is a channel that is used to notify the relay manager about a new relay client instance.
OnNewRelayClient chan *Client
+ OnReconnected chan struct{}
serverPicker *ServerPicker
}
@@ -23,6 +24,7 @@ type Guard struct {
func NewGuard(sp *ServerPicker) *Guard {
g := &Guard{
OnNewRelayClient: make(chan *Client, 1),
+ OnReconnected: make(chan struct{}, 1),
serverPicker: sp,
}
return g
@@ -39,14 +41,13 @@ func NewGuard(sp *ServerPicker) *Guard {
// - relayClient: The relay client instance that was disconnected.
// todo prevent multiple reconnection instances. In the current usage it should not happen, but it is better to prevent
func (g *Guard) StartReconnectTrys(ctx context.Context, relayClient *Client) {
- if relayClient == nil {
- goto RETRY
- }
- if g.isServerURLStillValid(relayClient) && g.quickReconnect(ctx, relayClient) {
+ // try to reconnect to the same server
+ if ok := g.tryToQuickReconnect(ctx, relayClient); ok {
+ g.notifyReconnected()
return
}
-RETRY:
+ // start a ticker to pick a new server
ticker := exponentTicker(ctx)
defer ticker.Stop()
@@ -64,6 +65,28 @@ RETRY:
}
}
+func (g *Guard) tryToQuickReconnect(parentCtx context.Context, rc *Client) bool {
+ if rc == nil {
+ return false
+ }
+
+ if !g.isServerURLStillValid(rc) {
+ return false
+ }
+
+ if cancelled := waiteBeforeRetry(parentCtx); !cancelled {
+ return false
+ }
+
+ log.Infof("try to reconnect to Relay server: %s", rc.connectionURL)
+
+ if err := rc.Connect(); err != nil {
+ log.Errorf("failed to reconnect to relay server: %s", err)
+ return false
+ }
+ return true
+}
+
func (g *Guard) retry(ctx context.Context) error {
log.Infof("try to pick up a new Relay server")
relayClient, err := g.serverPicker.PickServer(ctx)
@@ -78,23 +101,6 @@ func (g *Guard) retry(ctx context.Context) error {
return nil
}
-func (g *Guard) quickReconnect(parentCtx context.Context, rc *Client) bool {
- ctx, cancel := context.WithTimeout(parentCtx, 1500*time.Millisecond)
- defer cancel()
- <-ctx.Done()
-
- if parentCtx.Err() != nil {
- return false
- }
- log.Infof("try to reconnect to Relay server: %s", rc.connectionURL)
-
- if err := rc.Connect(); err != nil {
- log.Errorf("failed to reconnect to relay server: %s", err)
- return false
- }
- return true
-}
-
func (g *Guard) drainRelayClientChan() {
select {
case <-g.OnNewRelayClient:
@@ -111,6 +117,13 @@ func (g *Guard) isServerURLStillValid(rc *Client) bool {
return false
}
+func (g *Guard) notifyReconnected() {
+ select {
+ case g.OnReconnected <- struct{}{}:
+ default:
+ }
+}
+
func exponentTicker(ctx context.Context) *backoff.Ticker {
bo := backoff.WithContext(&backoff.ExponentialBackOff{
InitialInterval: 2 * time.Second,
@@ -121,3 +134,15 @@ func exponentTicker(ctx context.Context) *backoff.Ticker {
return backoff.NewTicker(bo)
}
+
+func waiteBeforeRetry(ctx context.Context) bool {
+ timer := time.NewTimer(1500 * time.Millisecond)
+ defer timer.Stop()
+
+ select {
+ case <-timer.C:
+ return true
+ case <-ctx.Done():
+ return false
+ }
+}
diff --git a/relay/client/manager.go b/relay/client/manager.go
index d847bb879..26b113050 100644
--- a/relay/client/manager.go
+++ b/relay/client/manager.go
@@ -165,6 +165,9 @@ func (m *Manager) Ready() bool {
}
func (m *Manager) SetOnReconnectedListener(f func()) {
+ m.listenerLock.Lock()
+ defer m.listenerLock.Unlock()
+
m.onReconnectedListenerFn = f
}
@@ -284,6 +287,9 @@ func (m *Manager) openConnVia(serverAddress, peerKey string) (net.Conn, error) {
}
func (m *Manager) onServerConnected() {
+ m.listenerLock.Lock()
+ defer m.listenerLock.Unlock()
+
if m.onReconnectedListenerFn == nil {
return
}
@@ -304,8 +310,11 @@ func (m *Manager) onServerDisconnected(serverAddress string) {
func (m *Manager) listenGuardEvent(ctx context.Context) {
for {
select {
+ case <-m.reconnectGuard.OnReconnected:
+ m.onServerConnected()
case rc := <-m.reconnectGuard.OnNewRelayClient:
m.storeClient(rc)
+ m.onServerConnected()
case <-ctx.Done():
return
}
@@ -317,7 +326,6 @@ func (m *Manager) storeClient(client *Client) {
defer m.relayClientMu.Unlock()
m.relayClient = client
- m.relayClient.SetOnConnectedListener(m.onServerConnected)
m.relayClient.SetOnDisconnectListener(m.onServerDisconnected)
}
diff --git a/relay/messages/message.go b/relay/messages/message.go
index 39ca0aa90..7794c57bc 100644
--- a/relay/messages/message.go
+++ b/relay/messages/message.go
@@ -23,20 +23,26 @@ const (
MsgTypeAuth = 6
MsgTypeAuthResponse = 7
- SizeOfVersionByte = 1
- SizeOfMsgType = 1
+ // base size of the message
+ sizeOfVersionByte = 1
+ sizeOfMsgType = 1
+ sizeOfProtoHeader = sizeOfVersionByte + sizeOfMsgType
- SizeOfProtoHeader = SizeOfVersionByte + SizeOfMsgType
-
- sizeOfMagicByte = 4
-
- headerSizeTransport = IDSize
+ // auth message
+ sizeOfMagicByte = 4
+ headerSizeAuth = sizeOfMagicByte + IDSize
+ offsetMagicByte = sizeOfProtoHeader
+ offsetAuthPeerID = sizeOfProtoHeader + sizeOfMagicByte
+ headerTotalSizeAuth = sizeOfProtoHeader + headerSizeAuth
+ // hello message
headerSizeHello = sizeOfMagicByte + IDSize
headerSizeHelloResp = 0
- headerSizeAuth = sizeOfMagicByte + IDSize
- headerSizeAuthResp = 0
+ // transport
+ headerSizeTransport = IDSize
+ offsetTransportID = sizeOfProtoHeader
+ headerTotalSizeTransport = sizeOfProtoHeader + headerSizeTransport
)
var (
@@ -73,7 +79,7 @@ func (m MsgType) String() string {
// ValidateVersion checks if the given version is supported by the protocol
func ValidateVersion(msg []byte) (int, error) {
- if len(msg) < SizeOfVersionByte {
+ if len(msg) < sizeOfProtoHeader {
return 0, ErrInvalidMessageLength
}
version := int(msg[0])
@@ -85,11 +91,11 @@ func ValidateVersion(msg []byte) (int, error) {
// DetermineClientMessageType determines the message type from the first the message
func DetermineClientMessageType(msg []byte) (MsgType, error) {
- if len(msg) < SizeOfMsgType {
+ if len(msg) < sizeOfProtoHeader {
return 0, ErrInvalidMessageLength
}
- msgType := MsgType(msg[0])
+ msgType := MsgType(msg[1])
switch msgType {
case
MsgTypeHello,
@@ -105,11 +111,11 @@ func DetermineClientMessageType(msg []byte) (MsgType, error) {
// DetermineServerMessageType determines the message type from the first the message
func DetermineServerMessageType(msg []byte) (MsgType, error) {
- if len(msg) < SizeOfMsgType {
+ if len(msg) < sizeOfProtoHeader {
return 0, ErrInvalidMessageLength
}
- msgType := MsgType(msg[0])
+ msgType := MsgType(msg[1])
switch msgType {
case
MsgTypeHelloResponse,
@@ -134,12 +140,12 @@ func MarshalHelloMsg(peerID []byte, additions []byte) ([]byte, error) {
return nil, fmt.Errorf("invalid peerID length: %d", len(peerID))
}
- msg := make([]byte, SizeOfProtoHeader+sizeOfMagicByte, SizeOfProtoHeader+headerSizeHello+len(additions))
+ msg := make([]byte, sizeOfProtoHeader+sizeOfMagicByte, sizeOfProtoHeader+headerSizeHello+len(additions))
msg[0] = byte(CurrentProtocolVersion)
msg[1] = byte(MsgTypeHello)
- copy(msg[SizeOfProtoHeader:SizeOfProtoHeader+sizeOfMagicByte], magicHeader)
+ copy(msg[sizeOfProtoHeader:sizeOfProtoHeader+sizeOfMagicByte], magicHeader)
msg = append(msg, peerID...)
msg = append(msg, additions...)
@@ -151,14 +157,14 @@ func MarshalHelloMsg(peerID []byte, additions []byte) ([]byte, error) {
// UnmarshalHelloMsg extracts peerID and the additional data from the hello message. The Additional data is used to
// authenticate the client with the server.
func UnmarshalHelloMsg(msg []byte) ([]byte, []byte, error) {
- if len(msg) < headerSizeHello {
+ if len(msg) < sizeOfProtoHeader+headerSizeHello {
return nil, nil, ErrInvalidMessageLength
}
- if !bytes.Equal(msg[:sizeOfMagicByte], magicHeader) {
+ if !bytes.Equal(msg[sizeOfProtoHeader:sizeOfProtoHeader+sizeOfMagicByte], magicHeader) {
return nil, nil, errors.New("invalid magic header")
}
- return msg[sizeOfMagicByte:headerSizeHello], msg[headerSizeHello:], nil
+ return msg[sizeOfProtoHeader+sizeOfMagicByte : sizeOfProtoHeader+headerSizeHello], msg[headerSizeHello:], nil
}
// Deprecated: Use MarshalAuthResponse instead.
@@ -167,7 +173,7 @@ func UnmarshalHelloMsg(msg []byte) ([]byte, []byte, error) {
// instance URL. This URL will be used by choose the common Relay server in case if the peers are in different Relay
// servers.
func MarshalHelloResponse(additionalData []byte) ([]byte, error) {
- msg := make([]byte, SizeOfProtoHeader, SizeOfProtoHeader+headerSizeHelloResp+len(additionalData))
+ msg := make([]byte, sizeOfProtoHeader, sizeOfProtoHeader+headerSizeHelloResp+len(additionalData))
msg[0] = byte(CurrentProtocolVersion)
msg[1] = byte(MsgTypeHelloResponse)
@@ -180,7 +186,7 @@ func MarshalHelloResponse(additionalData []byte) ([]byte, error) {
// Deprecated: Use UnmarshalAuthResponse instead.
// UnmarshalHelloResponse extracts the additional data from the hello response message.
func UnmarshalHelloResponse(msg []byte) ([]byte, error) {
- if len(msg) < headerSizeHelloResp {
+ if len(msg) < sizeOfProtoHeader+headerSizeHelloResp {
return nil, ErrInvalidMessageLength
}
return msg, nil
@@ -196,12 +202,12 @@ func MarshalAuthMsg(peerID []byte, authPayload []byte) ([]byte, error) {
return nil, fmt.Errorf("invalid peerID length: %d", len(peerID))
}
- msg := make([]byte, SizeOfProtoHeader+sizeOfMagicByte, SizeOfProtoHeader+headerSizeAuth+len(authPayload))
+ msg := make([]byte, sizeOfProtoHeader+sizeOfMagicByte, headerTotalSizeAuth+len(authPayload))
msg[0] = byte(CurrentProtocolVersion)
msg[1] = byte(MsgTypeAuth)
- copy(msg[SizeOfProtoHeader:SizeOfProtoHeader+sizeOfMagicByte], magicHeader)
+ copy(msg[sizeOfProtoHeader:], magicHeader)
msg = append(msg, peerID...)
msg = append(msg, authPayload...)
@@ -211,14 +217,14 @@ func MarshalAuthMsg(peerID []byte, authPayload []byte) ([]byte, error) {
// UnmarshalAuthMsg extracts peerID and the auth payload from the message
func UnmarshalAuthMsg(msg []byte) ([]byte, []byte, error) {
- if len(msg) < headerSizeAuth {
+ if len(msg) < headerTotalSizeAuth {
return nil, nil, ErrInvalidMessageLength
}
- if !bytes.Equal(msg[:sizeOfMagicByte], magicHeader) {
+ if !bytes.Equal(msg[offsetMagicByte:offsetMagicByte+sizeOfMagicByte], magicHeader) {
return nil, nil, errors.New("invalid magic header")
}
- return msg[sizeOfMagicByte:headerSizeAuth], msg[headerSizeAuth:], nil
+ return msg[offsetAuthPeerID:headerTotalSizeAuth], msg[headerTotalSizeAuth:], nil
}
// MarshalAuthResponse creates a response message to the auth.
@@ -227,7 +233,7 @@ func UnmarshalAuthMsg(msg []byte) ([]byte, []byte, error) {
// servers.
func MarshalAuthResponse(address string) ([]byte, error) {
ab := []byte(address)
- msg := make([]byte, SizeOfProtoHeader, SizeOfProtoHeader+headerSizeAuthResp+len(ab))
+ msg := make([]byte, sizeOfProtoHeader, sizeOfProtoHeader+len(ab))
msg[0] = byte(CurrentProtocolVersion)
msg[1] = byte(MsgTypeAuthResponse)
@@ -243,39 +249,34 @@ func MarshalAuthResponse(address string) ([]byte, error) {
// UnmarshalAuthResponse it is a confirmation message to auth success
func UnmarshalAuthResponse(msg []byte) (string, error) {
- if len(msg) < headerSizeAuthResp+1 {
+ if len(msg) < sizeOfProtoHeader+1 {
return "", ErrInvalidMessageLength
}
- return string(msg), nil
+ return string(msg[sizeOfProtoHeader:]), nil
}
// MarshalCloseMsg creates a close message.
// The close message is used to close the connection gracefully between the client and the server. The server and the
// client can send this message. After receiving this message, the server or client will close the connection.
func MarshalCloseMsg() []byte {
- msg := make([]byte, SizeOfProtoHeader)
-
- msg[0] = byte(CurrentProtocolVersion)
- msg[1] = byte(MsgTypeClose)
-
- return msg
+ return []byte{
+ byte(CurrentProtocolVersion),
+ byte(MsgTypeClose),
+ }
}
// MarshalTransportMsg creates a transport message.
// The transport message is used to exchange data between peers. The message contains the data to be exchanged and the
// destination peer hashed ID.
-func MarshalTransportMsg(peerID []byte, payload []byte) ([]byte, error) {
+func MarshalTransportMsg(peerID, payload []byte) ([]byte, error) {
if len(peerID) != IDSize {
return nil, fmt.Errorf("invalid peerID length: %d", len(peerID))
}
- msg := make([]byte, SizeOfProtoHeader+headerSizeTransport, SizeOfProtoHeader+headerSizeTransport+len(payload))
-
+ msg := make([]byte, headerTotalSizeTransport, headerTotalSizeTransport+len(payload))
msg[0] = byte(CurrentProtocolVersion)
msg[1] = byte(MsgTypeTransport)
-
- copy(msg[SizeOfProtoHeader:], peerID)
-
+ copy(msg[sizeOfProtoHeader:], peerID)
msg = append(msg, payload...)
return msg, nil
@@ -283,29 +284,29 @@ func MarshalTransportMsg(peerID []byte, payload []byte) ([]byte, error) {
// UnmarshalTransportMsg extracts the peerID and the payload from the transport message.
func UnmarshalTransportMsg(buf []byte) ([]byte, []byte, error) {
- if len(buf) < headerSizeTransport {
+ if len(buf) < headerTotalSizeTransport {
return nil, nil, ErrInvalidMessageLength
}
- return buf[:headerSizeTransport], buf[headerSizeTransport:], nil
+ return buf[offsetTransportID:headerTotalSizeTransport], buf[headerTotalSizeTransport:], nil
}
// UnmarshalTransportID extracts the peerID from the transport message.
func UnmarshalTransportID(buf []byte) ([]byte, error) {
- if len(buf) < headerSizeTransport {
+ if len(buf) < headerTotalSizeTransport {
return nil, ErrInvalidMessageLength
}
- return buf[:headerSizeTransport], nil
+ return buf[offsetTransportID:headerTotalSizeTransport], nil
}
// UpdateTransportMsg updates the peerID in the transport message.
// With this function the server can reuse the given byte slice to update the peerID in the transport message. So do
// need to allocate a new byte slice.
func UpdateTransportMsg(msg []byte, peerID []byte) error {
- if len(msg) < len(peerID) {
+ if len(msg) < offsetTransportID+len(peerID) {
return ErrInvalidMessageLength
}
- copy(msg, peerID)
+ copy(msg[offsetTransportID:], peerID)
return nil
}
diff --git a/relay/messages/message_test.go b/relay/messages/message_test.go
index 6e917da71..19bede07b 100644
--- a/relay/messages/message_test.go
+++ b/relay/messages/message_test.go
@@ -6,12 +6,21 @@ import (
func TestMarshalHelloMsg(t *testing.T) {
peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+")
- bHello, err := MarshalHelloMsg(peerID, nil)
+ msg, err := MarshalHelloMsg(peerID, nil)
if err != nil {
t.Fatalf("error: %v", err)
}
- receivedPeerID, _, err := UnmarshalHelloMsg(bHello[SizeOfProtoHeader:])
+ msgType, err := DetermineClientMessageType(msg)
+ if err != nil {
+ t.Fatalf("error: %v", err)
+ }
+
+ if msgType != MsgTypeHello {
+ t.Errorf("expected %d, got %d", MsgTypeHello, msgType)
+ }
+
+ receivedPeerID, _, err := UnmarshalHelloMsg(msg)
if err != nil {
t.Fatalf("error: %v", err)
}
@@ -22,12 +31,21 @@ func TestMarshalHelloMsg(t *testing.T) {
func TestMarshalAuthMsg(t *testing.T) {
peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+")
- bHello, err := MarshalAuthMsg(peerID, []byte{})
+ msg, err := MarshalAuthMsg(peerID, []byte{})
if err != nil {
t.Fatalf("error: %v", err)
}
- receivedPeerID, _, err := UnmarshalAuthMsg(bHello[SizeOfProtoHeader:])
+ msgType, err := DetermineClientMessageType(msg)
+ if err != nil {
+ t.Fatalf("error: %v", err)
+ }
+
+ if msgType != MsgTypeAuth {
+ t.Errorf("expected %d, got %d", MsgTypeAuth, msgType)
+ }
+
+ receivedPeerID, _, err := UnmarshalAuthMsg(msg)
if err != nil {
t.Fatalf("error: %v", err)
}
@@ -36,6 +54,31 @@ func TestMarshalAuthMsg(t *testing.T) {
}
}
+func TestMarshalAuthResponse(t *testing.T) {
+ address := "myaddress"
+ msg, err := MarshalAuthResponse(address)
+ if err != nil {
+ t.Fatalf("error: %v", err)
+ }
+
+ msgType, err := DetermineServerMessageType(msg)
+ if err != nil {
+ t.Fatalf("error: %v", err)
+ }
+
+ if msgType != MsgTypeAuthResponse {
+ t.Errorf("expected %d, got %d", MsgTypeAuthResponse, msgType)
+ }
+
+ respAddr, err := UnmarshalAuthResponse(msg)
+ if err != nil {
+ t.Fatalf("error: %v", err)
+ }
+ if respAddr != address {
+ t.Errorf("expected %s, got %s", address, respAddr)
+ }
+}
+
func TestMarshalTransportMsg(t *testing.T) {
peerID := []byte("abdFAaBcawquEiCMzAabYosuUaGLtSNhKxz+")
payload := []byte("payload")
@@ -44,7 +87,25 @@ func TestMarshalTransportMsg(t *testing.T) {
t.Fatalf("error: %v", err)
}
- id, respPayload, err := UnmarshalTransportMsg(msg[SizeOfProtoHeader:])
+ msgType, err := DetermineClientMessageType(msg)
+ if err != nil {
+ t.Fatalf("error: %v", err)
+ }
+
+ if msgType != MsgTypeTransport {
+ t.Errorf("expected %d, got %d", MsgTypeTransport, msgType)
+ }
+
+ uPeerID, err := UnmarshalTransportID(msg)
+ if err != nil {
+ t.Fatalf("failed to unmarshal transport id: %v", err)
+ }
+
+ if string(uPeerID) != string(peerID) {
+ t.Errorf("expected %s, got %s", peerID, uPeerID)
+ }
+
+ id, respPayload, err := UnmarshalTransportMsg(msg)
if err != nil {
t.Fatalf("error: %v", err)
}
@@ -57,3 +118,21 @@ func TestMarshalTransportMsg(t *testing.T) {
t.Errorf("expected %s, got %s", payload, respPayload)
}
}
+
+func TestMarshalHealthcheck(t *testing.T) {
+ msg := MarshalHealthcheck()
+
+ _, err := ValidateVersion(msg)
+ if err != nil {
+ t.Fatalf("error: %v", err)
+ }
+
+ msgType, err := DetermineServerMessageType(msg)
+ if err != nil {
+ t.Fatalf("error: %v", err)
+ }
+
+ if msgType != MsgTypeHealthCheck {
+ t.Errorf("expected %d, got %d", MsgTypeHealthCheck, msgType)
+ }
+}
diff --git a/relay/metrics/realy.go b/relay/metrics/realy.go
index 4dc98a0e0..2e90940e6 100644
--- a/relay/metrics/realy.go
+++ b/relay/metrics/realy.go
@@ -29,37 +29,53 @@ type Metrics struct {
}
func NewMetrics(ctx context.Context, meter metric.Meter) (*Metrics, error) {
- bytesSent, err := meter.Int64Counter("relay_transfer_sent_bytes_total")
+ bytesSent, err := meter.Int64Counter("relay_transfer_sent_bytes_total",
+ metric.WithDescription("Total number of bytes sent to peers"),
+ )
if err != nil {
return nil, err
}
- bytesRecv, err := meter.Int64Counter("relay_transfer_received_bytes_total")
+ bytesRecv, err := meter.Int64Counter("relay_transfer_received_bytes_total",
+ metric.WithDescription("Total number of bytes received from peers"),
+ )
if err != nil {
return nil, err
}
- peers, err := meter.Int64UpDownCounter("relay_peers")
+ peers, err := meter.Int64UpDownCounter("relay_peers",
+ metric.WithDescription("Number of connected peers"),
+ )
if err != nil {
return nil, err
}
- peersActive, err := meter.Int64ObservableGauge("relay_peers_active")
+ peersActive, err := meter.Int64ObservableGauge("relay_peers_active",
+ metric.WithDescription("Number of active connected peers"),
+ )
if err != nil {
return nil, err
}
- peersIdle, err := meter.Int64ObservableGauge("relay_peers_idle")
+ peersIdle, err := meter.Int64ObservableGauge("relay_peers_idle",
+ metric.WithDescription("Number of idle connected peers"),
+ )
if err != nil {
return nil, err
}
- authTime, err := meter.Float64Histogram("relay_peer_authentication_time_milliseconds", metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
+ authTime, err := meter.Float64Histogram("relay_peer_authentication_time_milliseconds",
+ metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
+ metric.WithDescription("Time taken to authenticate a peer"),
+ )
if err != nil {
return nil, err
}
- peerStoreTime, err := meter.Float64Histogram("relay_peer_store_time_milliseconds", metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
+ peerStoreTime, err := meter.Float64Histogram("relay_peer_store_time_milliseconds",
+ metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
+ metric.WithDescription("Time taken to store a new peer connection"),
+ )
if err != nil {
return nil, err
}
diff --git a/relay/server/handshake.go b/relay/server/handshake.go
index 0257300f8..babd6f955 100644
--- a/relay/server/handshake.go
+++ b/relay/server/handshake.go
@@ -68,12 +68,14 @@ func (h *handshake) handshakeReceive() ([]byte, error) {
return nil, fmt.Errorf("read from %s: %w", h.conn.RemoteAddr(), err)
}
- _, err = messages.ValidateVersion(buf[:n])
+ buf = buf[:n]
+
+ _, err = messages.ValidateVersion(buf)
if err != nil {
return nil, fmt.Errorf("validate version from %s: %w", h.conn.RemoteAddr(), err)
}
- msgType, err := messages.DetermineClientMessageType(buf[messages.SizeOfVersionByte:n])
+ msgType, err := messages.DetermineClientMessageType(buf)
if err != nil {
return nil, fmt.Errorf("determine message type from %s: %w", h.conn.RemoteAddr(), err)
}
@@ -85,10 +87,10 @@ func (h *handshake) handshakeReceive() ([]byte, error) {
switch msgType {
//nolint:staticcheck
case messages.MsgTypeHello:
- bytePeerID, peerID, err = h.handleHelloMsg(buf[messages.SizeOfProtoHeader:n])
+ bytePeerID, peerID, err = h.handleHelloMsg(buf)
case messages.MsgTypeAuth:
h.handshakeMethodAuth = true
- bytePeerID, peerID, err = h.handleAuthMsg(buf[messages.SizeOfProtoHeader:n])
+ bytePeerID, peerID, err = h.handleAuthMsg(buf)
default:
return nil, fmt.Errorf("invalid message type %d from %s", msgType, h.conn.RemoteAddr())
}
diff --git a/relay/server/listener/quic/conn.go b/relay/server/listener/quic/conn.go
new file mode 100644
index 000000000..909ec1cc6
--- /dev/null
+++ b/relay/server/listener/quic/conn.go
@@ -0,0 +1,101 @@
+package quic
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "sync"
+ "time"
+
+ "github.com/quic-go/quic-go"
+)
+
+type Conn struct {
+ session quic.Connection
+ closed bool
+ closedMu sync.Mutex
+ ctx context.Context
+ ctxCancel context.CancelFunc
+}
+
+func NewConn(session quic.Connection) *Conn {
+ ctx, cancel := context.WithCancel(context.Background())
+ return &Conn{
+ session: session,
+ ctx: ctx,
+ ctxCancel: cancel,
+ }
+}
+
+func (c *Conn) Read(b []byte) (n int, err error) {
+ dgram, err := c.session.ReceiveDatagram(c.ctx)
+ if err != nil {
+ return 0, c.remoteCloseErrHandling(err)
+ }
+ // Copy data to b, ensuring we don’t exceed the size of b
+ n = copy(b, dgram)
+ return n, nil
+}
+
+func (c *Conn) Write(b []byte) (int, error) {
+ if err := c.session.SendDatagram(b); err != nil {
+ return 0, c.remoteCloseErrHandling(err)
+ }
+ return len(b), nil
+}
+
+func (c *Conn) LocalAddr() net.Addr {
+ return c.session.LocalAddr()
+}
+
+func (c *Conn) RemoteAddr() net.Addr {
+ return c.session.RemoteAddr()
+}
+
+func (c *Conn) SetReadDeadline(t time.Time) error {
+ return nil
+}
+
+func (c *Conn) SetWriteDeadline(t time.Time) error {
+ return fmt.Errorf("SetWriteDeadline is not implemented")
+}
+
+func (c *Conn) SetDeadline(t time.Time) error {
+ return fmt.Errorf("SetDeadline is not implemented")
+}
+
+func (c *Conn) Close() error {
+ c.closedMu.Lock()
+ if c.closed {
+ c.closedMu.Unlock()
+ return nil
+ }
+ c.closed = true
+ c.closedMu.Unlock()
+
+ c.ctxCancel() // Cancel the context
+
+ sessionErr := c.session.CloseWithError(0, "normal closure")
+ return sessionErr
+}
+
+func (c *Conn) isClosed() bool {
+ c.closedMu.Lock()
+ defer c.closedMu.Unlock()
+ return c.closed
+}
+
+func (c *Conn) remoteCloseErrHandling(err error) error {
+ if c.isClosed() {
+ return net.ErrClosed
+ }
+
+ // Check if the connection was closed remotely
+ var appErr *quic.ApplicationError
+ if errors.As(err, &appErr) && appErr.ErrorCode == 0x0 {
+ return net.ErrClosed
+ }
+
+ return err
+}
diff --git a/relay/server/listener/quic/listener.go b/relay/server/listener/quic/listener.go
new file mode 100644
index 000000000..17a5e8ab6
--- /dev/null
+++ b/relay/server/listener/quic/listener.go
@@ -0,0 +1,67 @@
+package quic
+
+import (
+ "context"
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "net"
+
+ "github.com/quic-go/quic-go"
+ log "github.com/sirupsen/logrus"
+)
+
+type Listener struct {
+ // Address is the address to listen on
+ Address string
+ // TLSConfig is the TLS configuration for the server
+ TLSConfig *tls.Config
+
+ listener *quic.Listener
+ acceptFn func(conn net.Conn)
+}
+
+func (l *Listener) Listen(acceptFn func(conn net.Conn)) error {
+ l.acceptFn = acceptFn
+
+ quicCfg := &quic.Config{
+ EnableDatagrams: true,
+ InitialPacketSize: 1452,
+ }
+ listener, err := quic.ListenAddr(l.Address, l.TLSConfig, quicCfg)
+ if err != nil {
+ return fmt.Errorf("failed to create QUIC listener: %v", err)
+ }
+
+ l.listener = listener
+ log.Infof("QUIC server listening on address: %s", l.Address)
+
+ for {
+ session, err := listener.Accept(context.Background())
+ if err != nil {
+ if errors.Is(err, quic.ErrServerClosed) {
+ return nil
+ }
+
+ log.Errorf("Failed to accept QUIC session: %v", err)
+ continue
+ }
+
+ log.Infof("QUIC client connected from: %s", session.RemoteAddr())
+ conn := NewConn(session)
+ l.acceptFn(conn)
+ }
+}
+
+func (l *Listener) Shutdown(ctx context.Context) error {
+ if l.listener == nil {
+ return nil
+ }
+
+ log.Infof("stopping QUIC listener")
+ if err := l.listener.Close(); err != nil {
+ return fmt.Errorf("listener shutdown failed: %v", err)
+ }
+ log.Infof("QUIC listener stopped")
+ return nil
+}
diff --git a/relay/server/listener/ws/conn.go b/relay/server/listener/ws/conn.go
index 12e721fdb..3ec08945b 100644
--- a/relay/server/listener/ws/conn.go
+++ b/relay/server/listener/ws/conn.go
@@ -8,8 +8,8 @@ import (
"sync"
"time"
+ "github.com/coder/websocket"
log "github.com/sirupsen/logrus"
- "nhooyr.io/websocket"
)
const (
diff --git a/relay/server/listener/ws/listener.go b/relay/server/listener/ws/listener.go
index 5c62c0826..3a95951ee 100644
--- a/relay/server/listener/ws/listener.go
+++ b/relay/server/listener/ws/listener.go
@@ -8,8 +8,8 @@ import (
"net"
"net/http"
+ "github.com/coder/websocket"
log "github.com/sirupsen/logrus"
- "nhooyr.io/websocket"
)
// URLPath is the path for the websocket connection.
@@ -88,6 +88,8 @@ func (l *Listener) onAccept(w http.ResponseWriter, r *http.Request) {
return
}
+ log.Infof("WS client connected from: %s", rAddr)
+
conn := NewConn(wsConn, lAddr, rAddr)
l.acceptFn(conn)
}
diff --git a/relay/server/peer.go b/relay/server/peer.go
index f65fb786a..aa9790f63 100644
--- a/relay/server/peer.go
+++ b/relay/server/peer.go
@@ -84,7 +84,7 @@ func (p *Peer) Work() {
return
}
- msgType, err := messages.DetermineClientMessageType(msg[messages.SizeOfVersionByte:])
+ msgType, err := messages.DetermineClientMessageType(msg)
if err != nil {
p.log.Errorf("failed to determine message type: %s", err)
return
@@ -191,7 +191,7 @@ func (p *Peer) handleHealthcheckEvents(ctx context.Context, hc *healthcheck.Send
}
func (p *Peer) handleTransportMsg(msg []byte) {
- peerID, err := messages.UnmarshalTransportID(msg[messages.SizeOfProtoHeader:])
+ peerID, err := messages.UnmarshalTransportID(msg)
if err != nil {
p.log.Errorf("failed to unmarshal transport message: %s", err)
return
@@ -204,7 +204,7 @@ func (p *Peer) handleTransportMsg(msg []byte) {
return
}
- err = messages.UpdateTransportMsg(msg[messages.SizeOfProtoHeader:], p.idB)
+ err = messages.UpdateTransportMsg(msg, p.idB)
if err != nil {
p.log.Errorf("failed to update transport message: %s", err)
return
diff --git a/relay/server/relay.go b/relay/server/relay.go
index 6cd8506ae..a5e77bc61 100644
--- a/relay/server/relay.go
+++ b/relay/server/relay.go
@@ -150,6 +150,8 @@ func (r *Relay) Accept(conn net.Conn) {
func (r *Relay) Shutdown(ctx context.Context) {
log.Infof("close connection with all peers")
r.closeMu.Lock()
+ defer r.closeMu.Unlock()
+
wg := sync.WaitGroup{}
peers := r.store.Peers()
for _, peer := range peers {
@@ -161,7 +163,7 @@ func (r *Relay) Shutdown(ctx context.Context) {
}
wg.Wait()
r.metricsCancel()
- r.closeMu.Unlock()
+ r.closed = true
}
// InstanceURL returns the instance URL of the relay server
diff --git a/relay/server/server.go b/relay/server/server.go
index 0036e2390..10aabcace 100644
--- a/relay/server/server.go
+++ b/relay/server/server.go
@@ -3,13 +3,18 @@ package server
import (
"context"
"crypto/tls"
+ "sync"
+ "github.com/hashicorp/go-multierror"
log "github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/metric"
+ nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/relay/auth"
"github.com/netbirdio/netbird/relay/server/listener"
+ "github.com/netbirdio/netbird/relay/server/listener/quic"
"github.com/netbirdio/netbird/relay/server/listener/ws"
+ quictls "github.com/netbirdio/netbird/relay/tls"
)
// ListenerConfig is the configuration for the listener.
@@ -24,8 +29,8 @@ type ListenerConfig struct {
// It is the gate between the WebSocket listener and the Relay server logic.
// In a new HTTP connection, the server will accept the connection and pass it to the Relay server via the Accept method.
type Server struct {
- relay *Relay
- wSListener listener.Listener
+ relay *Relay
+ listeners []listener.Listener
}
// NewServer creates a new relay server instance.
@@ -39,35 +44,63 @@ func NewServer(meter metric.Meter, exposedAddress string, tlsSupport bool, authV
return nil, err
}
return &Server{
- relay: relay,
+ relay: relay,
+ listeners: make([]listener.Listener, 0, 2),
}, nil
}
// Listen starts the relay server.
func (r *Server) Listen(cfg ListenerConfig) error {
- r.wSListener = &ws.Listener{
+ wSListener := &ws.Listener{
Address: cfg.Address,
TLSConfig: cfg.TLSConfig,
}
+ r.listeners = append(r.listeners, wSListener)
- wslErr := r.wSListener.Listen(r.relay.Accept)
- if wslErr != nil {
- log.Errorf("failed to bind ws server: %s", wslErr)
+ tlsConfigQUIC, err := quictls.ServerQUICTLSConfig(cfg.TLSConfig)
+ if err != nil {
+ log.Warnf("Not starting QUIC listener: %v", err)
+ } else {
+ quicListener := &quic.Listener{
+ Address: cfg.Address,
+ TLSConfig: tlsConfigQUIC,
+ }
+
+ r.listeners = append(r.listeners, quicListener)
}
- return wslErr
+ errChan := make(chan error, len(r.listeners))
+ wg := sync.WaitGroup{}
+ for _, l := range r.listeners {
+ wg.Add(1)
+ go func(listener listener.Listener) {
+ defer wg.Done()
+ errChan <- listener.Listen(r.relay.Accept)
+ }(l)
+ }
+
+ wg.Wait()
+ close(errChan)
+ var multiErr *multierror.Error
+ for err := range errChan {
+ multiErr = multierror.Append(multiErr, err)
+ }
+
+ return nberrors.FormatErrorOrNil(multiErr)
}
// Shutdown stops the relay server. If there are active connections, they will be closed gracefully. In case of a context,
// the connections will be forcefully closed.
-func (r *Server) Shutdown(ctx context.Context) (err error) {
- // stop service new connections
- if r.wSListener != nil {
- err = r.wSListener.Shutdown(ctx)
- }
-
+func (r *Server) Shutdown(ctx context.Context) error {
r.relay.Shutdown(ctx)
- return
+
+ var multiErr *multierror.Error
+ for _, l := range r.listeners {
+ if err := l.Shutdown(ctx); err != nil {
+ multiErr = multierror.Append(multiErr, err)
+ }
+ }
+ return nberrors.FormatErrorOrNil(multiErr)
}
// InstanceURL returns the instance URL of the relay server.
diff --git a/relay/tls/alpn.go b/relay/tls/alpn.go
new file mode 100644
index 000000000..29497d401
--- /dev/null
+++ b/relay/tls/alpn.go
@@ -0,0 +1,3 @@
+package tls
+
+const nbalpn = "nb-quic"
diff --git a/relay/tls/client_dev.go b/relay/tls/client_dev.go
new file mode 100644
index 000000000..52e5535c5
--- /dev/null
+++ b/relay/tls/client_dev.go
@@ -0,0 +1,26 @@
+//go:build devcert
+
+package tls
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/util/embeddedroots"
+)
+
+func ClientQUICTLSConfig() *tls.Config {
+ certPool, err := x509.SystemCertPool()
+ if err != nil || certPool == nil {
+ log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
+ certPool = embeddedroots.Get()
+ }
+
+ return &tls.Config{
+ InsecureSkipVerify: true, // Debug mode allows insecure connections
+ NextProtos: []string{nbalpn}, // Ensure this matches the server's ALPN
+ RootCAs: certPool,
+ }
+}
diff --git a/relay/tls/client_prod.go b/relay/tls/client_prod.go
new file mode 100644
index 000000000..62e218bc3
--- /dev/null
+++ b/relay/tls/client_prod.go
@@ -0,0 +1,25 @@
+//go:build !devcert
+
+package tls
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/util/embeddedroots"
+)
+
+func ClientQUICTLSConfig() *tls.Config {
+ certPool, err := x509.SystemCertPool()
+ if err != nil || certPool == nil {
+ log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
+ certPool = embeddedroots.Get()
+ }
+
+ return &tls.Config{
+ NextProtos: []string{nbalpn},
+ RootCAs: certPool,
+ }
+}
diff --git a/relay/tls/doc.go b/relay/tls/doc.go
new file mode 100644
index 000000000..38b807f84
--- /dev/null
+++ b/relay/tls/doc.go
@@ -0,0 +1,36 @@
+// Package tls provides utilities for configuring and managing Transport Layer
+// Security (TLS) in server and client environments, with a focus on QUIC
+// protocol support and testing configurations.
+//
+// The package includes functions for cloning and customizing TLS
+// configurations as well as generating self-signed certificates for
+// development and testing purposes.
+//
+// Key Features:
+//
+// - `ServerQUICTLSConfig`: Creates a server-side TLS configuration tailored
+// for QUIC protocol with specified or default settings. QUIC requires a
+// specific TLS configuration with proper ALPN (Application-Layer Protocol
+// Negotiation) support, making the TLS settings crucial for establishing
+// secure connections.
+//
+// - `ClientQUICTLSConfig`: Provides a client-side TLS configuration suitable
+// for QUIC protocol. The configuration differs between development
+// (insecure testing) and production (strict verification).
+//
+// - `generateTestTLSConfig`: Generates a self-signed TLS configuration for
+// use in local development and testing scenarios.
+//
+// Usage:
+//
+// This package provides separate implementations for development and production
+// environments. The development implementation (guarded by `//go:build devcert`)
+// supports testing configurations with self-signed certificates and insecure
+// client connections. The production implementation (guarded by `//go:build
+// !devcert`) ensures that valid and secure TLS configurations are supplied
+// and used.
+//
+// The QUIC protocol is highly reliant on properly configured TLS settings,
+// and this package ensures that configurations meet the requirements for
+// secure and efficient QUIC communication.
+package tls
diff --git a/relay/tls/server_dev.go b/relay/tls/server_dev.go
new file mode 100644
index 000000000..1a01658fc
--- /dev/null
+++ b/relay/tls/server_dev.go
@@ -0,0 +1,79 @@
+//go:build devcert
+
+package tls
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "math/big"
+ "net"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+func ServerQUICTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) {
+ if originTLSCfg == nil {
+ log.Warnf("QUIC server will use self signed certificate for testing!")
+ return generateTestTLSConfig()
+ }
+
+ cfg := originTLSCfg.Clone()
+ cfg.NextProtos = []string{nbalpn}
+ return cfg, nil
+}
+
+// GenerateTestTLSConfig creates a self-signed certificate for testing
+func generateTestTLSConfig() (*tls.Config, error) {
+ log.Infof("generating test TLS config")
+ privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, err
+ }
+
+ template := x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ Organization: []string{"Test Organization"},
+ },
+ NotBefore: time.Now(),
+ NotAfter: time.Now().Add(time.Hour * 24 * 180), // Valid for 180 days
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{
+ x509.ExtKeyUsageServerAuth,
+ },
+ BasicConstraintsValid: true,
+ DNSNames: []string{"localhost"},
+ IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
+ }
+
+ // Create certificate
+ certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
+ if err != nil {
+ return nil, err
+ }
+
+ certPEM := pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: certDER,
+ })
+
+ privateKeyPEM := pem.EncodeToMemory(&pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
+ })
+
+ tlsCert, err := tls.X509KeyPair(certPEM, privateKeyPEM)
+ if err != nil {
+ return nil, err
+ }
+
+ return &tls.Config{
+ Certificates: []tls.Certificate{tlsCert},
+ NextProtos: []string{nbalpn},
+ }, nil
+}
diff --git a/relay/tls/server_prod.go b/relay/tls/server_prod.go
new file mode 100644
index 000000000..9d1c47d88
--- /dev/null
+++ b/relay/tls/server_prod.go
@@ -0,0 +1,17 @@
+//go:build !devcert
+
+package tls
+
+import (
+ "crypto/tls"
+ "fmt"
+)
+
+func ServerQUICTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) {
+ if originTLSCfg == nil {
+ return nil, fmt.Errorf("valid TLS config is required for QUIC listener")
+ }
+ cfg := originTLSCfg.Clone()
+ cfg.NextProtos = []string{nbalpn}
+ return cfg, nil
+}
diff --git a/release_files/darwin-ui-installer.sh b/release_files/darwin-ui-installer.sh
index 5179f02d6..de331730f 100644
--- a/release_files/darwin-ui-installer.sh
+++ b/release_files/darwin-ui-installer.sh
@@ -2,13 +2,13 @@
export PATH=$PATH:/usr/local/bin:/opt/homebrew/bin
-# check if wiretrustee is installed
-WT_BIN=$(which wiretrustee)
-if [ -n "$WT_BIN" ]
+# check if netbird is installed
+NB_BIN=$(which netbird)
+if [ -n "$NB_BIN" ]
then
- echo "Stopping and uninstalling Wiretrustee daemon"
- wiretrustee service stop || true
- wiretrustee service uninstall || true
+ echo "Stopping and uninstalling Netbird daemon"
+ netbird service stop || true
+ netbird service uninstall || true
fi
# check if netbird is installed
diff --git a/release_files/install.sh b/release_files/install.sh
index bb917c39a..459645c58 100755
--- a/release_files/install.sh
+++ b/release_files/install.sh
@@ -263,16 +263,16 @@ install_netbird() {
add_aur_repo
;;
brew)
- # Remove Wiretrustee if it had been installed using Homebrew before
- if brew ls --versions wiretrustee >/dev/null 2>&1; then
- echo "Removing existing wiretrustee client"
+ # Remove Netbird if it had been installed using Homebrew before
+ if brew ls --versions netbird >/dev/null 2>&1; then
+ echo "Removing existing netbird client"
# Stop and uninstall daemon service:
- wiretrustee service stop
- wiretrustee service uninstall
+ netbird service stop
+ netbird service uninstall
# Unlik the app
- brew unlink wiretrustee
+ brew unlink netbird
fi
brew install netbirdio/tap/netbird
diff --git a/release_files/ui-post-install.sh b/release_files/ui-post-install.sh
new file mode 100644
index 000000000..f6e8ddf92
--- /dev/null
+++ b/release_files/ui-post-install.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# Check if netbird-ui is running
+if pgrep -x -f /usr/bin/netbird-ui >/dev/null 2>&1;
+then
+ runner=$(ps --no-headers -o '%U' -p $(pgrep -x -f /usr/bin/netbird-ui) | sed 's/^[ \t]*//;s/[ \t]*$//')
+ # Only re-run if it was already running
+ pkill -x -f /usr/bin/netbird-ui >/dev/null 2>&1
+ su -l - "$runner" -c 'nohup /usr/bin/netbird-ui > /dev/null 2>&1 &'
+fi
diff --git a/signal/README.md b/signal/README.md
index 9e3207cfa..0033eaf90 100644
--- a/signal/README.md
+++ b/signal/README.md
@@ -60,7 +60,7 @@ subdomain sub.mydomain.com).
```bash
# create a volume
-docker volume create wiretrustee-signal
+docker volume create netbird-signal
# run the docker container
docker run -d --name netbird-signal \
-p 10000:10000 \
diff --git a/signal/cmd/run.go b/signal/cmd/run.go
index 1bb2f1d0c..3a671a848 100644
--- a/signal/cmd/run.go
+++ b/signal/cmd/run.go
@@ -8,6 +8,8 @@ import (
"fmt"
"net"
"net/http"
+ // nolint:gosec
+ _ "net/http/pprof"
"strings"
"time"
@@ -82,6 +84,8 @@ var (
RunE: func(cmd *cobra.Command, args []string) error {
flag.Parse()
+ startPprof()
+
opts, certManager, err := getTLSConfigurations()
if err != nil {
return err
@@ -170,6 +174,15 @@ var (
}
)
+func startPprof() {
+ go func() {
+ log.Debugf("Starting pprof server on 127.0.0.1:6060")
+ if err := http.ListenAndServe("127.0.0.1:6060", nil); err != nil {
+ log.Fatalf("pprof server failed: %v", err)
+ }
+ }()
+}
+
func getTLSConfigurations() ([]grpc.ServerOption, *autocert.Manager, error) {
var (
err error
diff --git a/signal/metrics/app.go b/signal/metrics/app.go
index f8be88be7..e3b1c67cd 100644
--- a/signal/metrics/app.go
+++ b/signal/metrics/app.go
@@ -20,59 +20,91 @@ type AppMetrics struct {
MessagesForwarded metric.Int64Counter
MessageForwardFailures metric.Int64Counter
MessageForwardLatency metric.Float64Histogram
+
+ MessageSize metric.Int64Histogram
}
func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) {
- activePeers, err := meter.Int64UpDownCounter("active_peers")
+ activePeers, err := meter.Int64UpDownCounter("active_peers",
+ metric.WithDescription("Number of active connected peers"),
+ )
if err != nil {
return nil, err
}
peerConnectionDuration, err := meter.Int64Histogram("peer_connection_duration_seconds",
- metric.WithExplicitBucketBoundaries(getPeerConnectionDurationBucketBoundaries()...))
+ metric.WithExplicitBucketBoundaries(getPeerConnectionDurationBucketBoundaries()...),
+ metric.WithDescription("Duration of how long a peer was connected"),
+ )
if err != nil {
return nil, err
}
- registrations, err := meter.Int64Counter("registrations_total")
+ registrations, err := meter.Int64Counter("registrations_total",
+ metric.WithDescription("Total number of peer registrations"),
+ )
if err != nil {
return nil, err
}
- deregistrations, err := meter.Int64Counter("deregistrations_total")
+ deregistrations, err := meter.Int64Counter("deregistrations_total",
+ metric.WithDescription("Total number of peer deregistrations"),
+ )
if err != nil {
return nil, err
}
- registrationFailures, err := meter.Int64Counter("registration_failures_total")
+ registrationFailures, err := meter.Int64Counter("registration_failures_total",
+ metric.WithDescription("Total number of peer registration failures"),
+ )
if err != nil {
return nil, err
}
registrationDelay, err := meter.Float64Histogram("registration_delay_milliseconds",
- metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
+ metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
+ metric.WithDescription("Duration of how long it takes to register a peer"),
+ )
if err != nil {
return nil, err
}
getRegistrationDelay, err := meter.Float64Histogram("get_registration_delay_milliseconds",
- metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
+ metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
+ metric.WithDescription("Duration of how long it takes to load a connection from the registry"),
+ )
if err != nil {
return nil, err
}
- messagesForwarded, err := meter.Int64Counter("messages_forwarded_total")
+ messagesForwarded, err := meter.Int64Counter("messages_forwarded_total",
+ metric.WithDescription("Total number of messages forwarded to peers"),
+ )
if err != nil {
return nil, err
}
- messageForwardFailures, err := meter.Int64Counter("message_forward_failures_total")
+ messageForwardFailures, err := meter.Int64Counter("message_forward_failures_total",
+ metric.WithDescription("Total number of message forwarding failures"),
+ )
if err != nil {
return nil, err
}
messageForwardLatency, err := meter.Float64Histogram("message_forward_latency_milliseconds",
- metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...))
+ metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
+ metric.WithDescription("Duration of how long it takes to forward a message to a peer"),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ messageSize, err := meter.Int64Histogram(
+ "message.size.bytes",
+ metric.WithUnit("bytes"),
+ metric.WithExplicitBucketBoundaries(getMessageSizeBucketBoundaries()...),
+ metric.WithDescription("Records the size of each message sent"),
+ )
if err != nil {
return nil, err
}
@@ -92,9 +124,26 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) {
MessagesForwarded: messagesForwarded,
MessageForwardFailures: messageForwardFailures,
MessageForwardLatency: messageForwardLatency,
+
+ MessageSize: messageSize,
}, nil
}
+func getMessageSizeBucketBoundaries() []float64 {
+ return []float64{
+ 100,
+ 250,
+ 500,
+ 1000,
+ 5000,
+ 10000,
+ 50000,
+ 100000,
+ 500000,
+ 1000000,
+ }
+}
+
func getStandardBucketBoundaries() []float64 {
return []float64{
0.1,
diff --git a/signal/server/signal.go b/signal/server/signal.go
index 305fd052b..3cae7e860 100644
--- a/signal/server/signal.go
+++ b/signal/server/signal.go
@@ -13,6 +13,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
+ gproto "google.golang.org/protobuf/proto"
"github.com/netbirdio/netbird/signal/metrics"
"github.com/netbirdio/netbird/signal/peer"
@@ -52,13 +53,13 @@ func NewServer(ctx context.Context, meter metric.Meter) (*Server, error) {
return nil, fmt.Errorf("creating app metrics: %v", err)
}
- dispatcher, err := dispatcher.NewDispatcher(ctx, meter)
+ d, err := dispatcher.NewDispatcher(ctx, meter)
if err != nil {
return nil, fmt.Errorf("creating dispatcher: %v", err)
}
s := &Server{
- dispatcher: dispatcher,
+ dispatcher: d,
registry: peer.NewRegistry(appMetrics),
metrics: appMetrics,
}
@@ -75,7 +76,7 @@ func (s *Server) Send(ctx context.Context, msg *proto.EncryptedMessage) (*proto.
return &proto.EncryptedMessage{}, nil
}
- return s.dispatcher.SendMessage(context.Background(), msg)
+ return s.dispatcher.SendMessage(ctx, msg)
}
// ConnectStream connects to the exchange stream
@@ -98,76 +99,83 @@ func (s *Server) ConnectStream(stream proto.SignalExchange_ConnectStreamServer)
log.Debugf("peer connected [%s] [streamID %d] ", p.Id, p.StreamID)
for {
- // read incoming messages
- msg, err := stream.Recv()
- if err == io.EOF {
- break
- } else if err != nil {
- return err
- }
+ select {
+ case <-stream.Context().Done():
+ log.Debugf("stream closed for peer [%s] [streamID %d] due to context cancellation", p.Id, p.StreamID)
+ return stream.Context().Err()
+ default:
+ // read incoming messages
+ msg, err := stream.Recv()
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ return err
+ }
- log.Debugf("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
+ log.Debugf("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
- _, err = s.dispatcher.SendMessage(stream.Context(), msg)
- if err != nil {
- log.Debugf("error while sending message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
+ _, err = s.dispatcher.SendMessage(stream.Context(), msg)
+ if err != nil {
+ log.Debugf("error while sending message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
+ }
}
}
-
- <-stream.Context().Done()
- return stream.Context().Err()
}
func (s *Server) RegisterPeer(stream proto.SignalExchange_ConnectStreamServer) (*peer.Peer, error) {
log.Debugf("registering new peer")
- if meta, hasMeta := metadata.FromIncomingContext(stream.Context()); hasMeta {
- if id, found := meta[proto.HeaderId]; found {
- p := peer.NewPeer(id[0], stream)
-
- s.registry.Register(p)
- s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer)
-
- return p, nil
- } else {
- s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorMissingId)))
- return nil, status.Errorf(codes.FailedPrecondition, "missing connection header: "+proto.HeaderId)
- }
- } else {
+ meta, hasMeta := metadata.FromIncomingContext(stream.Context())
+ if !hasMeta {
s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorMissingMeta)))
return nil, status.Errorf(codes.FailedPrecondition, "missing connection stream meta")
}
+
+ id, found := meta[proto.HeaderId]
+ if !found {
+ s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorMissingId)))
+ return nil, status.Errorf(codes.FailedPrecondition, "missing connection header: %s", proto.HeaderId)
+ }
+
+ p := peer.NewPeer(id[0], stream)
+ s.registry.Register(p)
+ s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer)
+ return p, nil
}
func (s *Server) DeregisterPeer(p *peer.Peer) {
log.Debugf("peer disconnected [%s] [streamID %d] ", p.Id, p.StreamID)
s.registry.Deregister(p)
-
s.metrics.PeerConnectionDuration.Record(p.Stream.Context(), int64(time.Since(p.RegisteredAt).Seconds()))
}
func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedMessage) {
log.Debugf("forwarding a new message from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
-
getRegistrationStart := time.Now()
// lookup the target peer where the message is going to
- if dstPeer, found := s.registry.Get(msg.RemoteKey); found {
- s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationFound)))
- start := time.Now()
- // forward the message to the target peer
- if err := dstPeer.Stream.Send(msg); err != nil {
- log.Warnf("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
- // todo respond to the sender?
- s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeError)))
- } else {
- // in milliseconds
- s.metrics.MessageForwardLatency.Record(ctx, float64(time.Since(start).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream)))
- s.metrics.MessagesForwarded.Add(ctx, 1)
- }
- } else {
+ dstPeer, found := s.registry.Get(msg.RemoteKey)
+
+ if !found {
s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationNotFound)))
s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeNotConnected)))
log.Debugf("message from peer [%s] can't be forwarded to peer [%s] because destination peer is not connected", msg.Key, msg.RemoteKey)
// todo respond to the sender?
+ return
}
+
+ s.metrics.GetRegistrationDelay.Record(ctx, float64(time.Since(getRegistrationStart).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream), attribute.String(labelRegistrationStatus, labelRegistrationFound)))
+ start := time.Now()
+
+ // forward the message to the target peer
+ if err := dstPeer.Stream.Send(msg); err != nil {
+ log.Warnf("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
+ // todo respond to the sender?
+ s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeError)))
+ return
+ }
+
+ // in milliseconds
+ s.metrics.MessageForwardLatency.Record(ctx, float64(time.Since(start).Nanoseconds())/1e6, metric.WithAttributes(attribute.String(labelType, labelTypeStream)))
+ s.metrics.MessagesForwarded.Add(ctx, 1)
+ s.metrics.MessageSize.Record(ctx, int64(gproto.Size(msg)), metric.WithAttributes(attribute.String(labelType, labelTypeMessage)))
}
diff --git a/util/embeddedroots/embeddedroots.go b/util/embeddedroots/embeddedroots.go
new file mode 100644
index 000000000..d205f5b69
--- /dev/null
+++ b/util/embeddedroots/embeddedroots.go
@@ -0,0 +1,42 @@
+package embeddedroots
+
+import (
+ "crypto/x509"
+ _ "embed"
+ "sync"
+)
+
+func Get() *x509.CertPool {
+ rootsVar.load()
+ return rootsVar.p
+}
+
+type roots struct {
+ once sync.Once
+ p *x509.CertPool
+}
+
+var rootsVar roots
+
+func (r *roots) load() {
+ r.once.Do(func() {
+ p := x509.NewCertPool()
+ p.AppendCertsFromPEM([]byte(isrgRootX1RootPEM))
+ p.AppendCertsFromPEM([]byte(isrgRootX2RootPEM))
+ r.p = p
+ })
+}
+
+// Subject: O = Internet Security Research Group, CN = ISRG Root X1
+// Key type: RSA 4096
+// Validity: until 2030-06-04 (generated 2015-06-04)
+//
+//go:embed isrg-root-x1.pem
+var isrgRootX1RootPEM string
+
+// Subject: O = Internet Security Research Group, CN = ISRG Root X2
+// Key type: ECDSA P-384
+// Validity: until 2035-09-04 (generated 2020-09-04)
+//
+//go:embed isrg-root-x2.pem
+var isrgRootX2RootPEM string
diff --git a/util/embeddedroots/isrg-root-x1.pem b/util/embeddedroots/isrg-root-x1.pem
new file mode 100644
index 000000000..57d4a3766
--- /dev/null
+++ b/util/embeddedroots/isrg-root-x1.pem
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
+WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
+ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
+MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
+h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
+A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
+T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
+B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
+B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
+KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
+OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
+jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
+qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
+rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
+hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
+ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
+3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
+NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
+ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
+TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
+jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
+oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
+4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
+mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
+emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/util/embeddedroots/isrg-root-x2.pem b/util/embeddedroots/isrg-root-x2.pem
new file mode 100644
index 000000000..7d903edc9
--- /dev/null
+++ b/util/embeddedroots/isrg-root-x2.pem
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
+CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
+R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
+MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
+ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
+EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
+ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
+AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
+zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
+tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
+/q4AaOeMSQ+2b1tbFfLn
+-----END CERTIFICATE-----
diff --git a/util/grpc/dialer.go b/util/grpc/dialer.go
index 4fbffe342..f6d6d2f04 100644
--- a/util/grpc/dialer.go
+++ b/util/grpc/dialer.go
@@ -3,14 +3,16 @@ package grpc
import (
"context"
"crypto/tls"
+ "crypto/x509"
"fmt"
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/status"
"net"
"os/user"
"runtime"
"time"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
@@ -18,6 +20,7 @@ import (
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
+ "github.com/netbirdio/netbird/util/embeddedroots"
nbnet "github.com/netbirdio/netbird/util/net"
)
@@ -37,7 +40,6 @@ func WithCustomDialer() grpc.DialOption {
}
}
- log.Debug("Using nbnet.NewDialer()")
conn, err := nbnet.NewDialer().DialContext(ctx, "tcp", addr)
if err != nil {
log.Errorf("Failed to dial: %s", err)
@@ -57,9 +59,16 @@ func Backoff(ctx context.Context) backoff.BackOff {
func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
-
if tlsEnabled {
- transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))
+ certPool, err := x509.SystemCertPool()
+ if err != nil || certPool == nil {
+ log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
+ certPool = embeddedroots.Get()
+ }
+
+ transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
+ RootCAs: certPool,
+ }))
}
connCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
diff --git a/util/net/env.go b/util/net/env.go
index 099da39b7..32425665d 100644
--- a/util/net/env.go
+++ b/util/net/env.go
@@ -2,6 +2,7 @@ package net
import (
"os"
+ "strconv"
log "github.com/sirupsen/logrus"
@@ -10,20 +11,24 @@ import (
const (
envDisableCustomRouting = "NB_DISABLE_CUSTOM_ROUTING"
- envSkipSocketMark = "NB_SKIP_SOCKET_MARK"
)
+// CustomRoutingDisabled returns true if custom routing is disabled.
+// This will fall back to the operation mode before the exit node functionality was implemented.
+// In particular exclusion routes won't be set up and all dialers and listeners will use net.Dial and net.Listen, respectively.
func CustomRoutingDisabled() bool {
if netstack.IsEnabled() {
return true
}
- return os.Getenv(envDisableCustomRouting) == "true"
-}
-func SkipSocketMark() bool {
- if skipSocketMark := os.Getenv(envSkipSocketMark); skipSocketMark == "true" {
- log.Infof("%s is set to true, skipping SO_MARK", envSkipSocketMark)
- return true
+ var customRoutingDisabled bool
+ if val := os.Getenv(envDisableCustomRouting); val != "" {
+ var err error
+ customRoutingDisabled, err = strconv.ParseBool(val)
+ if err != nil {
+ log.Warnf("failed to parse %s: %v", envDisableCustomRouting, err)
+ }
}
- return false
+
+ return customRoutingDisabled
}
diff --git a/util/net/env_generic.go b/util/net/env_generic.go
new file mode 100644
index 000000000..6d142a838
--- /dev/null
+++ b/util/net/env_generic.go
@@ -0,0 +1,12 @@
+//go:build !linux || android
+
+package net
+
+func Init() {
+ // nothing to do on non-linux
+}
+
+func AdvancedRouting() bool {
+ // non-linux currently doesn't support advanced routing
+ return false
+}
diff --git a/util/net/env_linux.go b/util/net/env_linux.go
new file mode 100644
index 000000000..124bf64de
--- /dev/null
+++ b/util/net/env_linux.go
@@ -0,0 +1,119 @@
+//go:build linux && !android
+
+package net
+
+import (
+ "errors"
+ "os"
+ "strconv"
+ "syscall"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/vishvananda/netlink"
+
+ "github.com/netbirdio/netbird/client/iface/netstack"
+)
+
+const (
+ // these have the same effect, skip socket env supported for backward compatibility
+ envSkipSocketMark = "NB_SKIP_SOCKET_MARK"
+ envUseLegacyRouting = "NB_USE_LEGACY_ROUTING"
+)
+
+var advancedRoutingSupported bool
+
+func Init() {
+ advancedRoutingSupported = checkAdvancedRoutingSupport()
+}
+
+func AdvancedRouting() bool {
+ return advancedRoutingSupported
+}
+
+func checkAdvancedRoutingSupport() bool {
+ var err error
+
+ var legacyRouting bool
+ if val := os.Getenv(envUseLegacyRouting); val != "" {
+ legacyRouting, err = strconv.ParseBool(val)
+ if err != nil {
+ log.Warnf("failed to parse %s: %v", envUseLegacyRouting, err)
+ }
+ }
+
+ var skipSocketMark bool
+ if val := os.Getenv(envSkipSocketMark); val != "" {
+ skipSocketMark, err = strconv.ParseBool(val)
+ if err != nil {
+ log.Warnf("failed to parse %s: %v", envSkipSocketMark, err)
+ }
+ }
+
+ // requested to disable advanced routing
+ if legacyRouting || skipSocketMark ||
+ // envCustomRoutingDisabled disables the custom dialers.
+ // There is no point in using advanced routing without those, as they set up fwmarks on the sockets.
+ CustomRoutingDisabled() ||
+ // netstack mode doesn't need routing at all
+ netstack.IsEnabled() {
+
+ log.Info("advanced routing has been requested to be disabled")
+ return false
+ }
+
+ if !CheckFwmarkSupport() || !CheckRuleOperationsSupport() {
+ log.Warn("system doesn't support required routing features, falling back to legacy routing")
+ return false
+ }
+
+ log.Info("system supports advanced routing")
+
+ return true
+}
+
+func CheckFwmarkSupport() bool {
+ // temporarily enable advanced routing to check fwmarks are supported
+ old := advancedRoutingSupported
+ advancedRoutingSupported = true
+ defer func() {
+ advancedRoutingSupported = old
+ }()
+
+ dialer := NewDialer()
+ dialer.Timeout = 100 * time.Millisecond
+
+ conn, err := dialer.Dial("udp", "127.0.0.1:9")
+ if err != nil {
+ log.Warnf("failed to dial with fwmark: %v", err)
+ return false
+ }
+ if err := conn.Close(); err != nil {
+ log.Warnf("failed to close connection: %v", err)
+
+ }
+
+ return true
+}
+
+func CheckRuleOperationsSupport() bool {
+ rule := netlink.NewRule()
+ // low precedence, semi-random
+ rule.Priority = 32321
+ rule.Table = syscall.RT_TABLE_MAIN
+ rule.Family = netlink.FAMILY_V4
+
+ if err := netlink.RuleAdd(rule); err != nil {
+ if errors.Is(err, syscall.EOPNOTSUPP) {
+ log.Warn("IP rule operations are not supported")
+ return false
+ }
+ log.Warnf("failed to test rule support: %v", err)
+ return false
+ }
+
+ if err := netlink.RuleDel(rule); err != nil {
+ log.Warnf("failed to delete test rule: %v", err)
+ }
+ return true
+}
diff --git a/util/net/net.go b/util/net/net.go
index 403aa87e7..7b43b952f 100644
--- a/util/net/net.go
+++ b/util/net/net.go
@@ -1,6 +1,7 @@
package net
import (
+ "math/big"
"net"
"github.com/google/uuid"
@@ -26,3 +27,22 @@ type RemoveHookFunc func(connID ConnectionID) error
func GenerateConnID() ConnectionID {
return ConnectionID(uuid.NewString())
}
+
+func GetLastIPFromNetwork(network *net.IPNet, fromEnd int) net.IP {
+ // Calculate the last IP in the CIDR range
+ var endIP net.IP
+ for i := 0; i < len(network.IP); i++ {
+ endIP = append(endIP, network.IP[i]|^network.Mask[i])
+ }
+
+ // convert to big.Int
+ endInt := big.NewInt(0)
+ endInt.SetBytes(endIP)
+
+ // subtract fromEnd from the last ip
+ fromEndBig := big.NewInt(int64(fromEnd))
+ resultInt := big.NewInt(0)
+ resultInt.Sub(endInt, fromEndBig)
+
+ return resultInt.Bytes()
+}
diff --git a/util/net/net_linux.go b/util/net/net_linux.go
index fc486ebd4..eae483a26 100644
--- a/util/net/net_linux.go
+++ b/util/net/net_linux.go
@@ -5,13 +5,11 @@ package net
import (
"fmt"
"syscall"
-
- log "github.com/sirupsen/logrus"
)
// SetSocketMark sets the SO_MARK option on the given socket connection
func SetSocketMark(conn syscall.Conn) error {
- if isSocketMarkDisabled() {
+ if !AdvancedRouting() {
return nil
}
@@ -25,7 +23,7 @@ func SetSocketMark(conn syscall.Conn) error {
// SetSocketOpt sets the SO_MARK option on the given file descriptor
func SetSocketOpt(fd int) error {
- if isSocketMarkDisabled() {
+ if !AdvancedRouting() {
return nil
}
@@ -36,7 +34,7 @@ func setRawSocketMark(conn syscall.RawConn) error {
var setErr error
err := conn.Control(func(fd uintptr) {
- if isSocketMarkDisabled() {
+ if !AdvancedRouting() {
return
}
setErr = setSocketOptInt(int(fd))
@@ -55,15 +53,3 @@ func setRawSocketMark(conn syscall.RawConn) error {
func setSocketOptInt(fd int) error {
return syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_MARK, NetbirdFwmark)
}
-
-func isSocketMarkDisabled() bool {
- if CustomRoutingDisabled() {
- log.Infof("Custom routing is disabled, skipping SO_MARK")
- return true
- }
-
- if SkipSocketMark() {
- return true
- }
- return false
-}