diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 4e584ecc2..b23baf031 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -10,6 +10,18 @@ concurrency: cancel-in-progress: true jobs: + codespell: + name: codespell + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: codespell + uses: codespell-project/actions-codespell@v2 + with: + ignore_words_list: erro,clienta + skip: go.mod,go.sum + only_warn: 1 golangci: strategy: fail-fast: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5833638c5..c3d43a65b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ on: - 'client/ui/**' env: - SIGN_PIPE_VER: "v0.0.9" + SIGN_PIPE_VER: "v0.0.10" GORELEASER_VER: "v1.14.1" concurrency: diff --git a/.golangci.yaml b/.golangci.yaml index 5034db708..757a60a39 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -12,6 +12,50 @@ linters-settings: # Default: false check-type-assertions: false + gosec: + includes: + - G101 # Look for hard coded credentials + #- G102 # Bind to all interfaces + - G103 # Audit the use of unsafe block + - G104 # Audit errors not checked + - G106 # Audit the use of ssh.InsecureIgnoreHostKey + #- G107 # Url provided to HTTP request as taint input + - G108 # Profiling endpoint automatically exposed on /debug/pprof + - G109 # Potential Integer overflow made by strconv.Atoi result conversion to int16/32 + - G110 # Potential DoS vulnerability via decompression bomb + - G111 # Potential directory traversal + #- G112 # Potential slowloris attack + - G113 # Usage of Rat.SetString in math/big with an overflow (CVE-2022-23772) + #- G114 # Use of net/http serve function that has no support for setting timeouts + - G201 # SQL query construction using format string + - G202 # SQL query construction using string concatenation + - G203 # Use of unescaped data in HTML templates + #- G204 # Audit use of command execution + - G301 # Poor file permissions used when creating a directory + - G302 # Poor file permissions used with chmod + - G303 # Creating tempfile using a predictable path + - G304 # File path provided as taint input + - G305 # File traversal when extracting zip/tar archive + - G306 # Poor file permissions used when writing to a new file + - G307 # Poor file permissions used when creating a file with os.Create + #- G401 # Detect the usage of DES, RC4, MD5 or SHA1 + #- G402 # Look for bad TLS connection settings + - G403 # Ensure minimum RSA key length of 2048 bits + #- G404 # Insecure random number source (rand) + #- G501 # Import blocklist: crypto/md5 + - G502 # Import blocklist: crypto/des + - G503 # Import blocklist: crypto/rc4 + - G504 # Import blocklist: net/http/cgi + #- G505 # Import blocklist: crypto/sha1 + - G601 # Implicit memory aliasing of items from a range statement + - G602 # Slice access out of bounds + + gocritic: + disabled-checks: + - commentFormatting + - captLocal + - deprecatedComment + govet: # Enable all analyzers. # Default: false @@ -19,6 +63,12 @@ linters-settings: enable: - nilness + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + linters: disable-all: true enable: @@ -28,13 +78,23 @@ linters: - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - ineffassign # detects when assignments to existing variables are not used - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - tenv # Tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17. - typecheck # like the front-end of a Go compiler, parses and type-checks Go code - unused # checks for unused constants, variables, functions and types ## disable by default but the have interesting results so lets add them - bodyclose # checks whether HTTP response body is closed successfully + - dupword # dupword checks for duplicate words in the source code + - durationcheck # durationcheck checks for two durations multiplied together + - forbidigo # forbidigo forbids identifiers + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gosec # inspects source code for security problems + - mirror # mirror reports wrong mirror patterns of bytes/strings usage + - misspell # misspess finds commonly misspelled English words in comments - nilerr # finds the code that returns nil even if it checks that the error is not nil - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - predeclared # predeclared finds code that shadows one of Go's predeclared identifiers - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers. - wastedassign # wastedassign finds wasted assignment statements issues: # Maximum count of issues with the same text. @@ -43,12 +103,21 @@ issues: max-same-issues: 5 exclude-rules: - - path: sharedsock/filter.go + # allow fmt + - path: management/cmd/root\.go + linters: forbidigo + - path: signal/cmd/root\.go + linters: forbidigo + - path: sharedsock/filter\.go linters: - unused - - path: client/firewall/iptables/rule.go + - path: client/firewall/iptables/rule\.go linters: - unused - - path: mock.go + - path: test\.go linters: - - nilnil \ No newline at end of file + - mirror + - gosec + - path: mock\.go + linters: + - nilnil diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80be72fa9..29a12402e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -183,6 +183,42 @@ To start NetBird the management service: ./management management --log-level debug --log-file console --config ./management.json ``` +#### Windows Netbird Installer +Create dist directory +```shell +mkdir -p dist/netbird_windows_amd64 +``` + +UI client +```shell +CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -o netbird-ui.exe -ldflags "-s -w -H windowsgui" ./client/ui +mv netbird-ui.exe ./dist/netbird_windows_amd64/ +``` + +Client +```shell +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o netbird.exe ./client/ +mv netbird.exe ./dist/netbird_windows_amd64/ +``` +> Windows clients have a Wireguard driver requirement. You can download the wintun driver from https://www.wintun.net/builds/wintun-0.14.1.zip, after decompressing, you can copy the file `windtun\bin\ARCH\wintun.dll` to `./dist/netbird_windows_amd64/`. + +NSIS compiler +- [Windows-nsis]( https://nsis.sourceforge.io/Download) +- [MacOS-makensis](https://formulae.brew.sh/formula/makensis#default) +- [Linux-makensis](https://manpages.ubuntu.com/manpages/trusty/man1/makensis.1.html) + +NSIS Plugins. Download and move them to the NSIS plugins folder. +- [EnVar](https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip) +- [ShellExecAsUser](https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z) + +Windows Installer +```shell +export APPVER=0.0.0.1 +makensis -V4 client/installer.nsis +``` + +The installer `netbird-installer.exe` will be created in root directory. + ### Test suite The tests can be started via: diff --git a/client/android/login.go b/client/android/login.go index afd61055f..3840c75c1 100644 --- a/client/android/login.go +++ b/client/android/login.go @@ -205,8 +205,8 @@ func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, err go urlOpener.Open(flowInfo.VerificationURIComplete) - waitTimeout := time.Duration(flowInfo.ExpiresIn) - waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout*time.Second) + waitTimeout := time.Duration(flowInfo.ExpiresIn) * time.Second + waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout) defer cancel() tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo) if err != nil { diff --git a/client/cmd/login.go b/client/cmd/login.go index 2ddab46f3..ac79199e2 100644 --- a/client/cmd/login.go +++ b/client/cmd/login.go @@ -85,6 +85,7 @@ var loginCmd = &cobra.Command{ PreSharedKey: preSharedKey, ManagementUrl: managementURL, IsLinuxDesktopClient: isLinuxRunningDesktop(), + Hostname: hostName, } var loginErr error @@ -114,7 +115,7 @@ var loginCmd = &cobra.Command{ if loginResp.NeedsSSOLogin { openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode) - _, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode}) + _, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName}) if err != nil { return fmt.Errorf("waiting sso login failed with: %v", err) } @@ -177,8 +178,8 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode) - waitTimeout := time.Duration(flowInfo.ExpiresIn) - waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second) + waitTimeout := time.Duration(flowInfo.ExpiresIn) * time.Second + waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout) defer c() tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo) diff --git a/client/cmd/root.go b/client/cmd/root.go index 770bd21fa..24c027d0c 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -92,7 +92,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL)) rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location") rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level") - rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the the log will be output to stdout") + rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout") rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)") rootCmd.PersistentFlags().StringVar(&preSharedKey, "preshared-key", "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.") rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device") diff --git a/client/cmd/status.go b/client/cmd/status.go index 9dfd042f8..74d2061ff 100644 --- a/client/cmd/status.go +++ b/client/cmd/status.go @@ -234,7 +234,7 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput { continue } if isPeerConnected { - peersConnected = peersConnected + 1 + peersConnected++ localICE = pbPeerState.GetLocalIceCandidateType() remoteICE = pbPeerState.GetRemoteIceCandidateType() @@ -407,7 +407,7 @@ func parsePeers(peers peersStateOutput) string { peerState.LastStatusUpdate.Format("2006-01-02 15:04:05"), ) - peersString = peersString + peerString + peersString += peerString } return peersString } diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go index 47ae9ddb4..28423776f 100644 --- a/client/cmd/testutil.go +++ b/client/cmd/testutil.go @@ -22,6 +22,7 @@ import ( ) func startTestingServices(t *testing.T) string { + t.Helper() config := &mgmt.Config{} _, err := util.ReadJson("../testdata/management.json", config) if err != nil { @@ -44,6 +45,7 @@ func startTestingServices(t *testing.T) string { } func startSignal(t *testing.T) (*grpc.Server, net.Listener) { + t.Helper() lis, err := net.Listen("tcp", ":0") if err != nil { t.Fatal(err) @@ -60,6 +62,7 @@ func startSignal(t *testing.T) (*grpc.Server, net.Listener) { } func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Listener) { + t.Helper() lis, err := net.Listen("tcp", ":0") if err != nil { t.Fatal(err) @@ -70,7 +73,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste t.Fatal(err) } - peersUpdateManager := mgmt.NewPeersUpdateManager() + peersUpdateManager := mgmt.NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} if err != nil { return nil, nil @@ -98,6 +101,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste func startClientDaemon( t *testing.T, ctx context.Context, managementURL, configPath string, ) (*grpc.Server, net.Listener) { + t.Helper() lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) diff --git a/client/cmd/up.go b/client/cmd/up.go index 80ed04b57..dd4c7290e 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -149,6 +149,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0, CustomDNSAddress: customDNSAddressConverted, IsLinuxDesktopClient: isLinuxRunningDesktop(), + Hostname: hostName, } var loginErr error @@ -179,7 +180,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode) - _, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode}) + _, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName}) if err != nil { return fmt.Errorf("waiting sso login failed with: %v", err) } diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index 4ce904df6..b9243f4ca 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -463,14 +463,16 @@ func (m *Manager) actionToStr(action fw.Action) string { } func (m *Manager) transformIPsetName(ipsetName string, sPort, dPort string) string { - if ipsetName == "" { + switch { + case ipsetName == "": return "" - } else if sPort != "" && dPort != "" { + case sPort != "" && dPort != "": return ipsetName + "-sport-dport" - } else if sPort != "" { + case sPort != "": return ipsetName + "-sport" - } else if dPort != "" { + case dPort != "": return ipsetName + "-dport" + default: + return ipsetName } - return ipsetName } diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index 2d2013aa2..90375d3e2 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -206,6 +206,7 @@ func TestIptablesManagerIPSet(t *testing.T) { } func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, chainName string, mustExists bool, rulespec ...string) { + t.Helper() exists, err := ipv4Client.Exists("filter", chainName, rulespec...) require.NoError(t, err, "failed to check rule") require.Falsef(t, !exists && mustExists, "rule '%v' does not exist", rulespec) diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index 6c46048b4..93379bad8 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -791,7 +791,7 @@ func (m *Manager) flushWithBackoff() (err error) { return err } time.Sleep(backoffTime) - backoffTime = backoffTime * 2 + backoffTime *= 2 continue } break diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 6fd11e652..7119e791c 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -355,14 +355,16 @@ func (m *Manager) RemovePacketHook(hookID string) error { for _, arr := range m.incomingRules { for _, r := range arr { if r.id == hookID { - return m.DeleteRule(&r) + rule := r + return m.DeleteRule(&rule) } } } for _, arr := range m.outgoingRules { for _, r := range arr { if r.id == hookID { - return m.DeleteRule(&r) + rule := r + return m.DeleteRule(&rule) } } } diff --git a/client/installer.nsis b/client/installer.nsis index e2e3ac118..fbffa326d 100644 --- a/client/installer.nsis +++ b/client/installer.nsis @@ -166,10 +166,9 @@ WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}" EnVar::SetHKLM EnVar::AddValueEx "path" "$INSTDIR" -SetShellVarContext current +SetShellVarContext all CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}" CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}" -SetShellVarContext all SectionEnd Section -Post @@ -196,10 +195,9 @@ Delete "$INSTDIR\${MAIN_APP_EXE}" Delete "$INSTDIR\wintun.dll" RmDir /r "$INSTDIR" -SetShellVarContext current +SetShellVarContext all Delete "$DESKTOP\${APP_NAME}.lnk" Delete "$SMPROGRAMS\${APP_NAME}.lnk" -SetShellVarContext all DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}" @@ -209,8 +207,7 @@ SectionEnd Function LaunchLink -SetShellVarContext current +SetShellVarContext all SetOutPath $INSTDIR ShellExecAsUser::ShellExecAsUser "" "$DESKTOP\${APP_NAME}.lnk" -SetShellVarContext all FunctionEnd diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go index 518e895cf..d55a1cad6 100644 --- a/client/internal/acl/manager_test.go +++ b/client/internal/acl/manager_test.go @@ -189,31 +189,33 @@ func TestDefaultManagerSquashRules(t *testing.T) { } r := rules[0] - if r.PeerIP != "0.0.0.0" { + switch { + case r.PeerIP != "0.0.0.0": t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP) return - } else if r.Direction != mgmProto.FirewallRule_IN { + case r.Direction != mgmProto.FirewallRule_IN: t.Errorf("direction should be IN, got: %v", r.Direction) return - } else if r.Protocol != mgmProto.FirewallRule_ALL { + case r.Protocol != mgmProto.FirewallRule_ALL: t.Errorf("protocol should be ALL, got: %v", r.Protocol) return - } else if r.Action != mgmProto.FirewallRule_ACCEPT { + case r.Action != mgmProto.FirewallRule_ACCEPT: t.Errorf("action should be ACCEPT, got: %v", r.Action) return } r = rules[1] - if r.PeerIP != "0.0.0.0" { + switch { + case r.PeerIP != "0.0.0.0": t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP) return - } else if r.Direction != mgmProto.FirewallRule_OUT { + case r.Direction != mgmProto.FirewallRule_OUT: t.Errorf("direction should be OUT, got: %v", r.Direction) return - } else if r.Protocol != mgmProto.FirewallRule_ALL { + case r.Protocol != mgmProto.FirewallRule_ALL: t.Errorf("protocol should be ALL, got: %v", r.Protocol) return - } else if r.Action != mgmProto.FirewallRule_ACCEPT { + case r.Action != mgmProto.FirewallRule_ACCEPT: t.Errorf("action should be ACCEPT, got: %v", r.Action) return } @@ -281,7 +283,7 @@ func TestDefaultManagerSquashRulesNoAffect(t *testing.T) { manager := &DefaultManager{} if rules, _ := manager.squashAcceptRules(networkMap); len(rules) != len(networkMap.FirewallRules) { - t.Errorf("we should got same amount of rules as intput, got %v", len(rules)) + t.Errorf("we should get the same amount of rules as output, got %v", len(rules)) } } diff --git a/client/internal/auth/device_flow.go b/client/internal/auth/device_flow.go index c28e42772..3c51fe4f5 100644 --- a/client/internal/auth/device_flow.go +++ b/client/internal/auth/device_flow.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "fmt" - "github.com/netbirdio/netbird/client/internal" "io" "net/http" "net/url" "strings" "time" + + "github.com/netbirdio/netbird/client/internal" ) // HostedGrantType grant type for device flow on Hosted @@ -174,7 +175,7 @@ func (d *DeviceAuthorizationFlow) WaitToken(ctx context.Context, info AuthFlowIn if tokenResponse.Error == "authorization_pending" { continue } else if tokenResponse.Error == "slow_down" { - interval = interval + (3 * time.Second) + interval += (3 * time.Second) ticker.Reset(interval) continue } diff --git a/client/internal/auth/oauth.go b/client/internal/auth/oauth.go index 82adf91b9..23bde2be2 100644 --- a/client/internal/auth/oauth.go +++ b/client/internal/auth/oauth.go @@ -92,15 +92,15 @@ func authenticateWithPKCEFlow(ctx context.Context, config *internal.Config) (OAu func authenticateWithDeviceCodeFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) { deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL) if err != nil { - s, ok := gstatus.FromError(err) - if ok && s.Code() == codes.NotFound { + switch s, ok := gstatus.FromError(err); { + case ok && s.Code() == codes.NotFound: return nil, fmt.Errorf("no SSO provider returned from management. " + "Please proceed with setting up this device using setup keys " + "https://docs.netbird.io/how-to/register-machines-using-setup-keys") - } else if ok && s.Code() == codes.Unimplemented { + case ok && s.Code() == codes.Unimplemented: return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+ "please update your server or use Setup Keys to login", config.ManagementURL) - } else { + default: return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err) } } diff --git a/client/internal/config.go b/client/internal/config.go index cd665016b..646848a2f 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -273,9 +273,9 @@ func parseURL(serviceName, serviceURL string) (*url.URL, error) { if parsedMgmtURL.Port() == "" { switch parsedMgmtURL.Scheme { case "https": - parsedMgmtURL.Host = parsedMgmtURL.Host + ":443" + parsedMgmtURL.Host += ":443" case "http": - parsedMgmtURL.Host = parsedMgmtURL.Host + ":80" + parsedMgmtURL.Host += ":80" default: log.Infof("unable to determine a default port for schema %s in URL %s", parsedMgmtURL.Scheme, serviceURL) } diff --git a/client/internal/dns/network_manager_linux.go b/client/internal/dns/network_manager_linux.go index 805bd5390..548c6b8fd 100644 --- a/client/internal/dns/network_manager_linux.go +++ b/client/internal/dns/network_manager_linux.go @@ -7,12 +7,12 @@ import ( "encoding/binary" "fmt" "net/netip" - "regexp" "time" "github.com/godbus/dbus/v5" "github.com/hashicorp/go-version" "github.com/miekg/dns" + nbversion "github.com/netbirdio/netbird/version" log "github.com/sirupsen/logrus" ) @@ -122,7 +122,7 @@ func (n *networkManagerDbusConfigurator) applyDNSConfig(config HostDNSConfig) er searchDomains = append(searchDomains, dns.Fqdn(dConf.domain)) } - newDomainList := append(searchDomains, matchDomains...) + newDomainList := append(searchDomains, matchDomains...) //nolint:gocritic priority := networkManagerDbusSearchDomainOnlyPriority switch { @@ -289,12 +289,7 @@ func isNetworkManagerSupportedVersion() bool { } func parseVersion(inputVersion string) (*version.Version, error) { - reg, err := regexp.Compile(version.SemverRegexpRaw) - if err != nil { - return nil, err - } - - if inputVersion == "" || !reg.MatchString(inputVersion) { + if inputVersion == "" || !nbversion.SemverRegexp.MatchString(inputVersion) { return nil, fmt.Errorf("couldn't parse the provided version: Not SemVer") } diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 185818ed5..1aa65ef18 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -267,7 +267,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { if err != nil { return fmt.Errorf("not applying dns update, error: %v", err) } - muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...) + muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...) //nolint:gocritic s.updateMux(muxUpdates) s.updateLocalResolver(localRecords) @@ -503,7 +503,18 @@ func (s *DefaultServer) addHostRootZone() { handler := newUpstreamResolver(s.ctx, s.interfaceName, s.wgAddr) handler.upstreamServers = make([]string, len(s.hostsDnsList)) for n, ua := range s.hostsDnsList { - handler.upstreamServers[n] = fmt.Sprintf("%s:53", ua) + a, err := netip.ParseAddr(ua) + if err != nil { + log.Errorf("invalid upstream IP address: %s, error: %s", ua, err) + continue + } + + ipString := ua + if !a.Is4() { + ipString = fmt.Sprintf("[%s]", ua) + } + + handler.upstreamServers[n] = fmt.Sprintf("%s:53", ipString) } handler.deactivate = func() {} handler.reactivate = func() {} diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index d9fec43c5..031543d80 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -322,9 +322,9 @@ func TestUpdateDNSServer(t *testing.T) { func TestDNSFakeResolverHandleUpdates(t *testing.T) { ov := os.Getenv("NB_WG_KERNEL_DISABLED") - defer os.Setenv("NB_WG_KERNEL_DISABLED", ov) + defer t.Setenv("NB_WG_KERNEL_DISABLED", ov) - _ = os.Setenv("NB_WG_KERNEL_DISABLED", "true") + t.Setenv("NB_WG_KERNEL_DISABLED", "true") newNet, err := stdnet.NewNet(nil) if err != nil { t.Errorf("create stdnet: %v", err) @@ -339,7 +339,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) { err = wgIface.Create() if err != nil { - t.Errorf("crate and init wireguard interface: %v", err) + t.Errorf("create and init wireguard interface: %v", err) return } defer func() { @@ -771,10 +771,11 @@ func TestDNSPermanent_matchOnly(t *testing.T) { } func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { + t.Helper() ov := os.Getenv("NB_WG_KERNEL_DISABLED") - defer os.Setenv("NB_WG_KERNEL_DISABLED", ov) + defer t.Setenv("NB_WG_KERNEL_DISABLED", ov) - _ = os.Setenv("NB_WG_KERNEL_DISABLED", "true") + t.Setenv("NB_WG_KERNEL_DISABLED", "true") newNet, err := stdnet.NewNet(nil) if err != nil { t.Fatalf("create stdnet: %v", err) @@ -789,7 +790,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { err = wgIface.Create() if err != nil { - t.Fatalf("crate and init wireguard interface: %v", err) + t.Fatalf("create and init wireguard interface: %v", err) return nil, err } diff --git a/client/internal/ebpf/ebpf/manager_linux.go b/client/internal/ebpf/ebpf/manager_linux.go index 9dfdc0ad1..7520a6387 100644 --- a/client/internal/ebpf/ebpf/manager_linux.go +++ b/client/internal/ebpf/ebpf/manager_linux.go @@ -50,7 +50,7 @@ func GetEbpfManagerInstance() manager.Manager { } func (tf *GeneralManager) setFeatureFlag(feature uint16) { - tf.featureFlags = tf.featureFlags | feature + tf.featureFlags |= feature } func (tf *GeneralManager) loadXdp() error { diff --git a/client/internal/ebpf/ebpf/manager_linux_test.go b/client/internal/ebpf/ebpf/manager_linux_test.go index 956499e5b..5664a4565 100644 --- a/client/internal/ebpf/ebpf/manager_linux_test.go +++ b/client/internal/ebpf/ebpf/manager_linux_test.go @@ -8,12 +8,12 @@ func TestManager_setFeatureFlag(t *testing.T) { mgr := GeneralManager{} mgr.setFeatureFlag(featureFlagWGProxy) if mgr.featureFlags != 1 { - t.Errorf("invalid faeture state") + t.Errorf("invalid feature state") } mgr.setFeatureFlag(featureFlagDnsForwarder) if mgr.featureFlags != 3 { - t.Errorf("invalid faeture state") + t.Errorf("invalid feature state") } } @@ -27,7 +27,7 @@ func TestManager_unsetFeatureFlag(t *testing.T) { t.Errorf("unexpected error: %s", err) } if mgr.featureFlags != 2 { - t.Errorf("invalid faeture state, expected: %d, got: %d", 2, mgr.featureFlags) + t.Errorf("invalid feature state, expected: %d, got: %d", 2, mgr.featureFlags) } err = mgr.unsetFeatureFlag(featureFlagDnsForwarder) @@ -35,6 +35,6 @@ func TestManager_unsetFeatureFlag(t *testing.T) { t.Errorf("unexpected error: %s", err) } if mgr.featureFlags != 0 { - t.Errorf("invalid faeture state, expected: %d, got: %d", 0, mgr.featureFlags) + t.Errorf("invalid feature state, expected: %d, got: %d", 0, mgr.featureFlags) } } diff --git a/client/internal/engine.go b/client/internal/engine.go index 8f1f1315f..c1258e6ac 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -204,16 +204,13 @@ func (e *Engine) Start() error { e.dnsServer = dns.NewDefaultServerPermanentUpstream(e.ctx, e.wgInterface, e.mobileDep.HostDNSAddresses, *dnsConfig, e.mobileDep.NetworkChangeListener) go e.mobileDep.DnsReadyListener.OnReady() } - } else { - // todo fix custom address - if e.dnsServer == nil { + } else if e.dnsServer == nil { e.dnsServer, err = dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress, e.mobileDep.InterfaceName, wgAddr) if err != nil { e.close() return err } - } - } + } e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, routes) e.mobileDep.NetworkChangeListener.SetInterfaceIP(wgAddr) @@ -498,15 +495,13 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error { } else { log.Debugf("SSH server is already running") } - } else { + } else if !isNil(e.sshServer) { // Disable SSH server request, so stop it if it was running - if !isNil(e.sshServer) { - err := e.sshServer.Stop() - if err != nil { - log.Warnf("failed to stop SSH server %v", err) - } - e.sshServer = nil + err := e.sshServer.Stop() + if err != nil { + log.Warnf("failed to stop SSH server %v", err) } + e.sshServer = nil } return nil } @@ -648,7 +643,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { if config.GetSshConfig() != nil && config.GetSshConfig().GetSshPubKey() != nil { err := e.sshServer.AddAuthorizedKey(config.WgPubKey, string(config.GetSshConfig().GetSshPubKey())) if err != nil { - log.Warnf("failed adding authroized key to SSH DefaultServer %v", err) + log.Warnf("failed adding authorized key to SSH DefaultServer %v", err) } } } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 42012bd0a..08cd29da8 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -869,7 +869,7 @@ loop: case <-ticker.C: totalConnected := 0 for _, engine := range engines { - totalConnected = totalConnected + getConnectedPeers(engine) + totalConnected += getConnectedPeers(engine) } if totalConnected == expectedConnected { log.Infof("total connected=%d", totalConnected) @@ -1044,7 +1044,7 @@ func startManagement(dataDir string) (*grpc.Server, string, error) { return nil, "", err } - peersUpdateManager := server.NewPeersUpdateManager() + peersUpdateManager := server.NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} if err != nil { return nil, "", err diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index db43c6ca1..db37c0528 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -247,7 +247,7 @@ func (conn *Conn) Open() error { } err := conn.statusRecorder.UpdatePeerState(peerState) if err != nil { - log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err) + log.Warnf("error while updating the state of peer %s,err: %v", conn.config.Key, err) } defer func() { @@ -306,7 +306,7 @@ func (conn *Conn) Open() error { } err = conn.statusRecorder.UpdatePeerState(peerState) if err != nil { - log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err) + log.Warnf("error while updating the state of peer %s,err: %v", conn.config.Key, err) } err = conn.agent.GatherCandidates() diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index f75991d85..c69354eac 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -174,13 +174,13 @@ func (d *Status) UpdatePeerState(receivedState State) error { return nil } -func shouldSkipNotify(new, curr State) bool { +func shouldSkipNotify(received, curr State) bool { switch { - case new.ConnStatus == StatusConnecting: + case received.ConnStatus == StatusConnecting: return true - case new.ConnStatus == StatusDisconnected && curr.ConnStatus == StatusConnecting: + case received.ConnStatus == StatusDisconnected && curr.ConnStatus == StatusConnecting: return true - case new.ConnStatus == StatusDisconnected && curr.ConnStatus == StatusDisconnected: + case received.ConnStatus == StatusDisconnected && curr.ConnStatus == StatusDisconnected: return curr.IP != "" default: return false diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index fda7b012f..ee98d503d 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -12,6 +12,8 @@ import ( "github.com/netbirdio/netbird/route" ) +const minRangeBits = 7 + type routerPeerStatus struct { connected bool relayed bool diff --git a/client/internal/routemanager/iptables_linux.go b/client/internal/routemanager/iptables_linux.go index 9f6019305..e9fbb7d3c 100644 --- a/client/internal/routemanager/iptables_linux.go +++ b/client/internal/routemanager/iptables_linux.go @@ -173,7 +173,7 @@ func (i *iptablesManager) addJumpRules() error { return err } if i.ipv4Client != nil { - rule := append(iptablesDefaultForwardingRule, ipv4Forwarding) + rule := append(iptablesDefaultForwardingRule, ipv4Forwarding) //nolint:gocritic err = i.ipv4Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...) if err != nil { @@ -181,7 +181,7 @@ func (i *iptablesManager) addJumpRules() error { } i.rules[ipv4][ipv4Forwarding] = rule - rule = append(iptablesDefaultNatRule, ipv4Nat) + rule = append(iptablesDefaultNatRule, ipv4Nat) //nolint:gocritic err = i.ipv4Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...) if err != nil { return err @@ -190,14 +190,14 @@ func (i *iptablesManager) addJumpRules() error { } if i.ipv6Client != nil { - rule := append(iptablesDefaultForwardingRule, ipv6Forwarding) + rule := append(iptablesDefaultForwardingRule, ipv6Forwarding) //nolint:gocritic err = i.ipv6Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...) if err != nil { return err } i.rules[ipv6][ipv6Forwarding] = rule - rule = append(iptablesDefaultNatRule, ipv6Nat) + rule = append(iptablesDefaultNatRule, ipv6Nat) //nolint:gocritic err = i.ipv6Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...) if err != nil { return err diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 1f812983c..479ac873f 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -155,7 +155,7 @@ func (m *DefaultManager) classifiesRoutes(newRoutes []*route.Route) (map[string] if !ownNetworkIDs[networkID] { // if prefix is too small, lets assume is a possible default route which is not yet supported // we skip this route management - if newRoute.Network.Bits() < 7 { + if newRoute.Network.Bits() < minRangeBits { log.Errorf("this agent version: %s, doesn't support default routes, received %s, skipping this route", version.NetbirdVersion(), newRoute.Network) continue diff --git a/client/internal/routemanager/nftables_linux.go b/client/internal/routemanager/nftables_linux.go index e62b1a404..3ecfa9630 100644 --- a/client/internal/routemanager/nftables_linux.go +++ b/client/internal/routemanager/nftables_linux.go @@ -300,7 +300,7 @@ func (n *nftablesManager) acceptForwardRule(sourceNetwork string) error { dst := generateCIDRMatcherExpressions("destination", "0.0.0.0/0") var exprs []expr.Any - exprs = append(src, append(dst, &expr.Verdict{ + exprs = append(src, append(dst, &expr.Verdict{ //nolint:gocritic Kind: expr.VerdictAccept, })...) @@ -322,7 +322,7 @@ func (n *nftablesManager) acceptForwardRule(sourceNetwork string) error { src = generateCIDRMatcherExpressions("source", "0.0.0.0/0") dst = generateCIDRMatcherExpressions("destination", sourceNetwork) - exprs = append(src, append(dst, &expr.Verdict{ + exprs = append(src, append(dst, &expr.Verdict{ //nolint:gocritic Kind: expr.VerdictAccept, })...) @@ -421,9 +421,9 @@ func (n *nftablesManager) insertRoutingRule(format, chain string, pair routerPai var expression []expr.Any if isNat { - expression = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) + expression = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) //nolint:gocritic } else { - expression = append(sourceExp, append(destExp, exprCounterAccept...)...) + expression = append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic } ruleKey := genKey(format, pair.ID) diff --git a/client/internal/routemanager/nftables_linux_test.go b/client/internal/routemanager/nftables_linux_test.go index dec800156..d60d53e50 100644 --- a/client/internal/routemanager/nftables_linux_test.go +++ b/client/internal/routemanager/nftables_linux_test.go @@ -44,7 +44,7 @@ func TestNftablesManager_RestoreOrCreateContainers(t *testing.T) { sourceExp := generateCIDRMatcherExpressions("source", pair.source) destExp := generateCIDRMatcherExpressions("destination", pair.destination) - forward4Exp := append(sourceExp, append(destExp, exprCounterAccept...)...) + forward4Exp := append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic forward4RuleKey := genKey(forwardingFormat, pair.ID) inserted4Forwarding := nftablesTestingClient.InsertRule(&nftables.Rule{ Table: manager.tableIPv4, @@ -53,7 +53,7 @@ func TestNftablesManager_RestoreOrCreateContainers(t *testing.T) { UserData: []byte(forward4RuleKey), }) - nat4Exp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) + nat4Exp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) //nolint:gocritic nat4RuleKey := genKey(natFormat, pair.ID) inserted4Nat := nftablesTestingClient.InsertRule(&nftables.Rule{ @@ -76,7 +76,7 @@ func TestNftablesManager_RestoreOrCreateContainers(t *testing.T) { sourceExp = generateCIDRMatcherExpressions("source", pair.source) destExp = generateCIDRMatcherExpressions("destination", pair.destination) - forward6Exp := append(sourceExp, append(destExp, exprCounterAccept...)...) + forward6Exp := append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic forward6RuleKey := genKey(forwardingFormat, pair.ID) inserted6Forwarding := nftablesTestingClient.InsertRule(&nftables.Rule{ Table: manager.tableIPv6, @@ -85,7 +85,7 @@ func TestNftablesManager_RestoreOrCreateContainers(t *testing.T) { UserData: []byte(forward6RuleKey), }) - nat6Exp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) + nat6Exp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) //nolint:gocritic nat6RuleKey := genKey(natFormat, pair.ID) inserted6Nat := nftablesTestingClient.InsertRule(&nftables.Rule{ @@ -149,7 +149,7 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) { sourceExp := generateCIDRMatcherExpressions("source", testCase.inputPair.source) destExp := generateCIDRMatcherExpressions("destination", testCase.inputPair.destination) - testingExpression := append(sourceExp, destExp...) + testingExpression := append(sourceExp, destExp...) //nolint:gocritic fwdRuleKey := genKey(forwardingFormat, testCase.inputPair.ID) found := 0 @@ -188,7 +188,7 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) { sourceExp = generateCIDRMatcherExpressions("source", getInPair(testCase.inputPair).source) destExp = generateCIDRMatcherExpressions("destination", getInPair(testCase.inputPair).destination) - testingExpression = append(sourceExp, destExp...) + testingExpression = append(sourceExp, destExp...) //nolint:gocritic inFwdRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID) found = 0 @@ -252,7 +252,7 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) { sourceExp := generateCIDRMatcherExpressions("source", testCase.inputPair.source) destExp := generateCIDRMatcherExpressions("destination", testCase.inputPair.destination) - forwardExp := append(sourceExp, append(destExp, exprCounterAccept...)...) + forwardExp := append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic forwardRuleKey := genKey(forwardingFormat, testCase.inputPair.ID) insertedForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{ Table: table, @@ -261,7 +261,7 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) { UserData: []byte(forwardRuleKey), }) - natExp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) + natExp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) //nolint:gocritic natRuleKey := genKey(natFormat, testCase.inputPair.ID) insertedNat := nftablesTestingClient.InsertRule(&nftables.Rule{ @@ -274,7 +274,7 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) { sourceExp = generateCIDRMatcherExpressions("source", getInPair(testCase.inputPair).source) destExp = generateCIDRMatcherExpressions("destination", getInPair(testCase.inputPair).destination) - forwardExp = append(sourceExp, append(destExp, exprCounterAccept...)...) + forwardExp = append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID) insertedInForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{ Table: table, @@ -283,7 +283,7 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) { UserData: []byte(inForwardRuleKey), }) - natExp = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) + natExp = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) //nolint:gocritic inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID) insertedInNat := nftablesTestingClient.InsertRule(&nftables.Rule{ diff --git a/client/internal/routemanager/systemops_bsd.go b/client/internal/routemanager/systemops_bsd.go index e777ec8ec..b2da8075c 100644 --- a/client/internal/routemanager/systemops_bsd.go +++ b/client/internal/routemanager/systemops_bsd.go @@ -27,24 +27,24 @@ const ( RTF_MULTICAST = 0x800000 ) -func existsInRouteTable(prefix netip.Prefix) (bool, error) { +func getRoutesFromTable() ([]netip.Prefix, error) { tab, err := route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0) if err != nil { - return false, err + return nil, err } msgs, err := route.ParseRIB(route.RIBTypeRoute, tab) if err != nil { - return false, err + return nil, err } - + var prefixList []netip.Prefix for _, msg := range msgs { m := msg.(*route.RouteMessage) if m.Version < 3 || m.Version > 5 { - return false, fmt.Errorf("unexpected RIB message version: %d", m.Version) + return nil, fmt.Errorf("unexpected RIB message version: %d", m.Version) } if m.Type != 4 /* RTM_GET */ { - return true, fmt.Errorf("unexpected RIB message type: %d", m.Type) + return nil, fmt.Errorf("unexpected RIB message type: %d", m.Type) } if m.Flags&RTF_UP == 0 || @@ -52,31 +52,42 @@ func existsInRouteTable(prefix netip.Prefix) (bool, error) { continue } - dst, err := toIPAddr(m.Addrs[0]) - if err != nil { - return true, fmt.Errorf("unexpected RIB destination: %v", err) + addr, ok := toNetIPAddr(m.Addrs[0]) + if !ok { + continue } - mask, _ := toIPAddr(m.Addrs[2]) - cidr, _ := net.IPMask(mask.To4()).Size() - if dst.String() == prefix.Addr().String() && cidr == prefix.Bits() { - return true, nil + mask, ok := toNetIPMASK(m.Addrs[2]) + if !ok { + continue + } + cidr, _ := mask.Size() + + routePrefix := netip.PrefixFrom(addr, cidr) + if routePrefix.IsValid() { + prefixList = append(prefixList, routePrefix) } } - - return false, nil + return prefixList, nil } -func toIPAddr(a route.Addr) (net.IP, error) { +func toNetIPAddr(a route.Addr) (netip.Addr, bool) { switch t := a.(type) { case *route.Inet4Addr: ip := net.IPv4(t.IP[0], t.IP[1], t.IP[2], t.IP[3]) - return ip, nil - case *route.Inet6Addr: - ip := make(net.IP, net.IPv6len) - copy(ip, t.IP[:]) - return ip, nil + addr := netip.MustParseAddr(ip.String()) + return addr, true default: - return net.IP{}, fmt.Errorf("unknown family: %v", t) + return netip.Addr{}, false + } +} + +func toNetIPMASK(a route.Addr) (net.IPMask, bool) { + switch t := a.(type) { + case *route.Inet4Addr: + mask := net.IPv4Mask(t.IP[0], t.IP[1], t.IP[2], t.IP[3]) + return mask, true + default: + return nil, false } } diff --git a/client/internal/routemanager/systemops_linux.go b/client/internal/routemanager/systemops_linux.go index fb2938d55..0562826a5 100644 --- a/client/internal/routemanager/systemops_linux.go +++ b/client/internal/routemanager/systemops_linux.go @@ -60,15 +60,26 @@ func addToRouteTable(prefix netip.Prefix, addr string) error { return nil } -func removeFromRouteTable(prefix netip.Prefix) error { +func removeFromRouteTable(prefix netip.Prefix, addr string) error { _, ipNet, err := net.ParseCIDR(prefix.String()) if err != nil { return err } + addrMask := "/32" + if prefix.Addr().Unmap().Is6() { + addrMask = "/128" + } + + ip, _, err := net.ParseCIDR(addr + addrMask) + if err != nil { + return err + } + route := &netlink.Route{ Scope: netlink.SCOPE_UNIVERSE, Dst: ipNet, + Gw: ip, } err = netlink.RouteDel(route) @@ -79,15 +90,16 @@ func removeFromRouteTable(prefix netip.Prefix) error { return nil } -func existsInRouteTable(prefix netip.Prefix) (bool, error) { +func getRoutesFromTable() ([]netip.Prefix, error) { tab, err := syscall.NetlinkRIB(syscall.RTM_GETROUTE, syscall.AF_UNSPEC) if err != nil { - return true, err + return nil, err } msgs, err := syscall.ParseNetlinkMessage(tab) if err != nil { - return true, err + return nil, err } + var prefixList []netip.Prefix loop: for _, m := range msgs { switch m.Header.Type { @@ -95,9 +107,10 @@ loop: break loop case syscall.RTM_NEWROUTE: rt := (*routeInfoInMemory)(unsafe.Pointer(&m.Data[0])) - attrs, err := syscall.ParseNetlinkRouteAttr(&m) + msg := m + attrs, err := syscall.ParseNetlinkRouteAttr(&msg) if err != nil { - return true, err + return nil, err } if rt.Family != syscall.AF_INET { continue loop @@ -105,17 +118,21 @@ loop: for _, attr := range attrs { if attr.Attr.Type == syscall.RTA_DST { - ip := net.IP(attr.Value) + addr, ok := netip.AddrFromSlice(attr.Value) + if !ok { + continue + } mask := net.CIDRMask(int(rt.DstLen), len(attr.Value)*8) cidr, _ := mask.Size() - if ip.String() == prefix.Addr().String() && cidr == prefix.Bits() { - return true, nil + routePrefix := netip.PrefixFrom(addr, cidr) + if routePrefix.IsValid() && routePrefix.Addr().Is4() { + prefixList = append(prefixList, routePrefix) } } } } } - return false, nil + return prefixList, nil } func enableIPForwarding() error { @@ -130,5 +147,5 @@ func enableIPForwarding() error { return nil } - return os.WriteFile(ipv4ForwardingPath, []byte("1"), 0644) + return os.WriteFile(ipv4ForwardingPath, []byte("1"), 0644) //nolint:gosec } diff --git a/client/internal/routemanager/systemops_nonandroid.go b/client/internal/routemanager/systemops_nonandroid.go index 11a4890c0..11247c7dc 100644 --- a/client/internal/routemanager/systemops_nonandroid.go +++ b/client/internal/routemanager/systemops_nonandroid.go @@ -14,17 +14,6 @@ import ( var errRouteNotFound = fmt.Errorf("route not found") func addToRouteTableIfNoExists(prefix netip.Prefix, addr string) error { - defaultGateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0")) - if err != nil && err != errRouteNotFound { - return err - } - - gatewayIP := netip.MustParseAddr(defaultGateway.String()) - if prefix.Contains(gatewayIP) { - log.Warnf("skipping adding a new route for network %s because it overlaps with the default gateway: %s", prefix, gatewayIP) - return nil - } - ok, err := existsInRouteTable(prefix) if err != nil { return err @@ -34,20 +23,82 @@ func addToRouteTableIfNoExists(prefix netip.Prefix, addr string) error { return nil } - return addToRouteTable(prefix, addr) -} - -func removeFromRouteTableIfNonSystem(prefix netip.Prefix, addr string) error { - addrIP := net.ParseIP(addr) - prefixGateway, err := getExistingRIBRouteGateway(prefix) + ok, err = isSubRange(prefix) if err != nil { return err } - if prefixGateway != nil && !prefixGateway.Equal(addrIP) { - log.Warnf("route for network %s is pointing to a different gateway: %s, should be pointing to: %s, not removing", prefix, prefixGateway, addrIP) + + if ok { + err := addRouteForCurrentDefaultGateway(prefix) + if err != nil { + log.Warnf("unable to add route for current default gateway route. Will proceed without it. error: %s", err) + } + } + + return addToRouteTable(prefix, addr) +} + +func addRouteForCurrentDefaultGateway(prefix netip.Prefix) error { + defaultGateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0")) + if err != nil && err != errRouteNotFound { + return err + } + + addr := netip.MustParseAddr(defaultGateway.String()) + + if !prefix.Contains(addr) { + log.Debugf("skipping adding a new route for gateway %s because it is not in the network %s", addr, prefix) return nil } - return removeFromRouteTable(prefix) + + gatewayPrefix := netip.PrefixFrom(addr, 32) + + ok, err := existsInRouteTable(gatewayPrefix) + if err != nil { + return fmt.Errorf("unable to check if there is an existing route for gateway %s. error: %s", gatewayPrefix, err) + } + + if ok { + log.Debugf("skipping adding a new route for gateway %s because it already exists", gatewayPrefix) + return nil + } + + gatewayHop, err := getExistingRIBRouteGateway(gatewayPrefix) + if err != nil && err != errRouteNotFound { + return fmt.Errorf("unable to get the next hop for the default gateway address. error: %s", err) + } + log.Debugf("adding a new route for gateway %s with next hop %s", gatewayPrefix, gatewayHop) + return addToRouteTable(gatewayPrefix, gatewayHop.String()) +} + +func existsInRouteTable(prefix netip.Prefix) (bool, error) { + routes, err := getRoutesFromTable() + if err != nil { + return false, err + } + for _, tableRoute := range routes { + if tableRoute == prefix { + return true, nil + } + } + return false, nil +} + +func isSubRange(prefix netip.Prefix) (bool, error) { + routes, err := getRoutesFromTable() + if err != nil { + return false, err + } + for _, tableRoute := range routes { + if tableRoute.Bits() > minRangeBits && tableRoute.Contains(prefix.Addr()) && tableRoute.Bits() < prefix.Bits() { + return true, nil + } + } + return false, nil +} + +func removeFromRouteTableIfNonSystem(prefix netip.Prefix, addr string) error { + return removeFromRouteTable(prefix, addr) } func getExistingRIBRouteGateway(prefix netip.Prefix) (net.IP, error) { diff --git a/client/internal/routemanager/systemops_nonandroid_test.go b/client/internal/routemanager/systemops_nonandroid_test.go index 68c0e1d26..3646dc3da 100644 --- a/client/internal/routemanager/systemops_nonandroid_test.go +++ b/client/internal/routemanager/systemops_nonandroid_test.go @@ -24,13 +24,13 @@ func TestAddRemoveRoutes(t *testing.T) { shouldBeRemoved bool }{ { - name: "Should Add And Remove Route", + name: "Should Add And Remove Route 100.66.120.0/24", prefix: netip.MustParsePrefix("100.66.120.0/24"), shouldRouteToWireguard: true, shouldBeRemoved: true, }, { - name: "Should Not Add Or Remove Route", + name: "Should Not Add Or Remove Route 127.0.0.1/32", prefix: netip.MustParsePrefix("127.0.0.1/32"), shouldRouteToWireguard: false, shouldBeRemoved: false, @@ -51,29 +51,32 @@ func TestAddRemoveRoutes(t *testing.T) { require.NoError(t, err, "should create testing wireguard interface") err = addToRouteTableIfNoExists(testCase.prefix, wgInterface.Address().IP.String()) - require.NoError(t, err, "should not return err") + require.NoError(t, err, "addToRouteTableIfNoExists should not return err") prefixGateway, err := getExistingRIBRouteGateway(testCase.prefix) - require.NoError(t, err, "should not return err") + require.NoError(t, err, "getExistingRIBRouteGateway should not return err") if testCase.shouldRouteToWireguard { require.Equal(t, wgInterface.Address().IP.String(), prefixGateway.String(), "route should point to wireguard interface IP") } else { require.NotEqual(t, wgInterface.Address().IP.String(), prefixGateway.String(), "route should point to a different interface") } + exists, err := existsInRouteTable(testCase.prefix) + require.NoError(t, err, "existsInRouteTable should not return err") + if exists && testCase.shouldRouteToWireguard { + err = removeFromRouteTableIfNonSystem(testCase.prefix, wgInterface.Address().IP.String()) + require.NoError(t, err, "removeFromRouteTableIfNonSystem should not return err") - err = removeFromRouteTableIfNonSystem(testCase.prefix, wgInterface.Address().IP.String()) - require.NoError(t, err, "should not return err") + prefixGateway, err = getExistingRIBRouteGateway(testCase.prefix) + require.NoError(t, err, "getExistingRIBRouteGateway should not return err") - prefixGateway, err = getExistingRIBRouteGateway(testCase.prefix) - require.NoError(t, err, "should not return err") + internetGateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0")) + require.NoError(t, err) - internetGateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0")) - require.NoError(t, err) - - if testCase.shouldBeRemoved { - require.Equal(t, internetGateway, prefixGateway, "route should be pointing to default internet gateway") - } else { - require.NotEqual(t, internetGateway, prefixGateway, "route should be pointing to a different gateway than the internet gateway") + if testCase.shouldBeRemoved { + require.Equal(t, internetGateway, prefixGateway, "route should be pointing to default internet gateway") + } else { + require.NotEqual(t, internetGateway, prefixGateway, "route should be pointing to a different gateway than the internet gateway") + } } }) } @@ -123,7 +126,7 @@ func TestGetExistingRIBRouteGateway(t *testing.T) { func TestAddExistAndRemoveRouteNonAndroid(t *testing.T) { defaultGateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0")) - fmt.Println("defaultGateway: ", defaultGateway) + t.Log("defaultGateway: ", defaultGateway) if err != nil { t.Fatal("shouldn't return error when fetching the gateway: ", err) } @@ -207,7 +210,7 @@ func TestAddExistAndRemoveRouteNonAndroid(t *testing.T) { // route should either not have been added or should have been removed // In case of already existing route, it should not have been added (but still exist) ok, err := existsInRouteTable(testCase.prefix) - fmt.Println("Buffer string: ", buf.String()) + t.Log("Buffer string: ", buf.String()) require.NoError(t, err, "should not return err") if !strings.Contains(buf.String(), "because it already exists") { require.False(t, ok, "route should not exist") @@ -215,3 +218,66 @@ func TestAddExistAndRemoveRouteNonAndroid(t *testing.T) { }) } } + +func TestExistsInRouteTable(t *testing.T) { + addresses, err := net.InterfaceAddrs() + if err != nil { + t.Fatal("shouldn't return error when fetching interface addresses: ", err) + } + + var addressPrefixes []netip.Prefix + for _, address := range addresses { + p := netip.MustParsePrefix(address.String()) + if p.Addr().Is4() { + addressPrefixes = append(addressPrefixes, p.Masked()) + } + } + + for _, prefix := range addressPrefixes { + exists, err := existsInRouteTable(prefix) + if err != nil { + t.Fatal("shouldn't return error when checking if address exists in route table: ", err) + } + if !exists { + t.Fatalf("address %s should exist in route table", prefix) + } + } +} + +func TestIsSubRange(t *testing.T) { + addresses, err := net.InterfaceAddrs() + if err != nil { + t.Fatal("shouldn't return error when fetching interface addresses: ", err) + } + + var subRangeAddressPrefixes []netip.Prefix + var nonSubRangeAddressPrefixes []netip.Prefix + for _, address := range addresses { + p := netip.MustParsePrefix(address.String()) + if !p.Addr().IsLoopback() && p.Addr().Is4() && p.Bits() < 32 { + p2 := netip.PrefixFrom(p.Masked().Addr(), p.Bits()+1) + subRangeAddressPrefixes = append(subRangeAddressPrefixes, p2) + nonSubRangeAddressPrefixes = append(nonSubRangeAddressPrefixes, p.Masked()) + } + } + + for _, prefix := range subRangeAddressPrefixes { + isSubRangePrefix, err := isSubRange(prefix) + if err != nil { + t.Fatal("shouldn't return error when checking if address is sub-range: ", err) + } + if !isSubRangePrefix { + t.Fatalf("address %s should be sub-range of an existing route in the table", prefix) + } + } + + for _, prefix := range nonSubRangeAddressPrefixes { + isSubRangePrefix, err := isSubRange(prefix) + if err != nil { + t.Fatal("shouldn't return error when checking if address is sub-range: ", err) + } + if isSubRangePrefix { + t.Fatalf("address %s should not be sub-range of an existing route in the table", prefix) + } + } +} diff --git a/client/internal/routemanager/systemops_nonlinux.go b/client/internal/routemanager/systemops_nonlinux.go index 537042099..47bd60eb0 100644 --- a/client/internal/routemanager/systemops_nonlinux.go +++ b/client/internal/routemanager/systemops_nonlinux.go @@ -21,8 +21,12 @@ func addToRouteTable(prefix netip.Prefix, addr string) error { return nil } -func removeFromRouteTable(prefix netip.Prefix) error { - cmd := exec.Command("route", "delete", prefix.String()) +func removeFromRouteTable(prefix netip.Prefix, addr string) error { + args := []string{"delete", prefix.String()} + if runtime.GOOS == "darwin" { + args = append(args, addr) + } + cmd := exec.Command("route", args...) out, err := cmd.Output() if err != nil { return err diff --git a/client/internal/routemanager/systemops_windows.go b/client/internal/routemanager/systemops_windows.go index 2233748bf..309c184b9 100644 --- a/client/internal/routemanager/systemops_windows.go +++ b/client/internal/routemanager/systemops_windows.go @@ -15,23 +15,32 @@ type Win32_IP4RouteTable struct { Mask string } -func existsInRouteTable(prefix netip.Prefix) (bool, error) { +func getRoutesFromTable() ([]netip.Prefix, error) { var routes []Win32_IP4RouteTable query := "SELECT Destination, Mask FROM Win32_IP4RouteTable" err := wmi.Query(query, &routes) if err != nil { - return true, err + return nil, err } + var prefixList []netip.Prefix for _, route := range routes { - ip := net.ParseIP(route.Mask) - ip = ip.To4() - mask := net.IPv4Mask(ip[0], ip[1], ip[2], ip[3]) + addr, err := netip.ParseAddr(route.Destination) + if err != nil { + continue + } + maskSlice := net.ParseIP(route.Mask).To4() + if maskSlice == nil { + continue + } + mask := net.IPv4Mask(maskSlice[0], maskSlice[1], maskSlice[2], maskSlice[3]) cidr, _ := mask.Size() - if route.Destination == prefix.Addr().String() && cidr == prefix.Bits() { - return true, nil + + routePrefix := netip.PrefixFrom(addr, cidr) + if routePrefix.IsValid() && routePrefix.Addr().Is4() { + prefixList = append(prefixList, routePrefix) } } - return false, nil + return prefixList, nil } diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 4dc989420..03eb3c49b 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -43,6 +43,7 @@ type LoginRequest struct { CleanNATExternalIPs bool `protobuf:"varint,6,opt,name=cleanNATExternalIPs,proto3" json:"cleanNATExternalIPs,omitempty"` CustomDNSAddress []byte `protobuf:"bytes,7,opt,name=customDNSAddress,proto3" json:"customDNSAddress,omitempty"` IsLinuxDesktopClient bool `protobuf:"varint,8,opt,name=isLinuxDesktopClient,proto3" json:"isLinuxDesktopClient,omitempty"` + Hostname string `protobuf:"bytes,9,opt,name=hostname,proto3" json:"hostname,omitempty"` } func (x *LoginRequest) Reset() { @@ -133,6 +134,13 @@ func (x *LoginRequest) GetIsLinuxDesktopClient() bool { return false } +func (x *LoginRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -210,6 +218,7 @@ type WaitSSOLoginRequest struct { unknownFields protoimpl.UnknownFields UserCode string `protobuf:"bytes,1,opt,name=userCode,proto3" json:"userCode,omitempty"` + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` } func (x *WaitSSOLoginRequest) Reset() { @@ -251,6 +260,13 @@ func (x *WaitSSOLoginRequest) GetUserCode() string { return "" } +func (x *WaitSSOLoginRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + type WaitSSOLoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1051,7 +1067,7 @@ var file_daemon_proto_rawDesc = []byte{ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xca, 0x02, 0x0a, 0x0c, 0x4c, 0x6f, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe6, 0x02, 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, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, @@ -1072,128 +1088,132 @@ var file_daemon_proto_rawDesc = []byte{ 0x73, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x69, 0x73, 0x4c, 0x69, 0x6e, 0x75, 0x78, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x69, 0x73, 0x4c, 0x69, 0x6e, 0x75, 0x78, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, - 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, - 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, - 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x31, - 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, - 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, - 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32, - 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb3, 0x01, 0x0a, 0x11, - 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, - 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, - 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, - 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, - 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, - 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, - 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, - 0x4c, 0x22, 0xcf, 0x02, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 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, 0x10, 0x63, - 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, - 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, - 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, - 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, + 0x6d, 0x65, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, + 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, + 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, + 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, + 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, + 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, + 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, + 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, + 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, + 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, + 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, + 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0xb3, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, + 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, + 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, + 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, 0xcf, 0x02, 0x0a, 0x09, 0x50, 0x65, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, + 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x18, 0x04, 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, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, + 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, + 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, + 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, - 0x71, 0x64, 0x6e, 0x22, 0x76, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, - 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x3d, 0x0a, 0x0b, 0x53, - 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, - 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x41, 0x0a, 0x0f, 0x4d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, - 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, - 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0xef, 0x01, - 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, - 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, - 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, - 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, - 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x32, - 0xf7, 0x02, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, - 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, - 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x76, 0x0a, 0x0e, 0x4c, + 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, + 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, + 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, + 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, + 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, + 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, + 0x71, 0x64, 0x6e, 0x22, 0x3d, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x22, 0x41, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0xef, 0x01, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, + 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, + 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x32, 0xf7, 0x02, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, + 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, + 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, + 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, + 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, + 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, + 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 8bed1ec9d..c2983c943 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -52,6 +52,8 @@ message LoginRequest { bytes customDNSAddress = 7; bool isLinuxDesktopClient = 8; + + string hostname = 9; } message LoginResponse { @@ -63,6 +65,7 @@ message LoginResponse { message WaitSSOLoginRequest { string userCode = 1; + string hostname = 2; } message WaitSSOLoginResponse {} diff --git a/client/server/server.go b/client/server/server.go index faac22273..b9c7b0a5e 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -7,6 +7,7 @@ import ( "time" "github.com/netbirdio/netbird/client/internal/auth" + "github.com/netbirdio/netbird/client/system" log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" @@ -181,6 +182,11 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro s.latestConfigInput.CustomDNSAddress = []byte{} } + if msg.Hostname != "" { + // nolint + ctx = context.WithValue(ctx, system.DeviceNameCtxKey, msg.Hostname) + } + s.mutex.Unlock() inputConfig.PreSharedKey = &msg.PreSharedKey @@ -275,6 +281,11 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin ctx = metadata.NewOutgoingContext(ctx, md) } + if msg.Hostname != "" { + // nolint + ctx = context.WithValue(ctx, system.DeviceNameCtxKey, msg.Hostname) + } + s.actCancel = cancel s.mutex.Unlock() diff --git a/client/ssh/client.go b/client/ssh/client.go index 29ebb2481..2dc70e8fc 100644 --- a/client/ssh/client.go +++ b/client/ssh/client.go @@ -2,11 +2,12 @@ package ssh import ( "fmt" - "golang.org/x/crypto/ssh" - "golang.org/x/term" "net" "os" "time" + + "golang.org/x/crypto/ssh" + "golang.org/x/term" ) // Client wraps crypto/ssh Client to simplify usage @@ -73,8 +74,7 @@ func (c *Client) OpenTerminal() error { if err := session.Wait(); err != nil { if e, ok := err.(*ssh.ExitError); ok { - switch e.ExitStatus() { - case 130: + if e.ExitStatus() == 130 { return nil } } diff --git a/client/ssh/util.go b/client/ssh/util.go index 8ecb77c93..cf5f1396e 100644 --- a/client/ssh/util.go +++ b/client/ssh/util.go @@ -9,9 +9,10 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "strings" + "golang.org/x/crypto/ed25519" gossh "golang.org/x/crypto/ssh" - "strings" ) // KeyType is a type of SSH key @@ -42,7 +43,7 @@ func GeneratePrivateKey(keyType KeyType) ([]byte, error) { case RSA: key, err = rsa.GenerateKey(rand.Reader, RSAKeySize) default: - return nil, fmt.Errorf("unsupported ket type %s", keyType) + return nil, fmt.Errorf("unsupported key type %s", keyType) } if err != nil { return nil, err diff --git a/client/system/info_darwin.go b/client/system/info_darwin.go index 71fe03f09..5ae2b4fc6 100644 --- a/client/system/info_darwin.go +++ b/client/system/info_darwin.go @@ -6,7 +6,6 @@ package system import ( "bytes" "context" - "fmt" "os" "os/exec" "runtime" @@ -24,7 +23,7 @@ func GetInfo(ctx context.Context) *Info { utsname := unix.Utsname{} err := unix.Uname(&utsname) if err != nil { - fmt.Println("getInfo:", err) + log.Warnf("getInfo: %s", err) } sysName := string(bytes.Split(utsname.Sysname[:], []byte{0})[0]) machine := string(bytes.Split(utsname.Machine[:], []byte{0})[0]) diff --git a/client/system/info_linux.go b/client/system/info_linux.go index 7d43fc035..21a4d482a 100644 --- a/client/system/info_linux.go +++ b/client/system/info_linux.go @@ -6,13 +6,14 @@ package system import ( "bytes" "context" - "fmt" "os" "os/exec" "runtime" "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/version" ) @@ -43,8 +44,8 @@ func GetInfo(ctx context.Context) *Info { } } - osStr := strings.Replace(info, "\n", "", -1) - osStr = strings.Replace(osStr, "\r\n", "", -1) + osStr := strings.ReplaceAll(info, "\n", "") + osStr = strings.ReplaceAll(osStr, "\r\n", "") osInfo := strings.Split(osStr, " ") if osName == "" { osName = osInfo[3] @@ -67,7 +68,7 @@ func _getInfo() string { cmd.Stderr = &stderr err := cmd.Run() if err != nil { - fmt.Println("getInfo:", err) + log.Warnf("getInfo: %s", err) } return out.String() } @@ -81,7 +82,7 @@ func _getReleaseInfo() string { cmd.Stderr = &stderr err := cmd.Run() if err != nil { - fmt.Println("getReleaseInfo:", err) + log.Warnf("geucwReleaseInfo: %s", err) } return out.String() } diff --git a/client/system/info_windows.go b/client/system/info_windows.go index c8c3276c9..69b4ad008 100644 --- a/client/system/info_windows.go +++ b/client/system/info_windows.go @@ -5,17 +5,24 @@ import ( "fmt" "os" "runtime" + "strings" log "github.com/sirupsen/logrus" + "github.com/yusufpapurcu/wmi" "golang.org/x/sys/windows/registry" "github.com/netbirdio/netbird/version" ) +type Win32_OperatingSystem struct { + Caption string +} + // GetInfo retrieves and parses the system information func GetInfo(ctx context.Context) *Info { - ver := getOSVersion() - gio := &Info{Kernel: "windows", OSVersion: ver, Core: ver, Platform: "unknown", OS: "windows", GoOS: runtime.GOOS, CPUs: runtime.NumCPU()} + osName, osVersion := getOSNameAndVersion() + buildVersion := getBuildVersion() + gio := &Info{Kernel: "windows", OSVersion: osVersion, Core: buildVersion, Platform: "unknown", OS: osName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU()} systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) gio.WiretrusteeVersion = version.NetbirdVersion() @@ -24,7 +31,35 @@ func GetInfo(ctx context.Context) *Info { return gio } -func getOSVersion() string { +func getOSNameAndVersion() (string, string) { + var dst []Win32_OperatingSystem + query := wmi.CreateQuery(&dst, "") + err := wmi.Query(query, &dst) + if err != nil { + log.Fatal(err) + } + + if len(dst) == 0 { + return "Windows", getBuildVersion() + } + + split := strings.Split(dst[0].Caption, " ") + + if len(split) < 3 { + return "Windows", getBuildVersion() + } + + name := split[1] + version := split[2] + if split[2] == "Server" { + name = fmt.Sprintf("%s %s", split[1], split[2]) + version = split[3] + } + + return name, version +} + +func getBuildVersion() string { k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) if err != nil { log.Error(err) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index e66d03d95..1dc0bb374 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -69,7 +69,7 @@ func main() { a.Run() } else { if err := checkPIDFile(); err != nil { - fmt.Println(err) + log.Errorf("check PID file: %v", err) return } systray.Run(client.onTrayReady, client.onTrayExit) @@ -634,5 +634,5 @@ func checkPIDFile() error { } } - return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o664) + return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o664) //nolint:gosec } diff --git a/dns/dns.go b/dns/dns.go index 16ebd1d96..18528c743 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -4,11 +4,12 @@ package dns import ( "fmt" - "github.com/miekg/dns" - "golang.org/x/net/idna" "net" "regexp" "strings" + + "github.com/miekg/dns" + "golang.org/x/net/idna" ) const ( @@ -85,6 +86,8 @@ func (s SimpleRecord) Len() uint16 { } } +var invalidHostMatcher = regexp.MustCompile(invalidHostLabel) + // GetParsedDomainLabel returns a domain label with max 59 characters, // parsed for old Hosts.txt requirements, and converted to ASCII and lowercase func GetParsedDomainLabel(name string) (string, error) { @@ -95,11 +98,9 @@ func GetParsedDomainLabel(name string) (string, error) { rawLabel := labels[0] ascii, err := idna.Punycode.ToASCII(rawLabel) if err != nil { - return "", fmt.Errorf("unable to convert host lavel to ASCII, error: %v", err) + return "", fmt.Errorf("unable to convert host label to ASCII, error: %v", err) } - invalidHostMatcher := regexp.MustCompile(invalidHostLabel) - validHost := strings.ToLower(invalidHostMatcher.ReplaceAllString(ascii, "-")) if len(validHost) > 58 { validHost = validHost[:59] diff --git a/go.mod b/go.mod index c6c8221e1..018d7ee01 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,8 @@ require ( github.com/miekg/dns v1.1.43 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20231027143200-a966bce7db88 + github.com/netbirdio/management-integrations/additions v0.0.0-20231205113053-c462587ae695 + github.com/netbirdio/management-integrations/integrations v0.0.0-20231205113053-c462587ae695 github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pion/logging v0.2.2 diff --git a/go.sum b/go.sum index 84b8816e9..9ad425dfd 100644 --- a/go.sum +++ b/go.sum @@ -495,8 +495,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc= github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ= -github.com/netbirdio/management-integrations/integrations v0.0.0-20231027143200-a966bce7db88 h1:zhe8qseauBuYOS910jpl5sv8Tb+36zxQPXrwYXqll0g= -github.com/netbirdio/management-integrations/integrations v0.0.0-20231027143200-a966bce7db88/go.mod h1:KSqjzHcqlodTWiuap5lRXxt5KT3vtYRoksL0KIrTK40= +github.com/netbirdio/management-integrations/additions v0.0.0-20231205113053-c462587ae695 h1:c/Rvyy/mqbFoKo6FS8ihQ3/3y+TAl0qDEH0pO2tXayM= +github.com/netbirdio/management-integrations/additions v0.0.0-20231205113053-c462587ae695/go.mod h1:31FhBNvQ+riHEIu6LSTmqr8IeuSIsGfQffqV4LFmbwA= +github.com/netbirdio/management-integrations/integrations v0.0.0-20231205113053-c462587ae695 h1:9HRnqSosRuKyOZgVN/hJW3DG2zVyt5AARmiQlSuDPIc= +github.com/netbirdio/management-integrations/integrations v0.0.0-20231205113053-c462587ae695/go.mod h1:B0nMS3es77gOvPYhc0K91fAzTkQLi/jRq5TffUN3klM= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0 h1:hirFRfx3grVA/9eEyjME5/z3nxdJlN9kfQpvWWPk32g= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949 h1:xbWM9BU6mwZZLHxEjxIX/V8Hv3HurQt4mReIE4mY4DM= diff --git a/iface/module_linux_test.go b/iface/module_linux_test.go index d61245549..97e9b1f78 100644 --- a/iface/module_linux_test.go +++ b/iface/module_linux_test.go @@ -132,6 +132,7 @@ func resetGlobals() { } func createFiles(t *testing.T) (string, []module) { + t.Helper() writeFile := func(path, text string) { if err := os.WriteFile(path, []byte(text), 0644); err != nil { t.Fatal(err) @@ -167,6 +168,7 @@ func createFiles(t *testing.T) (string, []module) { } func getRandomLoadedModule(t *testing.T) (string, error) { + t.Helper() f, err := os.Open("/proc/modules") if err != nil { return "", err diff --git a/iface/tun_adapter.go b/iface/tun_adapter.go index da0b1695b..c10eb3d19 100644 --- a/iface/tun_adapter.go +++ b/iface/tun_adapter.go @@ -1,6 +1,6 @@ package iface -// TunAdapter is an interface for create tun device from externel service +// TunAdapter is an interface for create tun device from external service type TunAdapter interface { ConfigureInterface(address string, mtu int, dns string, searchDomains string, routes string) (int, error) UpdateAddr(address string) error diff --git a/iface/tun_linux.go b/iface/tun_linux.go index 93c03436e..1a3537394 100644 --- a/iface/tun_linux.go +++ b/iface/tun_linux.go @@ -99,7 +99,8 @@ func (c *tunDevice) assignAddr() error { } if len(list) > 0 { for _, a := range list { - err = netlink.AddrDel(link, &a) + addr := a + err = netlink.AddrDel(link, &addr) if err != nil { return err } diff --git a/iface/wg_configurer_nonandroid.go b/iface/wg_configurer_nonandroid.go index 239dd94fe..c09dda9ad 100644 --- a/iface/wg_configurer_nonandroid.go +++ b/iface/wg_configurer_nonandroid.go @@ -141,7 +141,7 @@ func (c *wGConfigurer) removeAllowedIP(peerKey string, allowedIP string) error { for i, existingAllowedIP := range existingPeer.AllowedIPs { if existingAllowedIP.String() == ipNet.String() { - newAllowedIPs = append(existingPeer.AllowedIPs[:i], existingPeer.AllowedIPs[i+1:]...) + newAllowedIPs = append(existingPeer.AllowedIPs[:i], existingPeer.AllowedIPs[i+1:]...) //nolint:gocritic break } } diff --git a/infrastructure_files/configure.sh b/infrastructure_files/configure.sh index 6d2902816..1f70584f3 100755 --- a/infrastructure_files/configure.sh +++ b/infrastructure_files/configure.sh @@ -36,7 +36,7 @@ fi if [[ "x-$NETBIRD_DOMAIN" == "x-" ]]; then echo NETBIRD_DOMAIN is not set, please update your setup.env file - echo If you are migrating from old versions, you migh need to update your variables prefixes from + echo If you are migrating from old versions, you might need to update your variables prefixes from echo WIRETRUSTEE_.. TO NETBIRD_ exit 1 fi diff --git a/infrastructure_files/getting-started-with-zitadel.sh b/infrastructure_files/getting-started-with-zitadel.sh index d00c2719c..67d16e1e2 100644 --- a/infrastructure_files/getting-started-with-zitadel.sh +++ b/infrastructure_files/getting-started-with-zitadel.sh @@ -387,7 +387,7 @@ check_nb_domain() { if [ "$DOMAIN" == "netbird.example.com" ]; then echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr - retrun 1 + return 1 fi return 0 } diff --git a/infrastructure_files/turnserver.conf.tmpl b/infrastructure_files/turnserver.conf.tmpl index 9b31cb511..1dc38e62d 100644 --- a/infrastructure_files/turnserver.conf.tmpl +++ b/infrastructure_files/turnserver.conf.tmpl @@ -677,7 +677,7 @@ no-cli # #cli-password=$5$79a316b350311570$81df9cfb9af7f5e5a76eada31e7097b663a0670f99a3c07ded3f1c8e59c5658a # -# Or unsecure form for the same password: +# Or insecure form for the same password: # # cli-password=CHANGE_ME diff --git a/management/client/client_test.go b/management/client/client_test.go index 889b7a131..9ebb58420 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -31,6 +31,7 @@ import ( const ValidKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB" func startManagement(t *testing.T) (*grpc.Server, net.Listener) { + t.Helper() level, _ := log.ParseLevel("debug") log.SetLevel(level) @@ -57,7 +58,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { t.Fatal(err) } - peersUpdateManager := mgmt.NewPeersUpdateManager() + peersUpdateManager := mgmt.NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", eventStore, false) @@ -81,6 +82,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { } func startMockManagement(t *testing.T) (*grpc.Server, net.Listener, *mock_server.ManagementServiceServerMock, wgtypes.Key) { + t.Helper() lis, err := net.Listen("tcp", ":0") if err != nil { t.Fatal(err) @@ -168,7 +170,7 @@ func TestClient_LoginUnregistered_ShouldThrow_401(t *testing.T) { t.Error("expecting err on unregistered login, got nil") } if s, ok := status.FromError(err); !ok || s.Code() != codes.PermissionDenied { - t.Errorf("expecting err code %d denied on on unregistered login got %d", codes.PermissionDenied, s.Code()) + t.Errorf("expecting err code %d denied on unregistered login got %d", codes.PermissionDenied, s.Code()) } } @@ -283,7 +285,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) { testKey, err := wgtypes.GenerateKey() if err != nil { - log.Fatal(err) + t.Fatal(err) } serverAddr := lis.Addr().String() @@ -291,12 +293,12 @@ func Test_SystemMetaDataFromClient(t *testing.T) { testClient, err := NewClient(ctx, serverAddr, testKey, false) if err != nil { - log.Fatalf("error while creating testClient: %v", err) + t.Fatalf("error while creating testClient: %v", err) } key, err := testClient.GetServerPublicKey() if err != nil { - log.Fatalf("error while getting server public key from testclient, %v", err) + t.Fatalf("error while getting server public key from testclient, %v", err) } var actualMeta *mgmtProto.PeerSystemMeta @@ -362,7 +364,7 @@ func Test_GetDeviceAuthorizationFlow(t *testing.T) { testKey, err := wgtypes.GenerateKey() if err != nil { - log.Fatal(err) + t.Fatal(err) } serverAddr := lis.Addr().String() @@ -370,7 +372,7 @@ func Test_GetDeviceAuthorizationFlow(t *testing.T) { client, err := NewClient(ctx, serverAddr, testKey, false) if err != nil { - log.Fatalf("error while creating testClient: %v", err) + t.Fatalf("error while creating testClient: %v", err) } expectedFlowInfo := &mgmtProto.DeviceAuthorizationFlow{ @@ -406,7 +408,7 @@ func Test_GetPKCEAuthorizationFlow(t *testing.T) { testKey, err := wgtypes.GenerateKey() if err != nil { - log.Fatal(err) + t.Fatal(err) } serverAddr := lis.Addr().String() @@ -414,7 +416,7 @@ func Test_GetPKCEAuthorizationFlow(t *testing.T) { client, err := NewClient(ctx, serverAddr, testKey, false) if err != nil { - log.Fatalf("error while creating testClient: %v", err) + t.Fatalf("error while creating testClient: %v", err) } expectedFlowInfo := &mgmtProto.PKCEAuthorizationFlow{ diff --git a/management/cmd/management.go b/management/cmd/management.go index 1a00a0f57..f05de4e4e 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -130,7 +130,7 @@ var ( if err != nil { return fmt.Errorf("failed creating Store: %s: %v", config.Datadir, err) } - peersUpdateManager := server.NewPeersUpdateManager() + peersUpdateManager := server.NewPeersUpdateManager(appMetrics) var idpManager idp.Manager if config.IdpManagerConfig != nil { diff --git a/management/cmd/root.go b/management/cmd/root.go index de8b5b8b3..1c9b95bfd 100644 --- a/management/cmd/root.go +++ b/management/cmd/root.go @@ -67,7 +67,7 @@ func init() { rootCmd.MarkFlagRequired("config") //nolint rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "") - rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the the log will be output to stdout") + rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout") rootCmd.AddCommand(mgmtCmd) migrationCmd.PersistentFlags().StringVar(&mgmtDataDir, "datadir", defaultMgmtDataDir, "server data directory location") diff --git a/management/server/account.go b/management/server/account.go index 30a9bd200..4c13c8535 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -17,15 +17,18 @@ import ( "github.com/eko/gocache/v3/cache" cacheStore "github.com/eko/gocache/v3/store" + "github.com/netbirdio/management-integrations/additions" gocache "github.com/patrickmn/go-cache" "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/base62" nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" "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/route" ) @@ -42,6 +45,8 @@ const ( DefaultPeerLoginExpiration = 24 * time.Hour ) +type ExternalCacheManager cache.CacheInterface[*idp.UserData] + func cacheEntryExpiration() time.Duration { r := rand.Intn(int(CacheExpirationMax.Milliseconds()-CacheExpirationMin.Milliseconds())) + int(CacheExpirationMin.Milliseconds()) return time.Duration(r) * time.Millisecond @@ -57,19 +62,22 @@ type AccountManager interface { InviteUser(accountID string, initiatorUserID string, targetUserID string) error ListSetupKeys(accountID, userID string) ([]*SetupKey, error) SaveUser(accountID, initiatorUserID string, update *User) (*UserInfo, error) + SaveOrAddUser(accountID, initiatorUserID string, update *User, addIfNotExists bool) (*UserInfo, error) GetSetupKey(accountID, userID, keyID string) (*SetupKey, error) GetAccountByUserOrAccountID(userID, accountID, domain string) (*Account, error) GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*Account, *User, error) GetAccountFromPAT(pat string) (*Account, *User, *PersonalAccessToken, error) + DeleteAccount(accountID, userID string) error MarkPATUsed(tokenID string) error GetUser(claims jwtclaims.AuthorizationClaims) (*User, error) - GetPeers(accountID, userID string) ([]*Peer, error) + ListUsers(accountID string) ([]*User, error) + GetPeers(accountID, userID string) ([]*nbpeer.Peer, error) MarkPeerConnected(peerKey string, connected bool) error DeletePeer(accountID, peerID, userID string) error - UpdatePeer(accountID, userID string, peer *Peer) (*Peer, error) + UpdatePeer(accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) GetNetworkMap(peerID string) (*NetworkMap, error) GetPeerNetwork(peerID string) (*Network, error) - AddPeer(setupKey, userID string, peer *Peer) (*Peer, *NetworkMap, error) + AddPeer(setupKey, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, *NetworkMap, error) CreatePAT(accountID string, initiatorUserID string, targetUserID string, tokenName string, expiresIn int) (*PersonalAccessTokenGenerated, error) DeletePAT(accountID string, initiatorUserID string, targetUserID string, tokenID string) error GetPAT(accountID string, initiatorUserID string, targetUserID string, tokenID string) (*PersonalAccessToken, error) @@ -97,14 +105,17 @@ type AccountManager interface { DeleteNameServerGroup(accountID, nsGroupID, userID string) error ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error) GetDNSDomain() string + StoreEvent(initiatorID, targetID, accountID string, activityID activity.Activity, meta map[string]any) GetEvents(accountID, userID string) ([]*activity.Event, error) GetDNSSettings(accountID string, userID string) (*DNSSettings, error) SaveDNSSettings(accountID string, userID string, dnsSettingsToSave *DNSSettings) error - GetPeer(accountID, peerID, userID string) (*Peer, error) + GetPeer(accountID, peerID, userID string) (*nbpeer.Peer, error) UpdateAccountSettings(accountID, userID string, newSettings *Settings) (*Account, error) - LoginPeer(login PeerLogin) (*Peer, *NetworkMap, error) // used by peer gRPC API - SyncPeer(sync PeerSync) (*Peer, *NetworkMap, error) // used by peer gRPC API + LoginPeer(login PeerLogin) (*nbpeer.Peer, *NetworkMap, error) // used by peer gRPC API + SyncPeer(sync PeerSync) (*nbpeer.Peer, *NetworkMap, error) // used by peer gRPC API GetAllConnectedPeers() (map[string]struct{}, error) + HasConnectedChannel(peerID string) bool + GetExternalCacheManager() ExternalCacheManager } type DefaultAccountManager struct { @@ -112,12 +123,13 @@ type DefaultAccountManager struct { // cacheMux and cacheLoading helps to make sure that only a single cache reload runs at a time per accountID cacheMux sync.Mutex // cacheLoading keeps the accountIDs that are currently reloading. The accountID has to be removed once cache has been reloaded - cacheLoading map[string]chan struct{} - peersUpdateManager *PeersUpdateManager - idpManager idp.Manager - cacheManager cache.CacheInterface[[]*idp.UserData] - ctx context.Context - eventStore activity.Store + cacheLoading map[string]chan struct{} + peersUpdateManager *PeersUpdateManager + idpManager idp.Manager + cacheManager cache.CacheInterface[[]*idp.UserData] + externalCacheManager ExternalCacheManager + ctx context.Context + eventStore activity.Store // singleAccountMode indicates whether the instance has a single account. // If true, then every new user will end up under the same account. @@ -151,17 +163,24 @@ type Settings struct { // JWTGroupsClaimName from which we extract groups name to add it to account groups JWTGroupsClaimName string + + // Extra is a dictionary of Account settings + Extra *account.ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"` } // Copy copies the Settings struct func (s *Settings) Copy() *Settings { - return &Settings{ + settings := &Settings{ PeerLoginExpirationEnabled: s.PeerLoginExpirationEnabled, PeerLoginExpiration: s.PeerLoginExpiration, JWTGroupsEnabled: s.JWTGroupsEnabled, JWTGroupsClaimName: s.JWTGroupsClaimName, GroupsPropagationEnabled: s.GroupsPropagationEnabled, } + if s.Extra != nil { + settings.Extra = s.Extra.Copy() + } + return settings } // Account represents a unique account of the system @@ -177,8 +196,8 @@ type Account struct { SetupKeys map[string]*SetupKey `gorm:"-"` SetupKeysG []SetupKey `json:"-" gorm:"foreignKey:AccountID;references:id"` Network *Network `gorm:"embedded;embeddedPrefix:network_"` - Peers map[string]*Peer `gorm:"-"` - PeersG []Peer `json:"-" gorm:"foreignKey:AccountID;references:id"` + Peers map[string]*nbpeer.Peer `gorm:"-"` + PeersG []nbpeer.Peer `json:"-" gorm:"foreignKey:AccountID;references:id"` Users map[string]*User `gorm:"-"` UsersG []User `json:"-" gorm:"foreignKey:AccountID;references:id"` Groups map[string]*Group `gorm:"-"` @@ -204,6 +223,7 @@ type UserInfo struct { Status string `json:"-"` IsServiceUser bool `json:"is_service_user"` IsBlocked bool `json:"is_blocked"` + NonDeletable bool `json:"non_deletable"` LastLogin time.Time `json:"last_login"` Issued string `json:"issued"` IntegrationReference IntegrationReference `json:"-"` @@ -212,7 +232,7 @@ type UserInfo struct { // getRoutesToSync returns the enabled routes for the peer ID and the routes // from the ACL peers that have distribution groups associated with the peer ID. // Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID. -func (a *Account) getRoutesToSync(peerID string, aclPeers []*Peer) []*route.Route { +func (a *Account) getRoutesToSync(peerID string, aclPeers []*nbpeer.Peer) []*route.Route { routes, peerDisabledRoutes := a.getRoutingPeerRoutes(peerID) peerRoutesMembership := make(lookupMap) for _, r := range append(routes, peerDisabledRoutes...) { @@ -336,10 +356,22 @@ func (a *Account) GetGroup(groupID string) *Group { // GetPeerNetworkMap returns a group by ID if exists, nil otherwise func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string) *NetworkMap { + peer := a.Peers[peerID] + if peer == nil { + return &NetworkMap{ + Network: a.Network.Copy(), + } + } + validatedPeers := additions.ValidatePeers([]*nbpeer.Peer{peer}) + if len(validatedPeers) == 0 { + return &NetworkMap{ + Network: a.Network.Copy(), + } + } aclPeers, firewallRules := a.getPeerConnectionResources(peerID) // exclude expired peers - var peersToConnect []*Peer - var expiredPeers []*Peer + var peersToConnect []*nbpeer.Peer + var expiredPeers []*nbpeer.Peer for _, p := range aclPeers { expired, _ := p.LoginExpired(a.Settings.PeerLoginExpiration) if a.Settings.PeerLoginExpirationEnabled && expired { @@ -377,8 +409,8 @@ func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string) *NetworkMap { } // GetExpiredPeers returns peers that have been expired -func (a *Account) GetExpiredPeers() []*Peer { - var peers []*Peer +func (a *Account) GetExpiredPeers() []*nbpeer.Peer { + var peers []*nbpeer.Peer for _, peer := range a.GetPeersWithExpiration() { expired, _ := peer.LoginExpired(a.Settings.PeerLoginExpiration) if expired { @@ -417,8 +449,8 @@ func (a *Account) GetNextPeerExpiration() (time.Duration, bool) { } // GetPeersWithExpiration returns a list of peers that have Peer.LoginExpirationEnabled set to true and that were added by a user -func (a *Account) GetPeersWithExpiration() []*Peer { - peers := make([]*Peer, 0) +func (a *Account) GetPeersWithExpiration() []*nbpeer.Peer { + peers := make([]*nbpeer.Peer, 0) for _, peer := range a.Peers { if peer.LoginExpirationEnabled && peer.AddedWithSSOLogin() { peers = append(peers, peer) @@ -428,8 +460,8 @@ func (a *Account) GetPeersWithExpiration() []*Peer { } // GetPeers returns a list of all Account peers -func (a *Account) GetPeers() []*Peer { - var peers []*Peer +func (a *Account) GetPeers() []*nbpeer.Peer { + var peers []*nbpeer.Peer for _, peer := range a.Peers { peers = append(peers, peer) } @@ -443,7 +475,7 @@ func (a *Account) UpdateSettings(update *Settings) *Account { } // UpdatePeer saves new or replaces existing peer -func (a *Account) UpdatePeer(update *Peer) { +func (a *Account) UpdatePeer(update *nbpeer.Peer) { a.Peers[update.ID] = update } @@ -472,7 +504,7 @@ func (a *Account) DeletePeer(peerID string) { // FindPeerByPubKey looks for a Peer by provided WireGuard public key in the Account or returns error if it wasn't found. // It will return an object copy of the peer. -func (a *Account) FindPeerByPubKey(peerPubKey string) (*Peer, error) { +func (a *Account) FindPeerByPubKey(peerPubKey string) (*nbpeer.Peer, error) { for _, peer := range a.Peers { if peer.Key == peerPubKey { return peer.Copy(), nil @@ -483,8 +515,8 @@ func (a *Account) FindPeerByPubKey(peerPubKey string) (*Peer, error) { } // FindUserPeers returns a list of peers that user owns (created) -func (a *Account) FindUserPeers(userID string) ([]*Peer, error) { - peers := make([]*Peer, 0) +func (a *Account) FindUserPeers(userID string) ([]*nbpeer.Peer, error) { + peers := make([]*nbpeer.Peer, 0) for _, peer := range a.Peers { if peer.UserID == userID { peers = append(peers, peer) @@ -576,7 +608,7 @@ func (a *Account) getPeerDNSLabels() lookupMap { } func (a *Account) Copy() *Account { - peers := map[string]*Peer{} + peers := map[string]*nbpeer.Peer{} for id, peer := range a.Peers { peers[id] = peer.Copy() } @@ -653,7 +685,7 @@ func (a *Account) GetGroupAll() (*Group, error) { } // GetPeer looks up a Peer by ID -func (a *Account) GetPeer(peerID string) *Peer { +func (a *Account) GetPeer(peerID string) *nbpeer.Peer { return a.Peers[peerID] } @@ -816,9 +848,13 @@ func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManage goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute) goCacheStore := cacheStore.NewGoCache(goCacheClient) - am.cacheManager = cache.NewLoadable[[]*idp.UserData](am.loadAccount, cache.New[[]*idp.UserData](goCacheStore)) + // TODO: what is max expiration time? Should be quite long + am.externalCacheManager = cache.New[*idp.UserData]( + cacheStore.NewGoCache(goCacheClient), + ) + if !isNil(am.idpManager) { go func() { err := am.warmupIDPCache() @@ -833,6 +869,10 @@ func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManage return am, nil } +func (am *DefaultAccountManager) GetExternalCacheManager() ExternalCacheManager { + return am.externalCacheManager +} + // UpdateAccountSettings updates Account settings. // Only users with role UserRoleAdmin can update the account. // User that performs the update has to belong to the account. @@ -855,12 +895,17 @@ func (am *DefaultAccountManager) UpdateAccountSettings(accountID, userID string, return nil, err } + err = additions.ValidateExtraSettings(newSettings.Extra, account.Settings.Extra, account.Peers, userID, accountID, am.eventStore) + if err != nil { + return nil, err + } + user, err := account.FindUser(userID) if err != nil { return nil, err } - if !user.IsAdmin() { + if !user.HasAdminPower() { return nil, status.Errorf(status.PermissionDenied, "user is not allowed to update account") } @@ -873,11 +918,11 @@ func (am *DefaultAccountManager) UpdateAccountSettings(accountID, userID string, } else { am.checkAndSchedulePeerLoginExpiration(account) } - am.storeEvent(userID, accountID, accountID, event, nil) + am.StoreEvent(userID, accountID, accountID, event, nil) } if oldSettings.PeerLoginExpiration != newSettings.PeerLoginExpiration { - am.storeEvent(userID, accountID, accountID, activity.AccountPeerLoginExpirationDurationUpdated, nil) + am.StoreEvent(userID, accountID, accountID, activity.AccountPeerLoginExpirationDurationUpdated, nil) am.checkAndSchedulePeerLoginExpiration(account) } @@ -934,14 +979,15 @@ func (am *DefaultAccountManager) newAccount(userID, domain string) (*Account, er _, err := am.Store.GetAccount(accountId) statusErr, _ := status.FromError(err) - if err == nil { + switch { + case err == nil: log.Warnf("an account with ID already exists, retrying...") continue - } else if statusErr.Type() == status.NotFound { + case statusErr.Type() == status.NotFound: newAccount := newAccountWithId(accountId, userID, domain) - am.storeEvent(userID, newAccount.Id, accountId, activity.AccountCreated, nil) + am.StoreEvent(userID, newAccount.Id, accountId, activity.AccountCreated, nil) return newAccount, nil - } else { + default: return nil, err } } @@ -987,6 +1033,57 @@ func (am *DefaultAccountManager) warmupIDPCache() error { return nil } +// DeleteAccount deletes an account and all its users from local store and from the remote IDP if the requester is an admin and account owner +func (am *DefaultAccountManager) DeleteAccount(accountID, userID string) error { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + account, err := am.Store.GetAccount(accountID) + if err != nil { + return err + } + + user, err := account.FindUser(userID) + if err != nil { + return err + } + + if !user.HasAdminPower() { + return status.Errorf(status.PermissionDenied, "user is not allowed to delete account") + } + + if user.Id != account.CreatedBy { + return status.Errorf(status.PermissionDenied, "user is not allowed to delete account. Only account owner can delete account") + } + for _, otherUser := range account.Users { + if otherUser.IsServiceUser { + continue + } + + if otherUser.Id == userID { + continue + } + + deleteUserErr := am.deleteRegularUser(account, userID, otherUser.Id) + if deleteUserErr != nil { + return deleteUserErr + } + } + + err = am.deleteRegularUser(account, userID, userID) + if err != nil { + log.Errorf("failed deleting user %s. error: %s", userID, err) + return err + } + + err = am.Store.DeleteAccount(account) + if err != nil { + log.Errorf("failed deleting account %s. error: %s", accountID, err) + return err + } + log.Debugf("account %s deleted", accountID) + return nil +} + // GetAccountByUserOrAccountID looks for an account by user or accountID, if no account is provided and // userID doesn't have an account associated with it, one account is created func (am *DefaultAccountManager) GetAccountByUserOrAccountID(userID, accountID, domain string) (*Account, error) { @@ -1094,10 +1191,15 @@ func (am *DefaultAccountManager) lookupUserInCacheByEmail(email string, accountI // 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(userID string, account *Account) (*idp.UserData, error) { users := make(map[string]struct{}, len(account.Users)) + // ignore service users and users provisioned by integrations than are never logged in for _, user := range account.Users { - if !user.IsServiceUser { - users[user.Id] = struct{}{} + if user.IsServiceUser { + continue } + if user.Issued == UserIssuedIntegration { + continue + } + users[user.Id] = struct{}{} } log.Debugf("looking up user %s of account %s in cache", userID, account.Id) userData, err := am.lookupCache(users, account.Id) @@ -1167,16 +1269,20 @@ func (am *DefaultAccountManager) lookupCache(accountUsers map[string]struct{}, a userDataMap[datum.ID] = struct{}{} } - // check whether we need to reload the cache - // the accountUsers ID list is the source of truth and all the users should be in the cache - reload := len(accountUsers) != len(data) + // the accountUsers ID list of non integration users from store, we check if cache has all of them + // as result of for loop knownUsersCount will have number of users are not presented in the cashed + knownUsersCount := len(accountUsers) for user := range accountUsers { - if _, ok := userDataMap[user]; !ok { - reload = true + if _, ok := userDataMap[user]; ok { + knownUsersCount-- + continue } + log.Debugf("cache doesn't know about %s user", user) } - if reload { + // if we know users that are not yet in cache more likely cache is outdated + if knownUsersCount > 0 { + log.Debugf("cache doesn't know about %d users from store, reloading", knownUsersCount) // reload cache once avoiding loops data, err = am.refreshCache(accountID) if err != nil { @@ -1280,7 +1386,7 @@ func (am *DefaultAccountManager) handleNewUserAccount(domainAcc *Account, claims return nil, err } - am.storeEvent(claims.UserId, claims.UserId, account.Id, activity.UserJoined, nil) + am.StoreEvent(claims.UserId, claims.UserId, account.Id, activity.UserJoined, nil) return account, nil } @@ -1313,7 +1419,7 @@ func (am *DefaultAccountManager) redeemInvite(account *Account, userID string) e return } log.Debugf("user %s of account %s redeemed invite", user.ID, account.Id) - am.storeEvent(userID, userID, account.Id, activity.UserJoined, nil) + am.StoreEvent(userID, userID, account.Id, activity.UserJoined, nil) }() } @@ -1463,7 +1569,7 @@ func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.Authorizat am.updateAccountPeers(account) for _, g := range addNewGroups { if group := account.GetGroup(g); group != nil { - am.storeEvent(user.Id, user.Id, account.Id, activity.GroupAddedToUser, + am.StoreEvent(user.Id, user.Id, account.Id, activity.GroupAddedToUser, map[string]any{ "group": group.Name, "group_id": group.ID, @@ -1473,7 +1579,7 @@ func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.Authorizat } for _, g := range removeOldGroups { if group := account.GetGroup(g); group != nil { - am.storeEvent(user.Id, user.Id, account.Id, activity.GroupRemovedFromUser, + am.StoreEvent(user.Id, user.Id, account.Id, activity.GroupRemovedFromUser, map[string]any{ "group": group.Name, "group_id": group.ID, @@ -1571,9 +1677,15 @@ func (am *DefaultAccountManager) GetAllConnectedPeers() (map[string]struct{}, er return am.peersUpdateManager.GetAllConnectedPeers(), nil } +// HasConnectedChannel returns true if peers has channel in update manager, otherwise false +func (am *DefaultAccountManager) HasConnectedChannel(peerID string) bool { + return am.peersUpdateManager.HasChannel(peerID) +} + +var invalidDomainRegexp = regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`) + func isDomainValid(domain string) bool { - re := regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`) - return re.Match([]byte(domain)) + return invalidDomainRegexp.MatchString(domain) } // GetDNSDomain returns the configured dnsDomain @@ -1619,12 +1731,12 @@ func newAccountWithId(accountID, userID, domain string) *Account { log.Debugf("creating new account") network := NewNetwork() - peers := make(map[string]*Peer) + peers := make(map[string]*nbpeer.Peer) users := make(map[string]*User) routes := make(map[string]*route.Route) setupKeys := map[string]*SetupKey{} nameServersGroups := make(map[string]*nbdns.NameServerGroup) - users[userID] = NewAdminUser(userID) + users[userID] = NewOwnerUser(userID) dnsSettings := DNSSettings{ DisabledManagementGroups: make([]string, 0), } diff --git a/management/server/account/account.go b/management/server/account/account.go new file mode 100644 index 000000000..b8b71a6de --- /dev/null +++ b/management/server/account/account.go @@ -0,0 +1,13 @@ +package account + +type ExtraSettings struct { + // PeerApprovalEnabled enables or disables the need for peers bo be approved by an administrator + PeerApprovalEnabled bool +} + +// Copy copies the ExtraSettings struct +func (e *ExtraSettings) Copy() *ExtraSettings { + return &ExtraSettings{ + PeerApprovalEnabled: e.PeerApprovalEnabled, + } +} diff --git a/management/server/account_test.go b/management/server/account_test.go index c8a8a5dc9..5e204984e 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -15,6 +15,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/activity" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/route" "github.com/stretchr/testify/assert" @@ -25,10 +26,11 @@ import ( ) func verifyCanAddPeerToAccount(t *testing.T, manager AccountManager, account *Account, userID string) { - peer := &Peer{ + t.Helper() + peer := &nbpeer.Peer{ Key: "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=", Name: "test-host@netbird.io", - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host@netbird.io", GoOS: "linux", Kernel: "Linux", @@ -52,6 +54,7 @@ func verifyCanAddPeerToAccount(t *testing.T, manager AccountManager, account *Ac } func verifyNewAccountHasDefaultFields(t *testing.T, account *Account, createdBy string, domain string, expectedUsers []string) { + t.Helper() if len(account.Peers) != 0 { t.Errorf("expected account to have len(Peers) = %v, got %v", 0, len(account.Peers)) } @@ -108,13 +111,14 @@ func verifyNewAccountHasDefaultFields(t *testing.T, account *Account, createdBy func TestAccount_GetPeerNetworkMap(t *testing.T) { peerID1 := "peer-1" peerID2 := "peer-2" + // peerID3 := "peer-3" tt := []struct { name string accountSettings Settings peerID string expectedPeers []string expectedOfflinePeers []string - peers map[string]*Peer + peers map[string]*nbpeer.Peer }{ { name: "Should return ALL peers when global peer login expiration disabled", @@ -122,14 +126,14 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) { peerID: peerID1, expectedPeers: []string{peerID2}, expectedOfflinePeers: []string{}, - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { ID: peerID1, Key: "peer-1-key", IP: net.IP{100, 64, 0, 1}, Name: peerID1, DNSLabel: peerID1, - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ LastSeen: time.Now().UTC(), Connected: false, LoginExpired: true, @@ -143,7 +147,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) { IP: net.IP{100, 64, 0, 1}, Name: peerID2, DNSLabel: peerID2, - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ LastSeen: time.Now().UTC(), Connected: false, LoginExpired: false, @@ -160,14 +164,14 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) { peerID: peerID1, expectedPeers: []string{}, expectedOfflinePeers: []string{peerID2}, - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { ID: peerID1, Key: "peer-1-key", IP: net.IP{100, 64, 0, 1}, Name: peerID1, DNSLabel: peerID1, - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ LastSeen: time.Now().UTC(), Connected: false, LoginExpired: true, @@ -182,7 +186,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) { IP: net.IP{100, 64, 0, 1}, Name: peerID2, DNSLabel: peerID2, - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ LastSeen: time.Now().UTC(), Connected: false, LoginExpired: true, @@ -193,6 +197,159 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) { }, }, }, + // { + // name: "Should return only peers that are approved when peer approval is enabled", + // accountSettings: Settings{PeerApprovalEnabled: true}, + // peerID: peerID1, + // expectedPeers: []string{peerID3}, + // expectedOfflinePeers: []string{}, + // peers: map[string]*Peer{ + // "peer-1": { + // ID: peerID1, + // Key: "peer-1-key", + // IP: net.IP{100, 64, 0, 1}, + // Name: peerID1, + // DNSLabel: peerID1, + // Status: &PeerStatus{ + // LastSeen: time.Now().UTC(), + // Connected: false, + // Approved: true, + // }, + // UserID: userID, + // LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30), + // }, + // "peer-2": { + // ID: peerID2, + // Key: "peer-2-key", + // IP: net.IP{100, 64, 0, 1}, + // Name: peerID2, + // DNSLabel: peerID2, + // Status: &PeerStatus{ + // LastSeen: time.Now().UTC(), + // Connected: false, + // Approved: false, + // }, + // UserID: userID, + // LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30), + // }, + // "peer-3": { + // ID: peerID3, + // Key: "peer-3-key", + // IP: net.IP{100, 64, 0, 1}, + // Name: peerID3, + // DNSLabel: peerID3, + // Status: &PeerStatus{ + // LastSeen: time.Now().UTC(), + // Connected: false, + // Approved: true, + // }, + // UserID: userID, + // LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30), + // }, + // }, + // }, + // { + // name: "Should return all peers when peer approval is disabled", + // accountSettings: Settings{PeerApprovalEnabled: false}, + // peerID: peerID1, + // expectedPeers: []string{peerID2, peerID3}, + // expectedOfflinePeers: []string{}, + // peers: map[string]*Peer{ + // "peer-1": { + // ID: peerID1, + // Key: "peer-1-key", + // IP: net.IP{100, 64, 0, 1}, + // Name: peerID1, + // DNSLabel: peerID1, + // Status: &PeerStatus{ + // LastSeen: time.Now().UTC(), + // Connected: false, + // Approved: true, + // }, + // UserID: userID, + // LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30), + // }, + // "peer-2": { + // ID: peerID2, + // Key: "peer-2-key", + // IP: net.IP{100, 64, 0, 1}, + // Name: peerID2, + // DNSLabel: peerID2, + // Status: &PeerStatus{ + // LastSeen: time.Now().UTC(), + // Connected: false, + // Approved: false, + // }, + // UserID: userID, + // LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30), + // }, + // "peer-3": { + // ID: peerID3, + // Key: "peer-3-key", + // IP: net.IP{100, 64, 0, 1}, + // Name: peerID3, + // DNSLabel: peerID3, + // Status: &PeerStatus{ + // LastSeen: time.Now().UTC(), + // Connected: false, + // Approved: true, + // }, + // UserID: userID, + // LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30), + // }, + // }, + // }, + // { + // name: "Should return no peers when peer approval is enabled and the requesting peer is not approved", + // accountSettings: Settings{PeerApprovalEnabled: true}, + // peerID: peerID1, + // expectedPeers: []string{}, + // expectedOfflinePeers: []string{}, + // peers: map[string]*Peer{ + // "peer-1": { + // ID: peerID1, + // Key: "peer-1-key", + // IP: net.IP{100, 64, 0, 1}, + // Name: peerID1, + // DNSLabel: peerID1, + // Status: &PeerStatus{ + // LastSeen: time.Now().UTC(), + // Connected: false, + // Approved: false, + // }, + // UserID: userID, + // LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30), + // }, + // "peer-2": { + // ID: peerID2, + // Key: "peer-2-key", + // IP: net.IP{100, 64, 0, 1}, + // Name: peerID2, + // DNSLabel: peerID2, + // Status: &PeerStatus{ + // LastSeen: time.Now().UTC(), + // Connected: false, + // Approved: true, + // }, + // UserID: userID, + // LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30), + // }, + // "peer-3": { + // ID: peerID3, + // Key: "peer-3-key", + // IP: net.IP{100, 64, 0, 1}, + // Name: peerID3, + // DNSLabel: peerID3, + // Status: &PeerStatus{ + // LastSeen: time.Now().UTC(), + // Connected: false, + // Approved: true, + // }, + // UserID: userID, + // LastLogin: time.Now().UTC().Add(-time.Hour * 24 * 30 * 30), + // }, + // }, + // }, } netIP := net.IP{100, 64, 0, 0} @@ -207,6 +364,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) { for _, testCase := range tt { account := newAccountWithId("account-1", userID, "netbird.io") + account.UpdateSettings(&testCase.accountSettings) account.Network = network account.Peers = testCase.peers for _, peer := range account.Peers { @@ -251,7 +409,7 @@ func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) { } if account != nil && account.Users[userID] == nil { - t.Fatalf("expected to create an account for a user %s but no user was found after creation udner the account %s", userID, account.Id) + t.Fatalf("expected to create an account for a user %s but no user was found after creation under the account %s", userID, account.Id) return } @@ -304,7 +462,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) { inputInitUserParams: defaultInitAccount, testingFunc: require.NotEqual, expectedMSG: "account IDs shouldn't match", - expectedUserRole: UserRoleAdmin, + expectedUserRole: UserRoleOwner, expectedDomainCategory: "", expectedDomain: publicDomain, expectedPrimaryDomainStatus: false, @@ -326,7 +484,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) { inputInitUserParams: initUnknown, testingFunc: require.NotEqual, expectedMSG: "account IDs shouldn't match", - expectedUserRole: UserRoleAdmin, + expectedUserRole: UserRoleOwner, expectedDomain: unknownDomain, expectedDomainCategory: "", expectedPrimaryDomainStatus: false, @@ -344,7 +502,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) { inputInitUserParams: defaultInitAccount, testingFunc: require.NotEqual, expectedMSG: "account IDs shouldn't match", - expectedUserRole: UserRoleAdmin, + expectedUserRole: UserRoleOwner, expectedDomain: privateDomain, expectedDomainCategory: PrivateCategory, expectedPrimaryDomainStatus: true, @@ -385,7 +543,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) { inputInitUserParams: defaultInitAccount, testingFunc: require.Equal, expectedMSG: "account IDs should match", - expectedUserRole: UserRoleAdmin, + expectedUserRole: UserRoleOwner, expectedDomain: defaultInitAccount.Domain, expectedDomainCategory: PrivateCategory, expectedPrimaryDomainStatus: true, @@ -404,7 +562,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) { inputInitUserParams: defaultInitAccount, testingFunc: require.Equal, expectedMSG: "account IDs should match", - expectedUserRole: UserRoleAdmin, + expectedUserRole: UserRoleOwner, expectedDomain: defaultInitAccount.Domain, expectedDomainCategory: PrivateCategory, expectedPrimaryDomainStatus: true, @@ -422,7 +580,7 @@ func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) { inputInitUserParams: defaultInitAccount, testingFunc: require.NotEqual, expectedMSG: "account IDs shouldn't match", - expectedUserRole: UserRoleAdmin, + expectedUserRole: UserRoleOwner, expectedDomain: "", expectedDomainCategory: "", expectedPrimaryDomainStatus: false, @@ -626,7 +784,7 @@ func TestAccountManager_PrivateAccount(t *testing.T) { } if account != nil && account.Users[userId] == nil { - t.Fatalf("expected to create an account for a user %s but no user was found after creation udner the account %s", userId, account.Id) + t.Fatalf("expected to create an account for a user %s but no user was found after creation under the account %s", userId, account.Id) } } @@ -744,6 +902,31 @@ func TestAccountManager_GetAccount(t *testing.T) { } } +func TestAccountManager_DeleteAccount(t *testing.T) { + manager, err := createManager(t) + if err != nil { + t.Fatal(err) + return + } + + expectedId := "test_account" + userId := "account_creator" + account, err := createAccount(manager, expectedId, userId, "") + if err != nil { + t.Fatal(err) + } + + err = manager.DeleteAccount(account.Id, userId) + if err != nil { + t.Fatal(err) + } + + getAccount, err := manager.Store.GetAccount(account.Id) + if err == nil { + t.Fatal(fmt.Errorf("expected to get an error when trying to get deleted account, got %v", getAccount)) + } +} + func TestAccountManager_AddPeer(t *testing.T) { manager, err := createManager(t) if err != nil { @@ -778,9 +961,9 @@ func TestAccountManager_AddPeer(t *testing.T) { expectedPeerKey := key.PublicKey().String() expectedSetupKey := setupKey.Key - peer, _, err := manager.AddPeer(setupKey.Key, "", &Peer{ + peer, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: expectedPeerKey, - Meta: PeerSystemMeta{Hostname: expectedPeerKey}, + Meta: nbpeer.PeerSystemMeta{Hostname: expectedPeerKey}, }) if err != nil { t.Errorf("expecting peer to be added, got failure %v", err) @@ -846,9 +1029,9 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) { expectedPeerKey := key.PublicKey().String() expectedUserID := userID - peer, _, err := manager.AddPeer("", userID, &Peer{ + peer, _, err := manager.AddPeer("", userID, &nbpeer.Peer{ Key: expectedPeerKey, - Meta: PeerSystemMeta{Hostname: expectedPeerKey}, + Meta: nbpeer.PeerSystemMeta{Hostname: expectedPeerKey}, }) if err != nil { t.Errorf("expecting peer to be added, got failure %v, account users: %v", err, account.CreatedBy) @@ -913,7 +1096,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) { return } - getPeer := func() *Peer { + getPeer := func() *nbpeer.Peer { key, err := wgtypes.GeneratePrivateKey() if err != nil { t.Fatal(err) @@ -921,9 +1104,9 @@ func TestAccountManager_NetworkUpdates(t *testing.T) { } expectedPeerKey := key.PublicKey().String() - peer, _, err := manager.AddPeer(setupKey.Key, "", &Peer{ + peer, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: expectedPeerKey, - Meta: PeerSystemMeta{Hostname: expectedPeerKey}, + Meta: nbpeer.PeerSystemMeta{Hostname: expectedPeerKey}, }) if err != nil { t.Fatalf("expecting peer1 to be added, got failure %v", err) @@ -1095,9 +1278,9 @@ func TestAccountManager_DeletePeer(t *testing.T) { peerKey := key.PublicKey().String() - peer, _, err := manager.AddPeer(setupKey.Key, "", &Peer{ + peer, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: peerKey, - Meta: PeerSystemMeta{Hostname: peerKey}, + Meta: nbpeer.PeerSystemMeta{Hostname: peerKey}, }) if err != nil { t.Errorf("expecting peer to be added, got failure %v", err) @@ -1131,6 +1314,7 @@ func TestAccountManager_DeletePeer(t *testing.T) { } func getEvent(t *testing.T, accountID string, manager AccountManager, eventType activity.Activity) *activity.Event { + t.Helper() for { select { case <-time.After(time.Second): @@ -1155,7 +1339,7 @@ func TestGetUsersFromAccount(t *testing.T) { t.Fatal(err) } - users := map[string]*User{"1": {Id: "1", Role: "admin"}, "2": {Id: "2", Role: "user"}, "3": {Id: "3", Role: "user"}} + users := map[string]*User{"1": {Id: "1", Role: UserRoleOwner}, "2": {Id: "2", Role: "user"}, "3": {Id: "3", Role: "user"}} accountId := "test_account_id" account, err := createAccount(manager, accountId, users["1"].Id, "") @@ -1235,8 +1419,8 @@ func TestAccount_GetRoutesToSync(t *testing.T) { t.Fatal(err) } account := &Account{ - Peers: map[string]*Peer{ - "peer-1": {Key: "peer-1", Meta: PeerSystemMeta{GoOS: "linux"}}, "peer-2": {Key: "peer-2", Meta: PeerSystemMeta{GoOS: "linux"}}, "peer-3": {Key: "peer-1", Meta: PeerSystemMeta{GoOS: "linux"}}, + Peers: map[string]*nbpeer.Peer{ + "peer-1": {Key: "peer-1", Meta: nbpeer.PeerSystemMeta{GoOS: "linux"}}, "peer-2": {Key: "peer-2", Meta: nbpeer.PeerSystemMeta{GoOS: "linux"}}, "peer-3": {Key: "peer-1", Meta: nbpeer.PeerSystemMeta{GoOS: "linux"}}, }, Groups: map[string]*Group{"group1": {ID: "group1", Peers: []string{"peer-1", "peer-2"}}}, Routes: map[string]*route.Route{ @@ -1279,7 +1463,7 @@ func TestAccount_GetRoutesToSync(t *testing.T) { }, } - routes := account.getRoutesToSync("peer-2", []*Peer{{Key: "peer-1"}, {Key: "peer-3"}}) + routes := account.getRoutesToSync("peer-2", []*nbpeer.Peer{{Key: "peer-1"}, {Key: "peer-3"}}) assert.Len(t, routes, 2) routeIDs := make(map[string]struct{}, 2) @@ -1289,7 +1473,7 @@ func TestAccount_GetRoutesToSync(t *testing.T) { assert.Contains(t, routeIDs, "route-2") assert.Contains(t, routeIDs, "route-3") - emptyRoutes := account.getRoutesToSync("peer-3", []*Peer{{Key: "peer-1"}, {Key: "peer-2"}}) + emptyRoutes := account.getRoutesToSync("peer-3", []*nbpeer.Peer{{Key: "peer-1"}, {Key: "peer-2"}}) assert.Len(t, emptyRoutes, 0) } @@ -1310,10 +1494,10 @@ func TestAccount_Copy(t *testing.T) { Network: &Network{ Identifier: "net1", }, - Peers: map[string]*Peer{ + Peers: map[string]*nbpeer.Peer{ "peer1": { Key: "key1", - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ LastSeen: time.Now(), Connected: true, LoginExpired: false, @@ -1385,7 +1569,7 @@ func TestAccount_Copy(t *testing.T) { if err != nil { t.Fatal(err) } - account.Peers["peer1"].Status.Connected = false // we change original object to confirm that copy wont change + account.Peers["peer1"].Status.Connected = false // we change original object to confirm that copy won't change accCopyBytes, err := json.Marshal(accountCopy) if err != nil { t.Fatal(err) @@ -1440,9 +1624,9 @@ func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) { key, err := wgtypes.GenerateKey() require.NoError(t, err, "unable to generate WireGuard key") - peer, _, err := manager.AddPeer("", userID, &Peer{ + peer, _, err := manager.AddPeer("", userID, &nbpeer.Peer{ Key: key.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer"}, LoginExpirationEnabled: true, }) require.NoError(t, err, "unable to add peer") @@ -1489,9 +1673,9 @@ func TestDefaultAccountManager_MarkPeerConnected_PeerLoginExpiration(t *testing. key, err := wgtypes.GenerateKey() require.NoError(t, err, "unable to generate WireGuard key") - _, _, err = manager.AddPeer("", userID, &Peer{ + _, _, err = manager.AddPeer("", userID, &nbpeer.Peer{ Key: key.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer"}, LoginExpirationEnabled: true, }) require.NoError(t, err, "unable to add peer") @@ -1530,9 +1714,9 @@ func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *test key, err := wgtypes.GenerateKey() require.NoError(t, err, "unable to generate WireGuard key") - _, _, err = manager.AddPeer("", userID, &Peer{ + _, _, err = manager.AddPeer("", userID, &nbpeer.Peer{ Key: key.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer"}, LoginExpirationEnabled: true, }) require.NoError(t, err, "unable to add peer") @@ -1611,13 +1795,13 @@ func TestDefaultAccountManager_UpdateAccountSettings(t *testing.T) { func TestAccount_GetExpiredPeers(t *testing.T) { type test struct { name string - peers map[string]*Peer + peers map[string]*nbpeer.Peer expectedPeers map[string]struct{} } testCases := []test{ { name: "Peers with login expiration disabled, no expired peers", - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { LoginExpirationEnabled: false, }, @@ -1629,11 +1813,11 @@ func TestAccount_GetExpiredPeers(t *testing.T) { }, { name: "Two peers expired", - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { ID: "peer-1", LoginExpirationEnabled: true, - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ LastSeen: time.Now().UTC(), Connected: true, LoginExpired: false, @@ -1644,7 +1828,7 @@ func TestAccount_GetExpiredPeers(t *testing.T) { "peer-2": { ID: "peer-2", LoginExpirationEnabled: true, - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ LastSeen: time.Now().UTC(), Connected: true, LoginExpired: false, @@ -1656,7 +1840,7 @@ func TestAccount_GetExpiredPeers(t *testing.T) { "peer-3": { ID: "peer-3", LoginExpirationEnabled: true, - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ LastSeen: time.Now().UTC(), Connected: true, LoginExpired: false, @@ -1696,19 +1880,19 @@ func TestAccount_GetExpiredPeers(t *testing.T) { func TestAccount_GetPeersWithExpiration(t *testing.T) { type test struct { name string - peers map[string]*Peer + peers map[string]*nbpeer.Peer expectedPeers map[string]struct{} } testCases := []test{ { name: "No account peers, no peers with expiration", - peers: map[string]*Peer{}, + peers: map[string]*nbpeer.Peer{}, expectedPeers: map[string]struct{}{}, }, { name: "Peers with login expiration disabled, no peers with expiration", - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { LoginExpirationEnabled: false, UserID: userID, @@ -1722,7 +1906,7 @@ func TestAccount_GetPeersWithExpiration(t *testing.T) { }, { name: "Peers with login expiration enabled, return peers with expiration", - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { ID: "peer-1", LoginExpirationEnabled: true, @@ -1765,7 +1949,7 @@ func TestAccount_GetPeersWithExpiration(t *testing.T) { func TestAccount_GetNextPeerExpiration(t *testing.T) { type test struct { name string - peers map[string]*Peer + peers map[string]*nbpeer.Peer expiration time.Duration expirationEnabled bool expectedNextRun bool @@ -1776,7 +1960,7 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { testCases := []test{ { name: "No peers, no expiration", - peers: map[string]*Peer{}, + peers: map[string]*nbpeer.Peer{}, expiration: time.Second, expirationEnabled: false, expectedNextRun: false, @@ -1784,16 +1968,16 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { }, { name: "No connected peers, no expiration", - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: false, }, LoginExpirationEnabled: true, UserID: userID, }, "peer-2": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: true, }, LoginExpirationEnabled: false, @@ -1807,16 +1991,16 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { }, { name: "Connected peers with disabled expiration, no expiration", - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: true, }, LoginExpirationEnabled: false, UserID: userID, }, "peer-2": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: true, }, LoginExpirationEnabled: false, @@ -1830,9 +2014,9 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { }, { name: "Expired peers, no expiration", - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: true, LoginExpired: true, }, @@ -1840,7 +2024,7 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { UserID: userID, }, "peer-2": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: true, LoginExpired: true, }, @@ -1855,9 +2039,9 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { }, { name: "To be expired peer, return expiration", - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: true, LoginExpired: false, }, @@ -1866,7 +2050,7 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { UserID: userID, }, "peer-2": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: true, LoginExpired: true, }, @@ -1881,9 +2065,9 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { }, { name: "Peers added with setup keys, no expiration", - peers: map[string]*Peer{ + peers: map[string]*nbpeer.Peer{ "peer-1": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: true, LoginExpired: false, }, @@ -1891,7 +2075,7 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { SetupKey: "key", }, "peer-2": { - Status: &PeerStatus{ + Status: &nbpeer.PeerStatus{ Connected: true, LoginExpired: false, }, @@ -1926,7 +2110,7 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) { func TestAccount_SetJWTGroups(t *testing.T) { // create a new account account := &Account{ - Peers: map[string]*Peer{ + Peers: map[string]*nbpeer.Peer{ "peer1": {ID: "peer1", Key: "key1", UserID: "user1"}, "peer2": {ID: "peer2", Key: "key2", UserID: "user1"}, "peer3": {ID: "peer3", Key: "key3", UserID: "user1"}, @@ -1974,7 +2158,7 @@ func TestAccount_SetJWTGroups(t *testing.T) { func TestAccount_UserGroupsAddToPeers(t *testing.T) { account := &Account{ - Peers: map[string]*Peer{ + Peers: map[string]*nbpeer.Peer{ "peer1": {ID: "peer1", Key: "key1", UserID: "user1"}, "peer2": {ID: "peer2", Key: "key2", UserID: "user1"}, "peer3": {ID: "peer3", Key: "key3", UserID: "user1"}, @@ -2010,7 +2194,7 @@ func TestAccount_UserGroupsAddToPeers(t *testing.T) { func TestAccount_UserGroupsRemoveFromPeers(t *testing.T) { account := &Account{ - Peers: map[string]*Peer{ + Peers: map[string]*nbpeer.Peer{ "peer1": {ID: "peer1", Key: "key1", UserID: "user1"}, "peer2": {ID: "peer2", Key: "key2", UserID: "user1"}, "peer3": {ID: "peer3", Key: "key3", UserID: "user1"}, @@ -2038,15 +2222,17 @@ func TestAccount_UserGroupsRemoveFromPeers(t *testing.T) { } func createManager(t *testing.T) (*DefaultAccountManager, error) { + t.Helper() store, err := createStore(t) if err != nil { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.cloud", eventStore, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, false) } func createStore(t *testing.T) (Store, error) { + t.Helper() dataDir := t.TempDir() store, err := NewStoreFromJson(dataDir, nil) if err != nil { diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index ce36f520f..54a27e4cc 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -114,6 +114,22 @@ const ( PeerLoginExpired // DashboardLogin indicates that the user logged in to the dashboard DashboardLogin + // IntegrationCreated indicates that the user created an integration + IntegrationCreated + // IntegrationUpdated indicates that the user updated an integration + IntegrationUpdated + // IntegrationDeleted indicates that the user deleted an integration + IntegrationDeleted + // AccountPeerApprovalEnabled indicates that the user enabled peer approval for the account + AccountPeerApprovalEnabled + // AccountPeerApprovalDisabled indicates that the user disabled peer approval for the account + AccountPeerApprovalDisabled + // PeerApproved indicates that the peer has been approved + PeerApproved + // PeerApprovalRevoked indicates that the peer approval has been revoked + PeerApprovalRevoked + // TransferredOwnerRole indicates that the user transferred the owner role of the account + TransferredOwnerRole ) var activityMap = map[Activity]Code{ @@ -169,6 +185,14 @@ var activityMap = map[Activity]Code{ UserLoggedInPeer: {"User logged in peer", "user.peer.login"}, PeerLoginExpired: {"Peer login expired", "peer.login.expire"}, DashboardLogin: {"Dashboard login", "dashboard.login"}, + IntegrationCreated: {"Integration created", "integration.create"}, + IntegrationUpdated: {"Integration updated", "integration.update"}, + IntegrationDeleted: {"Integration deleted", "integration.delete"}, + AccountPeerApprovalEnabled: {"Account peer approval enabled", "account.setting.peer.approval.enable"}, + AccountPeerApprovalDisabled: {"Account peer approval disabled", "account.setting.peer.approval.disable"}, + PeerApproved: {"Peer approved", "peer.approve"}, + PeerApprovalRevoked: {"Peer approval revoked", "peer.approval.revoke"}, + TransferredOwnerRole: {"Transferred owner role", "transferred.owner.role"}, } // StringCode returns a string code of the activity diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go index a5130b0c5..b54db5276 100644 --- a/management/server/activity/sqlite/sqlite.go +++ b/management/server/activity/sqlite/sqlite.go @@ -14,7 +14,7 @@ import ( ) const ( - //eventSinkDB is the default name of the events database + // eventSinkDB is the default name of the events database eventSinkDB = "events.db" createTableQuery = "CREATE TABLE IF NOT EXISTS events " + "(id INTEGER PRIMARY KEY AUTOINCREMENT, " + @@ -28,22 +28,46 @@ const ( creatTableDeletedUsersQuery = `CREATE TABLE IF NOT EXISTS deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT);` selectDescQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta - FROM events - LEFT JOIN deleted_users i ON events.initiator_id = i.id - LEFT JOIN deleted_users t ON events.target_id = t.id + FROM events + LEFT JOIN ( + SELECT id, MAX(name) as name, MAX(email) as email + FROM deleted_users + GROUP BY id + ) i ON events.initiator_id = i.id + LEFT JOIN ( + SELECT id, MAX(name) as name, MAX(email) as email + FROM deleted_users + GROUP BY id + ) t ON events.target_id = t.id WHERE account_id = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?;` selectAscQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta - FROM events - LEFT JOIN deleted_users i ON events.initiator_id = i.id - LEFT JOIN deleted_users t ON events.target_id = t.id + FROM events + LEFT JOIN ( + SELECT id, MAX(name) as name, MAX(email) as email + FROM deleted_users + GROUP BY id + ) i ON events.initiator_id = i.id + LEFT JOIN ( + SELECT id, MAX(name) as name, MAX(email) as email + FROM deleted_users + GROUP BY id + ) t ON events.target_id = t.id WHERE account_id = ? ORDER BY timestamp ASC LIMIT ? OFFSET ?;` insertQuery = "INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) " + "VALUES(?, ?, ?, ?, ?, ?)" + /* + TODO: + The insert should avoid duplicated IDs in the table. So the query should be changes to something like: + `INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, name = EXCLUDED.name;` + For this to work we have to set the id column as primary key. But this is not possible because the id column is not unique + and some selfhosted deployments might have duplicates already so we need to clean the table first. + */ + insertDeleteUserQuery = `INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?)` fallbackName = "unknown" diff --git a/management/server/dns.go b/management/server/dns.go index f90a5e9f2..820a5431f 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -10,6 +10,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" ) @@ -47,8 +48,8 @@ func (am *DefaultAccountManager) GetDNSSettings(accountID string, userID string) return nil, err } - if !user.IsAdmin() { - return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view DNS settings") + if !user.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view DNS settings") } dnsSettings := account.DNSSettings.Copy() return &dnsSettings, nil @@ -69,8 +70,8 @@ func (am *DefaultAccountManager) SaveDNSSettings(accountID string, userID string return err } - if !user.IsAdmin() { - return status.Errorf(status.PermissionDenied, "only admins are allowed to update DNS settings") + if !user.HasAdminPower() { + return status.Errorf(status.PermissionDenied, "only users with admin power are allowed to update DNS settings") } if dnsSettingsToSave == nil { @@ -96,14 +97,14 @@ func (am *DefaultAccountManager) SaveDNSSettings(accountID string, userID string for _, id := range addedGroups { group := account.GetGroup(id) meta := map[string]any{"group": group.Name, "group_id": group.ID} - am.storeEvent(userID, accountID, accountID, activity.GroupAddedToDisabledManagementGroups, meta) + am.StoreEvent(userID, accountID, accountID, activity.GroupAddedToDisabledManagementGroups, meta) } removedGroups := difference(oldSettings.DisabledManagementGroups, dnsSettingsToSave.DisabledManagementGroups) for _, id := range removedGroups { group := account.GetGroup(id) meta := map[string]any{"group": group.Name, "group_id": group.ID} - am.storeEvent(userID, accountID, accountID, activity.GroupRemovedFromDisabledManagementGroups, meta) + am.StoreEvent(userID, accountID, accountID, activity.GroupRemovedFromDisabledManagementGroups, meta) } am.updateAccountPeers(account) @@ -200,7 +201,7 @@ func getPeerNSGroups(account *Account, peerID string) []*nbdns.NameServerGroup { } // peerIsNameserver returns true if the peer is a nameserver for a nsGroup -func peerIsNameserver(peer *Peer, nsGroup *nbdns.NameServerGroup) bool { +func peerIsNameserver(peer *nbpeer.Peer, nsGroup *nbdns.NameServerGroup) bool { for _, ns := range nsGroup.NameServers { if peer.IP.Equal(ns.IP.AsSlice()) { return true diff --git a/management/server/dns_test.go b/management/server/dns_test.go index a2c9d3aa2..bff0c9878 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -8,6 +8,7 @@ import ( "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/activity" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" ) @@ -186,15 +187,17 @@ func TestGetNetworkMap_DNSConfigSync(t *testing.T) { } func createDNSManager(t *testing.T) (*DefaultAccountManager, error) { + t.Helper() store, err := createDNSStore(t) if err != nil { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.test", eventStore, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, false) } func createDNSStore(t *testing.T) (Store, error) { + t.Helper() dataDir := t.TempDir() store, err := NewStoreFromJson(dataDir, nil) if err != nil { @@ -205,10 +208,11 @@ func createDNSStore(t *testing.T) (Store, error) { } func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error) { - peer1 := &Peer{ + t.Helper() + peer1 := &nbpeer.Peer{ Key: dnsPeer1Key, Name: "test-host1@netbird.io", - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host1@netbird.io", GoOS: "linux", Kernel: "Linux", @@ -220,10 +224,10 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, erro }, DNSLabel: dnsPeer1Key, } - peer2 := &Peer{ + peer2 := &nbpeer.Peer{ Key: dnsPeer2Key, Name: "test-host2@netbird.io", - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host2@netbird.io", GoOS: "linux", Kernel: "Linux", diff --git a/management/server/ephemeral.go b/management/server/ephemeral.go index 0e76e58ac..9d70a05d1 100644 --- a/management/server/ephemeral.go +++ b/management/server/ephemeral.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server/activity" + nbpeer "github.com/netbirdio/netbird/management/server/peer" ) const ( @@ -72,7 +73,7 @@ func (e *EphemeralManager) Stop() { // OnPeerConnected remove the peer from the linked list of ephemeral peers. Because it has been called when the peer // is active the manager will not delete it while it is active. -func (e *EphemeralManager) OnPeerConnected(peer *Peer) { +func (e *EphemeralManager) OnPeerConnected(peer *nbpeer.Peer) { if !peer.Ephemeral { return } @@ -93,7 +94,7 @@ func (e *EphemeralManager) OnPeerConnected(peer *Peer) { // OnPeerDisconnected add the peer to the linked list of ephemeral peers. Because of the peer // is inactive it will be deleted after the ephemeralLifeTime period. -func (e *EphemeralManager) OnPeerDisconnected(peer *Peer) { +func (e *EphemeralManager) OnPeerDisconnected(peer *nbpeer.Peer) { if !peer.Ephemeral { return } diff --git a/management/server/ephemeral_test.go b/management/server/ephemeral_test.go index d271e5fca..3e36335e3 100644 --- a/management/server/ephemeral_test.go +++ b/management/server/ephemeral_test.go @@ -4,6 +4,8 @@ import ( "fmt" "testing" "time" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" ) type MockStore struct { @@ -124,7 +126,7 @@ func seedPeers(store *MockStore, numberOfPeers int, numberOfEphemeralPeers int) for i := 0; i < numberOfPeers; i++ { peerId := fmt.Sprintf("peer_%d", i) - p := &Peer{ + p := &nbpeer.Peer{ ID: peerId, Ephemeral: false, } @@ -133,7 +135,7 @@ func seedPeers(store *MockStore, numberOfPeers int, numberOfEphemeralPeers int) for i := 0; i < numberOfEphemeralPeers; i++ { peerId := fmt.Sprintf("ephemeral_peer_%d", i) - p := &Peer{ + p := &nbpeer.Peer{ ID: peerId, Ephemeral: true, } diff --git a/management/server/event.go b/management/server/event.go index 6ada88d6f..450d1c00d 100644 --- a/management/server/event.go +++ b/management/server/event.go @@ -36,7 +36,7 @@ func (am *DefaultAccountManager) GetEvents(accountID, userID string) ([]*activit return filtered, nil } -func (am *DefaultAccountManager) storeEvent(initiatorID, targetID, accountID string, activityID activity.Activity, +func (am *DefaultAccountManager) StoreEvent(initiatorID, targetID, accountID string, activityID activity.Activity, meta map[string]any) { go func() { diff --git a/management/server/event_test.go b/management/server/event_test.go index 320728eef..401c80759 100644 --- a/management/server/event_test.go +++ b/management/server/event_test.go @@ -11,6 +11,7 @@ import ( func generateAndStoreEvents(t *testing.T, manager *DefaultAccountManager, typ activity.Activity, initiatorID, targetID, accountID string, count int) { + t.Helper() for i := 0; i < count; i++ { _, err := manager.eventStore.Save(&activity.Event{ Timestamp: time.Now().UTC(), diff --git a/management/server/file_store.go b/management/server/file_store.go index 73c52927e..818d9a4db 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -10,6 +10,7 @@ import ( "github.com/rs/xid" log "github.com/sirupsen/logrus" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/telemetry" @@ -204,7 +205,7 @@ func restore(file string) (*FileStore, error) { // Set the Peer.ID to the newly generated value. // Replace all the mentions of Peer.Key as ID (groups and routes). // Swap Peer.Key with Peer.ID in the Account.Peers map. - migrationPeers := make(map[string]*Peer) // key to Peer + migrationPeers := make(map[string]*nbpeer.Peer) // key to Peer for key, peer := range account.Peers { // set LastLogin for the peers that were onboarded before the peer login expiration feature if peer.LastLogin.IsZero() { @@ -351,6 +352,41 @@ func (s *FileStore) SaveAccount(account *Account) error { return s.persist(s.storeFile) } +func (s *FileStore) DeleteAccount(account *Account) error { + s.mux.Lock() + defer s.mux.Unlock() + + if account.Id == "" { + return status.Errorf(status.InvalidArgument, "account id should not be empty") + } + + for keyID := range account.SetupKeys { + delete(s.SetupKeyID2AccountID, strings.ToUpper(keyID)) + } + + // enforce peer to account index and delete peer to route indexes for rebuild + for _, peer := range account.Peers { + delete(s.PeerKeyID2AccountID, peer.Key) + delete(s.PeerID2AccountID, peer.ID) + } + + for _, user := range account.Users { + for _, pat := range user.PATs { + delete(s.TokenID2UserID, pat.ID) + delete(s.HashedPAT2TokenID, pat.HashedToken) + } + delete(s.UserID2AccountID, user.Id) + } + + if account.DomainCategory == PrivateCategory && account.IsDomainPrimaryAccount { + delete(s.PrivateDomain2AccountID, account.Domain) + } + + delete(s.Accounts, account.Id) + + return s.persist(s.storeFile) +} + // DeleteHashedPAT2TokenIDIndex removes an entry from the indexing map HashedPAT2TokenID func (s *FileStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error { s.mux.Lock() @@ -571,7 +607,7 @@ func (s *FileStore) SaveInstallationID(ID string) error { // SavePeerStatus stores the PeerStatus in memory. It doesn't attempt to persist data to speed up things. // PeerStatus will be saved eventually when some other changes occur. -func (s *FileStore) SavePeerStatus(accountID, peerID string, peerStatus PeerStatus) error { +func (s *FileStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer.PeerStatus) error { s.mux.Lock() defer s.mux.Unlock() diff --git a/management/server/file_store_test.go b/management/server/file_store_test.go index 705e9f149..ef9799378 100644 --- a/management/server/file_store_test.go +++ b/management/server/file_store_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/util" ) @@ -35,7 +36,7 @@ func TestStalePeerIndices(t *testing.T) { peerID := "some_peer" peerKey := "some_peer_key" - account.Peers[peerID] = &Peer{ + account.Peers[peerID] = &nbpeer.Peer{ ID: peerID, Key: peerKey, } @@ -89,13 +90,13 @@ func TestSaveAccount(t *testing.T) { account := newAccountWithId("account_id", "testuser", "") setupKey := GenerateDefaultSetupKey() account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &Peer{ + account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, - Meta: PeerSystemMeta{}, + Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", - Status: &PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } // SaveAccount should trigger persist @@ -121,17 +122,71 @@ func TestSaveAccount(t *testing.T) { } } +func TestDeleteAccount(t *testing.T) { + storeDir := t.TempDir() + storeFile := filepath.Join(storeDir, "store.json") + err := util.CopyFileContents("testdata/store.json", storeFile) + if err != nil { + t.Fatal(err) + } + + store, err := NewFileStore(storeDir, nil) + if err != nil { + t.Fatal(err) + } + var account *Account + for _, a := range store.Accounts { + account = a + break + } + + require.NotNil(t, account, "failed to restore a FileStore file and get at least one account") + + err = store.DeleteAccount(account) + require.NoError(t, err, "failed to delete account, error: %v", err) + + _, ok := store.Accounts[account.Id] + require.False(t, ok, "failed to delete account") + + for id := range account.Users { + _, ok := store.UserID2AccountID[id] + assert.False(t, ok, "failed to delete UserID2AccountID index") + for _, pat := range account.Users[id].PATs { + _, ok := store.HashedPAT2TokenID[pat.HashedToken] + assert.False(t, ok, "failed to delete HashedPAT2TokenID index") + _, ok = store.TokenID2UserID[pat.ID] + assert.False(t, ok, "failed to delete TokenID2UserID index") + } + } + + for _, p := range account.Peers { + _, ok := store.PeerKeyID2AccountID[p.Key] + assert.False(t, ok, "failed to delete PeerKeyID2AccountID index") + _, ok = store.PeerID2AccountID[p.ID] + assert.False(t, ok, "failed to delete PeerID2AccountID index") + } + + for id := range account.SetupKeys { + _, ok := store.SetupKeyID2AccountID[id] + assert.False(t, ok, "failed to delete SetupKeyID2AccountID index") + } + + _, ok = store.PrivateDomain2AccountID[account.Domain] + assert.False(t, ok, "failed to delete PrivateDomain2AccountID index") + +} + func TestStore(t *testing.T) { store := newStore(t) account := newAccountWithId("account_id", "testuser", "") - account.Peers["testpeer"] = &Peer{ + account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, - Meta: PeerSystemMeta{}, + Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", - Status: &PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } account.Groups["all"] = &Group{ ID: "all", @@ -546,19 +601,19 @@ func TestFileStore_SavePeerStatus(t *testing.T) { } // save status of non-existing peer - newStatus := PeerStatus{Connected: true, LastSeen: time.Now().UTC()} + newStatus := nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()} err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus) assert.Error(t, err) // save new status of existing peer - account.Peers["testpeer"] = &Peer{ + account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", ID: "testpeer", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, - Meta: PeerSystemMeta{}, + Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", - Status: &PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, + Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, } err = store.SaveAccount(account) @@ -580,6 +635,7 @@ func TestFileStore_SavePeerStatus(t *testing.T) { } func newStore(t *testing.T) *FileStore { + t.Helper() store, err := NewFileStore(t.TempDir(), nil) if err != nil { t.Errorf("failed creating a new store") diff --git a/management/server/group.go b/management/server/group.go index d626c3538..cf7320c29 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -101,7 +101,7 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *G removedPeers = difference(oldGroup.Peers, newGroup.Peers) } else { addedPeers = append(addedPeers, newGroup.Peers...) - am.storeEvent(userID, newGroup.ID, accountID, activity.GroupCreated, newGroup.EventMeta()) + am.StoreEvent(userID, newGroup.ID, accountID, activity.GroupCreated, newGroup.EventMeta()) } for _, p := range addedPeers { @@ -110,7 +110,7 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *G log.Errorf("peer %s not found under account %s while saving group", p, accountID) continue } - am.storeEvent(userID, peer.ID, accountID, activity.GroupAddedToPeer, + am.StoreEvent(userID, peer.ID, accountID, activity.GroupAddedToPeer, map[string]any{ "group": newGroup.Name, "group_id": newGroup.ID, "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()), @@ -123,7 +123,7 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *G log.Errorf("peer %s not found under account %s while saving group", p, accountID) continue } - am.storeEvent(userID, peer.ID, accountID, activity.GroupRemovedFromPeer, + am.StoreEvent(userID, peer.ID, accountID, activity.GroupRemovedFromPeer, map[string]any{ "group": newGroup.Name, "group_id": newGroup.ID, "peer_ip": peer.IP.String(), "peer_fqdn": peer.FQDN(am.GetDNSDomain()), @@ -163,9 +163,15 @@ func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string) return nil } - // check integration link + // disable a deleting integration group if the initiator is not an admin service user if g.Issued == GroupIssuedIntegration { - return &GroupLinkError{GroupIssuedIntegration, g.IntegrationReference.String()} + executingUser := account.Users[userId] + if executingUser == nil { + return status.Errorf(status.NotFound, "user not found") + } + if executingUser.Role != UserRoleAdmin || !executingUser.IsServiceUser { + return status.Errorf(status.PermissionDenied, "only service users with admin power can delete integration group") + } } // check route links @@ -235,7 +241,7 @@ func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string) return err } - am.storeEvent(userId, groupID, accountId, activity.GroupDeleted, g.EventMeta()) + am.StoreEvent(userId, groupID, accountId, activity.GroupDeleted, g.EventMeta()) am.updateAccountPeers(account) diff --git a/management/server/group_test.go b/management/server/group_test.go index 5db0ca900..e2051a656 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -1,9 +1,11 @@ package server import ( + "errors" "testing" nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/route" ) @@ -55,19 +57,28 @@ func TestDefaultAccountManager_DeleteGroup(t *testing.T) { { "integration", "grp-for-integration", - "integration", + "only service users with admin power can delete integration group", }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - err = am.DeleteGroup(account.Id, "", testCase.groupID) + err = am.DeleteGroup(account.Id, groupAdminUserID, testCase.groupID) if err == nil { t.Errorf("delete %s group successfully", testCase.groupID) return } - gErr, ok := err.(*GroupLinkError) + var sErr *status.Error + if errors.As(err, &sErr) { + if sErr.Message != testCase.expectedReason { + t.Errorf("invalid error case: %s, expected: %s", sErr.Message, testCase.expectedReason) + } + return + } + + var gErr *GroupLinkError + ok := errors.As(err, &gErr) if !ok { t.Error("invalid error type") return diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index f32f6347a..8d3d82661 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -17,6 +17,7 @@ import ( "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/jwtclaims" + nbpeer "github.com/netbirdio/netbird/management/server/peer" internalStatus "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/telemetry" ) @@ -196,7 +197,7 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi } } -func (s *GRPCServer) cancelPeerRoutines(peer *Peer) { +func (s *GRPCServer) cancelPeerRoutines(peer *nbpeer.Peer) { s.peersUpdateManager.CloseChannel(peer.ID) s.turnCredentialsManager.CancelRefresh(peer.ID) _ = s.accountManager.MarkPeerConnected(peer.Key, false) @@ -243,8 +244,8 @@ func mapError(err error) error { return status.Errorf(codes.Internal, "failed handling request") } -func extractPeerMeta(loginReq *proto.LoginRequest) PeerSystemMeta { - return PeerSystemMeta{ +func extractPeerMeta(loginReq *proto.LoginRequest) nbpeer.PeerSystemMeta { + return nbpeer.PeerSystemMeta{ Hostname: loginReq.GetMeta().GetHostname(), GoOS: loginReq.GetMeta().GetGoOS(), Kernel: loginReq.GetMeta().GetKernel(), @@ -413,7 +414,7 @@ func toWiretrusteeConfig(config *Config, turnCredentials *TURNCredentials) *prot } } -func toPeerConfig(peer *Peer, network *Network, dnsName string) *proto.PeerConfig { +func toPeerConfig(peer *nbpeer.Peer, network *Network, dnsName string) *proto.PeerConfig { netmask, _ := network.Net.Mask.Size() fqdn := peer.FQDN(dnsName) return &proto.PeerConfig{ @@ -423,7 +424,7 @@ func toPeerConfig(peer *Peer, network *Network, dnsName string) *proto.PeerConfi } } -func toRemotePeerConfig(peers []*Peer, dnsName string) []*proto.RemotePeerConfig { +func toRemotePeerConfig(peers []*nbpeer.Peer, dnsName string) []*proto.RemotePeerConfig { remotePeers := []*proto.RemotePeerConfig{} for _, rPeer := range peers { fqdn := rPeer.FQDN(dnsName) @@ -437,7 +438,7 @@ func toRemotePeerConfig(peers []*Peer, dnsName string) []*proto.RemotePeerConfig return remotePeers } -func toSyncResponse(config *Config, peer *Peer, turnCredentials *TURNCredentials, networkMap *NetworkMap, dnsName string) *proto.SyncResponse { +func toSyncResponse(config *Config, peer *nbpeer.Peer, turnCredentials *TURNCredentials, networkMap *NetworkMap, dnsName string) *proto.SyncResponse { wtConfig := toWiretrusteeConfig(config, turnCredentials) pConfig := toPeerConfig(peer, networkMap.Network, dnsName) @@ -477,7 +478,7 @@ func (s *GRPCServer) IsHealthy(ctx context.Context, req *proto.Empty) (*proto.Em } // sendInitialSync sends initial proto.SyncResponse to the peer requesting synchronization -func (s *GRPCServer) sendInitialSync(peerKey wgtypes.Key, peer *Peer, networkMap *NetworkMap, srv proto.ManagementService_SyncServer) error { +func (s *GRPCServer) sendInitialSync(peerKey wgtypes.Key, peer *nbpeer.Peer, networkMap *NetworkMap, srv proto.ManagementService_SyncServer) error { // make secret time based TURN credentials optional var turnCredentials *TURNCredentials if s.config.TURNConfig.TimeBasedCredentials { diff --git a/management/server/http/accounts_handler.go b/management/server/http/accounts_handler.go index a5d7a9501..c2751abd4 100644 --- a/management/server/http/accounts_handler.go +++ b/management/server/http/accounts_handler.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" "github.com/netbirdio/netbird/management/server/jwtclaims" @@ -40,7 +41,7 @@ func (h *AccountsHandler) GetAllAccounts(w http.ResponseWriter, r *http.Request) return } - if !user.IsAdmin() { + if !user.HasAdminPower() { util.WriteError(status.Errorf(status.PermissionDenied, "the user has no permission to access account data"), w) return } @@ -77,6 +78,10 @@ func (h *AccountsHandler) UpdateAccount(w http.ResponseWriter, r *http.Request) PeerLoginExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerLoginExpiration)), } + if req.Settings.Extra != nil { + settings.Extra = &account.ExtraSettings{PeerApprovalEnabled: *req.Settings.Extra.PeerApprovalEnabled} + } + if req.Settings.JwtGroupsEnabled != nil { settings.JWTGroupsEnabled = *req.Settings.JwtGroupsEnabled } @@ -98,15 +103,45 @@ func (h *AccountsHandler) UpdateAccount(w http.ResponseWriter, r *http.Request) util.WriteJSONObject(w, &resp) } +// DeleteAccount is a HTTP DELETE handler to delete an account +func (h *AccountsHandler) DeleteAccount(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w) + return + } + + claims := h.claimsExtractor.FromRequestContext(r) + vars := mux.Vars(r) + targetAccountID := vars["accountId"] + if len(targetAccountID) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid account ID"), w) + return + } + + err := h.accountManager.DeleteAccount(targetAccountID, claims.UserId) + if err != nil { + util.WriteError(err, w) + return + } + + util.WriteJSONObject(w, emptyObject{}) +} + func toAccountResponse(account *server.Account) *api.Account { + settings := api.AccountSettings{ + PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()), + PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled, + GroupsPropagationEnabled: &account.Settings.GroupsPropagationEnabled, + JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled, + JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName, + } + + if account.Settings.Extra != nil { + settings.Extra = &api.AccountExtraSettings{PeerApprovalEnabled: &account.Settings.Extra.PeerApprovalEnabled} + } + return &api.Account{ - Id: account.Id, - Settings: api.AccountSettings{ - PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()), - PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled, - GroupsPropagationEnabled: &account.Settings.GroupsPropagationEnabled, - JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled, - JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName, - }, + Id: account.Id, + Settings: settings, } } diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index a0a64fd98..2b8d614f6 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -66,9 +66,18 @@ components: description: Name of the claim from which we extract groups names to add it to account groups. type: string example: "roles" + extra: + $ref: '#/components/schemas/AccountExtraSettings' required: - peer_login_expiration_enabled - peer_login_expiration + AccountExtraSettings: + type: object + properties: + peer_approval_enabled: + description: (Cloud only) Enables or disables peer approval globally. If enabled, all peers added will be in pending state until approved by an admin. + type: boolean + example: true AccountRequest: type: object properties: @@ -213,11 +222,15 @@ components: login_expiration_enabled: type: boolean example: false + approval_required: + description: (Cloud only) Indicates whether peer needs approval + type: boolean + example: true required: - name - ssh_enabled - login_expiration_enabled - Peer: + PeerBase: allOf: - $ref: '#/components/schemas/PeerMinimum' - type: object @@ -281,6 +294,10 @@ components: type: string format: date-time example: 2023-05-05T09:00:35.477782Z + approval_required: + description: (Cloud only) Indicates whether peer needs approval + type: boolean + example: true required: - ip - connected @@ -294,6 +311,50 @@ components: - login_expiration_enabled - login_expired - last_login + AccessiblePeer: + allOf: + - $ref: '#/components/schemas/PeerMinimum' + - type: object + properties: + ip: + description: Peer's IP address + type: string + example: 10.64.0.1 + dns_label: + description: 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 + type: string + example: stage-host-1.netbird.cloud + user_id: + description: User ID of the user that enrolled this peer + type: string + example: google-oauth2|277474792786460067937 + required: + - ip + - dns_label + - user_id + Peer: + allOf: + - $ref: '#/components/schemas/PeerBase' + - type: object + properties: + accessible_peers: + description: List of accessible peers + type: array + items: + $ref: '#/components/schemas/AccessiblePeer' + required: + - accessible_peers + PeerBatch: + allOf: + - $ref: '#/components/schemas/PeerBase' + - type: object + properties: + accessible_peers_count: + description: Number of accessible peers + type: integer + example: 5 + required: + - accessible_peers_count SetupKey: type: object properties: @@ -1030,6 +1091,32 @@ paths: '500': "$ref": "#/components/responses/internal_error" /api/accounts/{accountId}: + delete: + summary: Delete an Account + description: Deletes an account and all its resources. Only administrators and account owners can delete accounts. + tags: [ Accounts ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: accountId + required: true + schema: + type: string + description: The unique identifier of an account + responses: + '200': + description: Delete account status code + content: { } + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" put: summary: Update an Account description: Update information about an account @@ -1364,7 +1451,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Peer' + $ref: '#/components/schemas/PeerBatch' '400': "$ref": "#/components/responses/bad_request" '401': diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index ddf8ce65f..8e41f5672 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -117,6 +117,24 @@ const ( UserStatusInvited UserStatus = "invited" ) +// AccessiblePeer defines model for AccessiblePeer. +type AccessiblePeer 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"` + + // Id Peer ID + Id string `json:"id"` + + // Ip Peer's IP address + Ip string `json:"ip"` + + // Name Peer's hostname + Name string `json:"name"` + + // UserId User ID of the user that enrolled this peer + UserId string `json:"user_id"` +} + // Account defines model for Account. type Account struct { // Id Account ID @@ -124,6 +142,12 @@ type Account struct { Settings AccountSettings `json:"settings"` } +// AccountExtraSettings defines model for AccountExtraSettings. +type AccountExtraSettings struct { + // PeerApprovalEnabled (Cloud only) Enables or disables peer approval globally. If enabled, all peers added will be in pending state until approved by an admin. + PeerApprovalEnabled *bool `json:"peer_approval_enabled,omitempty"` +} + // AccountRequest defines model for AccountRequest. type AccountRequest struct { Settings AccountSettings `json:"settings"` @@ -131,6 +155,8 @@ type AccountRequest struct { // AccountSettings defines model for AccountSettings. type AccountSettings struct { + Extra *AccountExtraSettings `json:"extra,omitempty"` + // GroupsPropagationEnabled Allows propagate the new user auto groups to peers that belongs to the user GroupsPropagationEnabled *bool `json:"groups_propagation_enabled,omitempty"` @@ -302,6 +328,123 @@ type NameserverGroupRequest struct { // Peer defines model for Peer. type Peer struct { + // AccessiblePeers List of accessible peers + AccessiblePeers []AccessiblePeer `json:"accessible_peers"` + + // ApprovalRequired (Cloud only) Indicates whether peer needs approval + ApprovalRequired *bool `json:"approval_required,omitempty"` + + // Connected Peer to Management connection status + Connected bool `json:"connected"` + + // 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"` + + // Groups Groups that the peer belongs to + Groups []GroupMinimum `json:"groups"` + + // Hostname Hostname of the machine + Hostname string `json:"hostname"` + + // Id Peer ID + Id string `json:"id"` + + // Ip Peer's IP address + Ip string `json:"ip"` + + // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. + LastLogin time.Time `json:"last_login"` + + // LastSeen Last time peer connected to Netbird's management service + LastSeen time.Time `json:"last_seen"` + + // LoginExpirationEnabled Indicates whether peer login expiration has been enabled or not + LoginExpirationEnabled bool `json:"login_expiration_enabled"` + + // LoginExpired Indicates whether peer's login expired or not + LoginExpired bool `json:"login_expired"` + + // Name Peer's hostname + Name string `json:"name"` + + // Os Peer's operating system and version + Os string `json:"os"` + + // SshEnabled Indicates whether SSH server is enabled on this peer + SshEnabled bool `json:"ssh_enabled"` + + // UiVersion Peer's desktop UI version + UiVersion *string `json:"ui_version,omitempty"` + + // UserId User ID of the user that enrolled this peer + UserId *string `json:"user_id,omitempty"` + + // Version Peer's daemon or cli version + Version string `json:"version"` +} + +// PeerBase defines model for PeerBase. +type PeerBase struct { + // ApprovalRequired (Cloud only) Indicates whether peer needs approval + ApprovalRequired *bool `json:"approval_required,omitempty"` + + // Connected Peer to Management connection status + Connected bool `json:"connected"` + + // 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"` + + // Groups Groups that the peer belongs to + Groups []GroupMinimum `json:"groups"` + + // Hostname Hostname of the machine + Hostname string `json:"hostname"` + + // Id Peer ID + Id string `json:"id"` + + // Ip Peer's IP address + Ip string `json:"ip"` + + // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. + LastLogin time.Time `json:"last_login"` + + // LastSeen Last time peer connected to Netbird's management service + LastSeen time.Time `json:"last_seen"` + + // LoginExpirationEnabled Indicates whether peer login expiration has been enabled or not + LoginExpirationEnabled bool `json:"login_expiration_enabled"` + + // LoginExpired Indicates whether peer's login expired or not + LoginExpired bool `json:"login_expired"` + + // Name Peer's hostname + Name string `json:"name"` + + // Os Peer's operating system and version + Os string `json:"os"` + + // SshEnabled Indicates whether SSH server is enabled on this peer + SshEnabled bool `json:"ssh_enabled"` + + // UiVersion Peer's desktop UI version + UiVersion *string `json:"ui_version,omitempty"` + + // UserId User ID of the user that enrolled this peer + UserId *string `json:"user_id,omitempty"` + + // Version Peer's daemon or cli version + Version string `json:"version"` +} + +// PeerBatch defines model for PeerBatch. +type PeerBatch struct { + // AccessiblePeersCount Number of accessible peers + AccessiblePeersCount int `json:"accessible_peers_count"` + + // ApprovalRequired (Cloud only) Indicates whether peer needs approval + ApprovalRequired *bool `json:"approval_required,omitempty"` + // Connected Peer to Management connection status Connected bool `json:"connected"` @@ -362,6 +505,8 @@ type PeerMinimum struct { // PeerRequest defines model for PeerRequest. type PeerRequest struct { + // ApprovalRequired (Cloud only) Indicates whether peer needs approval + ApprovalRequired *bool `json:"approval_required,omitempty"` LoginExpirationEnabled bool `json:"login_expiration_enabled"` Name string `json:"name"` SshEnabled bool `json:"ssh_enabled"` diff --git a/management/server/http/groups_handler_test.go b/management/server/http/groups_handler_test.go index aad03d50b..5b47b1208 100644 --- a/management/server/http/groups_handler_test.go +++ b/management/server/http/groups_handler_test.go @@ -19,10 +19,11 @@ import ( "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" ) -var TestPeers = map[string]*server.Peer{ +var TestPeers = map[string]*nbpeer.Peer{ "A": {Key: "A", ID: "peer-A-ID", IP: net.ParseIP("100.100.100.100")}, "B": {Key: "B", ID: "peer-B-ID", IP: net.ParseIP("200.200.200.200")}, } @@ -230,7 +231,7 @@ func TestWriteGroup(t *testing.T) { expectedBody: false, }, { - name: "Write Group PUT not not change Issue", + name: "Write Group PUT not change Issue", requestType: http.MethodPut, requestPath: "/api/groups/id-jwt-group", requestBody: bytes.NewBuffer( diff --git a/management/server/http/handler.go b/management/server/http/handler.go index c589512e5..8c77d27dc 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -7,6 +7,7 @@ import ( "github.com/rs/cors" "github.com/netbirdio/management-integrations/integrations" + s "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/http/middleware" "github.com/netbirdio/netbird/management/server/jwtclaims" @@ -105,6 +106,7 @@ func APIHandler(accountManager s.AccountManager, jwtValidator jwtclaims.JWTValid func (apiHandler *apiHandler) addAccountsEndpoint() { accountsHandler := NewAccountsHandler(apiHandler.AccountManager, apiHandler.AuthCfg) apiHandler.Router.HandleFunc("/accounts/{accountId}", accountsHandler.UpdateAccount).Methods("PUT", "OPTIONS") + apiHandler.Router.HandleFunc("/accounts/{accountId}", accountsHandler.DeleteAccount).Methods("DELETE", "OPTIONS") apiHandler.Router.HandleFunc("/accounts", accountsHandler.GetAllAccounts).Methods("GET", "OPTIONS") } diff --git a/management/server/http/middleware/access_control.go b/management/server/http/middleware/access_control.go index fc362ea80..2f9374fc1 100644 --- a/management/server/http/middleware/access_control.go +++ b/management/server/http/middleware/access_control.go @@ -33,6 +33,8 @@ func NewAccessControl(audience, userIDClaim string, getUser GetUser) *AccessCont } } +var tokenPathRegexp = regexp.MustCompile(`^.*/api/users/.*/tokens.*$`) + // Handler method of the middleware which forbids all modify requests for non admin users // It also adds func (a *AccessControl) Handler(h http.Handler) http.Handler { @@ -41,6 +43,7 @@ func (a *AccessControl) Handler(h http.Handler) http.Handler { user, err := a.getUser(claims) if err != nil { + log.Errorf("failed to get user from claims: %s", err) util.WriteError(status.Errorf(status.Unauthorized, "invalid JWT"), w) return } @@ -50,23 +53,17 @@ func (a *AccessControl) Handler(h http.Handler) http.Handler { return } - if !user.IsAdmin() { + if !user.HasAdminPower() { switch r.Method { case http.MethodDelete, http.MethodPost, http.MethodPatch, http.MethodPut: - ok, err := regexp.MatchString(`^.*/api/users/.*/tokens.*$`, r.URL.Path) - if err != nil { - log.Debugf("regex failed") - util.WriteError(status.Errorf(status.Internal, ""), w) - return - } - if ok { + if tokenPathRegexp.MatchString(r.URL.Path) { log.Debugf("valid Path") h.ServeHTTP(w, r) return } - util.WriteError(status.Errorf(status.PermissionDenied, "only admin can perform this operation"), w) + util.WriteError(status.Errorf(status.PermissionDenied, "only users with admin power can perform this operation"), w) return } } diff --git a/management/server/http/peers_handler.go b/management/server/http/peers_handler.go index a485d6ccf..734785e30 100644 --- a/management/server/http/peers_handler.go +++ b/management/server/http/peers_handler.go @@ -11,6 +11,7 @@ import ( "github.com/netbirdio/netbird/management/server/http/api" "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" ) @@ -31,17 +32,12 @@ func NewPeersHandler(accountManager server.AccountManager, authCfg AuthCfg) *Pee } } -func (h *PeersHandler) checkPeerStatus(peer *server.Peer) (*server.Peer, error) { +func (h *PeersHandler) checkPeerStatus(peer *nbpeer.Peer) (*nbpeer.Peer, error) { peerToReturn := peer.Copy() if peer.Status.Connected { - statuses, err := h.accountManager.GetAllConnectedPeers() - if err != nil { - return peerToReturn, err - } - // Although we have online status in store we do not yet have an updated channel so have to show it as disconnected // This may happen after server restart when not all peers are yet connected - if _, connected := statuses[peerToReturn.ID]; !connected { + if !h.accountManager.HasConnectedChannel(peer.ID) { peerToReturn.Status.Connected = false } } @@ -61,8 +57,14 @@ func (h *PeersHandler) getPeer(account *server.Account, peerID, userID string, w util.WriteError(err, w) return } + dnsDomain := h.accountManager.GetDNSDomain() - util.WriteJSONObject(w, toPeerResponse(peerToReturn, account, h.accountManager.GetDNSDomain())) + groupsInfo := toGroupsInfo(account.Groups, peer.ID) + + netMap := account.GetPeerNetworkMap(peerID, h.accountManager.GetDNSDomain()) + accessiblePeers := toAccessiblePeers(netMap, dnsDomain) + + util.WriteJSONObject(w, toSinglePeerResponse(peerToReturn, groupsInfo, dnsDomain, accessiblePeers)) } func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, peerID string, w http.ResponseWriter, r *http.Request) { @@ -73,15 +75,26 @@ func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, pe return } - update := &server.Peer{ID: peerID, SSHEnabled: req.SshEnabled, Name: req.Name, + update := &nbpeer.Peer{ID: peerID, SSHEnabled: req.SshEnabled, Name: req.Name, LoginExpirationEnabled: req.LoginExpirationEnabled} + + if req.ApprovalRequired != nil { + update.Status = &nbpeer.PeerStatus{RequiresApproval: *req.ApprovalRequired} + } + peer, err := h.accountManager.UpdatePeer(account.Id, user.Id, update) if err != nil { util.WriteError(err, w) return } dnsDomain := h.accountManager.GetDNSDomain() - util.WriteJSONObject(w, toPeerResponse(peer, account, dnsDomain)) + + groupMinimumInfo := toGroupsInfo(account.Groups, peer.ID) + + netMap := account.GetPeerNetworkMap(peerID, h.accountManager.GetDNSDomain()) + accessiblePeers := toAccessiblePeers(netMap, dnsDomain) + + util.WriteJSONObject(w, toSinglePeerResponse(peer, groupMinimumInfo, dnsDomain, accessiblePeers)) } func (h *PeersHandler) deletePeer(accountID, userID string, peerID string, w http.ResponseWriter) { @@ -142,14 +155,18 @@ func (h *PeersHandler) GetAllPeers(w http.ResponseWriter, r *http.Request) { dnsDomain := h.accountManager.GetDNSDomain() - respBody := []*api.Peer{} + respBody := make([]*api.PeerBatch, 0, len(peers)) for _, peer := range peers { peerToReturn, err := h.checkPeerStatus(peer) if err != nil { util.WriteError(err, w) return } - respBody = append(respBody, toPeerResponse(peerToReturn, account, dnsDomain)) + groupMinimumInfo := toGroupsInfo(account.Groups, peer.ID) + + accessiblePeerNumbers := h.accessiblePeersNumber(account, peer.ID) + + respBody = append(respBody, toPeerListItemResponse(peerToReturn, groupMinimumInfo, dnsDomain, accessiblePeerNumbers)) } util.WriteJSONObject(w, respBody) return @@ -158,17 +175,48 @@ func (h *PeersHandler) GetAllPeers(w http.ResponseWriter, r *http.Request) { } } -func toPeerResponse(peer *server.Peer, account *server.Account, dnsDomain string) *api.Peer { +func (h *PeersHandler) accessiblePeersNumber(account *server.Account, peerID string) int { + netMap := account.GetPeerNetworkMap(peerID, h.accountManager.GetDNSDomain()) + return len(netMap.Peers) + len(netMap.OfflinePeers) +} + +func toAccessiblePeers(netMap *server.NetworkMap, dnsDomain string) []api.AccessiblePeer { + accessiblePeers := make([]api.AccessiblePeer, 0, len(netMap.Peers)+len(netMap.OfflinePeers)) + for _, p := range netMap.Peers { + ap := api.AccessiblePeer{ + Id: p.ID, + Name: p.Name, + Ip: p.IP.String(), + DnsLabel: fqdn(p, dnsDomain), + UserId: p.UserID, + } + accessiblePeers = append(accessiblePeers, ap) + } + + for _, p := range netMap.OfflinePeers { + ap := api.AccessiblePeer{ + Id: p.ID, + Name: p.Name, + Ip: p.IP.String(), + DnsLabel: fqdn(p, dnsDomain), + UserId: p.UserID, + } + accessiblePeers = append(accessiblePeers, ap) + } + return accessiblePeers +} + +func toGroupsInfo(groups map[string]*server.Group, peerID string) []api.GroupMinimum { var groupsInfo []api.GroupMinimum groupsChecked := make(map[string]struct{}) - for _, group := range account.Groups { + for _, group := range groups { _, ok := groupsChecked[group.ID] if ok { continue } groupsChecked[group.ID] = struct{}{} for _, pk := range group.Peers { - if pk == peer.ID { + if pk == peerID { info := api.GroupMinimum{ Id: group.ID, Name: group.Name, @@ -179,12 +227,10 @@ func toPeerResponse(peer *server.Peer, account *server.Account, dnsDomain string } } } + return groupsInfo +} - fqdn := peer.FQDN(dnsDomain) - if fqdn == "" { - fqdn = peer.DNSLabel - } - +func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsDomain string, accessiblePeer []api.AccessiblePeer) *api.Peer { return &api.Peer{ Id: peer.ID, Name: peer.Name, @@ -198,9 +244,43 @@ func toPeerResponse(peer *server.Peer, account *server.Account, dnsDomain string Hostname: peer.Meta.Hostname, UserId: &peer.UserID, UiVersion: &peer.Meta.UIVersion, - DnsLabel: fqdn, + DnsLabel: fqdn(peer, dnsDomain), LoginExpirationEnabled: peer.LoginExpirationEnabled, LastLogin: peer.LastLogin, LoginExpired: peer.Status.LoginExpired, + AccessiblePeers: accessiblePeer, + ApprovalRequired: &peer.Status.RequiresApproval, + } +} + +func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsDomain string, accessiblePeersCount int) *api.PeerBatch { + return &api.PeerBatch{ + Id: peer.ID, + Name: peer.Name, + Ip: peer.IP.String(), + Connected: peer.Status.Connected, + LastSeen: peer.Status.LastSeen, + Os: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core), + Version: peer.Meta.WtVersion, + Groups: groupsInfo, + SshEnabled: peer.SSHEnabled, + Hostname: peer.Meta.Hostname, + UserId: &peer.UserID, + UiVersion: &peer.Meta.UIVersion, + DnsLabel: fqdn(peer, dnsDomain), + LoginExpirationEnabled: peer.LoginExpirationEnabled, + LastLogin: peer.LastLogin, + LoginExpired: peer.Status.LoginExpired, + AccessiblePeersCount: accessiblePeersCount, + ApprovalRequired: &peer.Status.RequiresApproval, + } +} + +func fqdn(peer *nbpeer.Peer, dnsDomain string) string { + fqdn := peer.FQDN(dnsDomain) + if fqdn == "" { + return peer.DNSLabel + } else { + return fqdn } } diff --git a/management/server/http/peers_handler_test.go b/management/server/http/peers_handler_test.go index 1856861d5..27978c487 100644 --- a/management/server/http/peers_handler_test.go +++ b/management/server/http/peers_handler_test.go @@ -3,7 +3,6 @@ package http import ( "bytes" "encoding/json" - "fmt" "io" "net" "net/http" @@ -14,6 +13,7 @@ import ( "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server/http/api" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/jwtclaims" @@ -26,11 +26,11 @@ import ( const testPeerID = "test_peer" const noUpdateChannelTestPeerID = "no-update-channel" -func initTestMetaData(peers ...*server.Peer) *PeersHandler { +func initTestMetaData(peers ...*nbpeer.Peer) *PeersHandler { return &PeersHandler{ accountManager: &mock_server.MockAccountManager{ - UpdatePeerFunc: func(accountID, userID string, update *server.Peer) (*server.Peer, error) { - var p *server.Peer + UpdatePeerFunc: func(accountID, userID string, update *nbpeer.Peer) (*nbpeer.Peer, error) { + var p *nbpeer.Peer for _, peer := range peers { if update.ID == peer.ID { p = peer.Copy() @@ -42,8 +42,8 @@ func initTestMetaData(peers ...*server.Peer) *PeersHandler { p.Name = update.Name return p, nil }, - GetPeerFunc: func(accountID, peerID, userID string) (*server.Peer, error) { - var p *server.Peer + GetPeerFunc: func(accountID, peerID, userID string) (*nbpeer.Peer, error) { + var p *nbpeer.Peer for _, peer := range peers { if peerID == peer.ID { p = peer.Copy() @@ -52,7 +52,7 @@ func initTestMetaData(peers ...*server.Peer) *PeersHandler { } return p, nil }, - GetPeersFunc: func(accountID, userID string) ([]*server.Peer, error) { + GetPeersFunc: func(accountID, userID string) ([]*nbpeer.Peer, error) { return peers, nil }, GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) { @@ -60,8 +60,9 @@ func initTestMetaData(peers ...*server.Peer) *PeersHandler { return &server.Account{ Id: claims.AccountId, Domain: "hotmail.com", - Peers: map[string]*server.Peer{ + Peers: map[string]*nbpeer.Peer{ peers[0].ID: peers[0], + peers[1].ID: peers[1], }, Users: map[string]*server.User{ "test_user": user, @@ -70,9 +71,17 @@ func initTestMetaData(peers ...*server.Peer) *PeersHandler { PeerLoginExpirationEnabled: true, PeerLoginExpiration: time.Hour, }, + Network: &server.Network{ + Identifier: "ciclqisab2ss43jdn8q0", + Net: net.IPNet{ + IP: net.ParseIP("100.67.0.0"), + Mask: net.IPv4Mask(255, 255, 0, 0), + }, + Serial: 51, + }, }, user, nil }, - GetAllConnectedPeersFunc: func() (map[string]struct{}, error) { + HasConnectedChannelFunc: func(peerID string) bool { statuses := make(map[string]struct{}) for _, peer := range peers { if peer.ID == noUpdateChannelTestPeerID { @@ -80,7 +89,8 @@ func initTestMetaData(peers ...*server.Peer) *PeersHandler { } statuses[peer.ID] = struct{}{} } - return statuses, nil + _, ok := statuses[peerID] + return ok }, }, claimsExtractor: jwtclaims.NewClaimsExtractor( @@ -99,15 +109,15 @@ func initTestMetaData(peers ...*server.Peer) *PeersHandler { // Use the metadata generated by initTestMetaData() to check for values func TestGetPeers(t *testing.T) { - peer := &server.Peer{ + peer := &nbpeer.Peer{ ID: testPeerID, Key: "key", SetupKey: "setupkey", IP: net.ParseIP("100.64.0.1"), - Status: &server.PeerStatus{Connected: true}, + Status: &nbpeer.PeerStatus{Connected: true}, Name: "PeerName", LoginExpirationEnabled: false, - Meta: server.PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "hostname", GoOS: "GoOS", Kernel: "kernel", @@ -136,7 +146,7 @@ func TestGetPeers(t *testing.T) { requestPath string requestBody io.Reader expectedArray bool - expectedPeer *server.Peer + expectedPeer *nbpeer.Peer }{ { name: "GetPeersMetaData", @@ -223,7 +233,7 @@ func TestGetPeers(t *testing.T) { } } - fmt.Println(got) + t.Log(got) assert.Equal(t, got.Name, tc.expectedPeer.Name) assert.Equal(t, got.Version, tc.expectedPeer.Meta.WtVersion) diff --git a/management/server/http/policies_handler.go b/management/server/http/policies_handler.go index c8f58f8a4..c7b69897a 100644 --- a/management/server/http/policies_handler.go +++ b/management/server/http/policies_handler.go @@ -290,17 +290,19 @@ func toPolicyResponse(account *server.Account, policy *server.Policy) *api.Polic Enabled: policy.Enabled, } for _, r := range policy.Rules { + rID := r.ID + rDescription := r.Description rule := api.PolicyRule{ - Id: &r.ID, + Id: &rID, Name: r.Name, Enabled: r.Enabled, - Description: &r.Description, + Description: &rDescription, Bidirectional: r.Bidirectional, Protocol: api.PolicyRuleProtocol(r.Protocol), Action: api.PolicyRuleAction(r.Action), } if len(r.Ports) != 0 { - portsCopy := r.Ports[:] + portsCopy := r.Ports rule.Ports = &portsCopy } for _, gid := range r.Sources { diff --git a/management/server/http/routes_handler_test.go b/management/server/http/routes_handler_test.go index 0bb4587e4..c02292f2a 100644 --- a/management/server/http/routes_handler_test.go +++ b/management/server/http/routes_handler_test.go @@ -11,6 +11,7 @@ import ( "testing" "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/route" @@ -55,12 +56,12 @@ var baseExistingRoute = &route.Route{ var testingAccount = &server.Account{ Id: testAccountID, Domain: "hotmail.com", - Peers: map[string]*server.Peer{ + Peers: map[string]*nbpeer.Peer{ existingPeerID: { Key: existingPeerKey, IP: netip.MustParseAddr(existingPeerIP1).AsSlice(), ID: existingPeerID, - Meta: server.PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ GoOS: "linux", }, }, @@ -68,7 +69,7 @@ var testingAccount = &server.Account{ Key: nonLinuxExistingPeerID, IP: netip.MustParseAddr(existingPeerIP2).AsSlice(), ID: nonLinuxExistingPeerID, - Meta: server.PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ GoOS: "darwin", }, }, diff --git a/management/server/http/setupkeys_handler.go b/management/server/http/setupkeys_handler.go index cddae672c..4adf3fdd0 100644 --- a/management/server/http/setupkeys_handler.go +++ b/management/server/http/setupkeys_handler.go @@ -192,13 +192,14 @@ func writeSuccess(w http.ResponseWriter, key *server.SetupKey) { func toResponseBody(key *server.SetupKey) *api.SetupKey { var state string - if key.IsExpired() { + switch { + case key.IsExpired(): state = "expired" - } else if key.IsRevoked() { + case key.IsRevoked(): state = "revoked" - } else if key.IsOverUsed() { + case key.IsOverUsed(): state = "overused" - } else { + default: state = "valid" } diff --git a/management/server/http/setupkeys_handler_test.go b/management/server/http/setupkeys_handler_test.go index d931a5e0b..7b68479ed 100644 --- a/management/server/http/setupkeys_handler_test.go +++ b/management/server/http/setupkeys_handler_test.go @@ -220,6 +220,7 @@ func TestSetupKeysHandlers(t *testing.T) { } func assertKeys(t *testing.T, got *api.SetupKey, expected *api.SetupKey) { + t.Helper() // this comparison is done manually because when converting to JSON dates formatted differently // assert.Equal(t, got.UpdatedAt, tc.expectedSetupKey.UpdatedAt) //doesn't work assert.WithinDurationf(t, got.UpdatedAt, expected.UpdatedAt, 0, "") diff --git a/management/server/http/users_handler.go b/management/server/http/users_handler.go index 0441e8cc0..5d92b65e5 100644 --- a/management/server/http/users_handler.go +++ b/management/server/http/users_handler.go @@ -94,7 +94,7 @@ func (h *UsersHandler) UpdateUser(w http.ResponseWriter, r *http.Request) { util.WriteJSONObject(w, toUserResponse(newUser, claims.UserId)) } -// DeleteUser is a DELETE request to delete a user (only works for service users right now) +// DeleteUser is a DELETE request to delete a user func (h *UsersHandler) DeleteUser(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { util.WriteErrorResponse("wrong HTTP method", http.StatusMethodNotAllowed, w) @@ -155,9 +155,14 @@ func (h *UsersHandler) CreateUser(w http.ResponseWriter, r *http.Request) { email = *req.Email } + name := "" + if req.Name != nil { + name = *req.Name + } + newUser, err := h.accountManager.CreateUser(account.Id, user.Id, &server.UserInfo{ Email: email, - Name: *req.Name, + Name: name, Role: req.Role, AutoGroups: req.AutoGroups, IsServiceUser: req.IsServiceUser, @@ -197,10 +202,14 @@ func (h *UsersHandler) GetAllUsers(w http.ResponseWriter, r *http.Request) { users := make([]*api.User, 0) for _, r := range data { + if r.NonDeletable { + continue + } if serviceUser == "" { users = append(users, toUserResponse(r, claims.UserId)) continue } + includeServiceUser, err := strconv.ParseBool(serviceUser) log.Debugf("Should include service user: %v", includeServiceUser) if err != nil { diff --git a/management/server/http/users_handler_test.go b/management/server/http/users_handler_test.go index b4d449be3..ff886ca9f 100644 --- a/management/server/http/users_handler_test.go +++ b/management/server/http/users_handler_test.go @@ -20,8 +20,9 @@ import ( ) const ( - serviceUserID = "serviceUserID" - regularUserID = "regularUserID" + serviceUserID = "serviceUserID" + nonDeletableServiceUserID = "nonDeletableServiceUserID" + regularUserID = "regularUserID" ) var usersTestAccount = &server.Account{ @@ -49,6 +50,13 @@ var usersTestAccount = &server.Account{ AutoGroups: []string{"group_1"}, Issued: server.UserIssuedAPI, }, + nonDeletableServiceUserID: { + Id: serviceUserID, + Role: "admin", + IsServiceUser: true, + NonDeletable: true, + Issued: server.UserIssuedIntegration, + }, }, } @@ -67,6 +75,7 @@ func initUsersTestData() *UsersHandler { Name: "", Email: "", IsServiceUser: v.IsServiceUser, + NonDeletable: v.NonDeletable, Issued: v.Issued, }) } diff --git a/management/server/idp/auth0_test.go b/management/server/idp/auth0_test.go index 63c634d4e..febc0ab89 100644 --- a/management/server/idp/auth0_test.go +++ b/management/server/idp/auth0_test.go @@ -65,6 +65,7 @@ func (mc *mockAuth0Credentials) Authenticate() (JWTToken, error) { } func newTestJWT(t *testing.T, expInt int) string { + t.Helper() now := time.Now() token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "iat": now.Unix(), diff --git a/management/server/idp/mock.go b/management/server/idp/mock.go new file mode 100644 index 000000000..7605466e7 --- /dev/null +++ b/management/server/idp/mock.go @@ -0,0 +1,77 @@ +package idp + +// MockIDP is a mock implementation of the IDP interface +type MockIDP struct { + UpdateUserAppMetadataFunc func(userId string, appMetadata AppMetadata) error + GetUserDataByIDFunc func(userId string, appMetadata AppMetadata) (*UserData, error) + GetAccountFunc func(accountId string) ([]*UserData, error) + GetAllAccountsFunc func() (map[string][]*UserData, error) + CreateUserFunc func(email, name, accountID, invitedByEmail string) (*UserData, error) + GetUserByEmailFunc func(email string) ([]*UserData, error) + InviteUserByIDFunc func(userID string) error + DeleteUserFunc func(userID string) error +} + +// UpdateUserAppMetadata is a mock implementation of the IDP interface UpdateUserAppMetadata method +func (m *MockIDP) UpdateUserAppMetadata(userId string, appMetadata AppMetadata) error { + if m.UpdateUserAppMetadataFunc != nil { + return m.UpdateUserAppMetadataFunc(userId, appMetadata) + } + return nil +} + +// GetUserDataByID is a mock implementation of the IDP interface GetUserDataByID method +func (m *MockIDP) GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error) { + if m.GetUserDataByIDFunc != nil { + return m.GetUserDataByIDFunc(userId, appMetadata) + } + return nil, nil +} + +// GetAccount is a mock implementation of the IDP interface GetAccount method +func (m *MockIDP) GetAccount(accountId string) ([]*UserData, error) { + if m.GetAccountFunc != nil { + return m.GetAccountFunc(accountId) + } + return nil, nil +} + +// GetAllAccounts is a mock implementation of the IDP interface GetAllAccounts method +func (m *MockIDP) GetAllAccounts() (map[string][]*UserData, error) { + if m.GetAllAccountsFunc != nil { + return m.GetAllAccountsFunc() + } + return nil, nil +} + +// CreateUser is a mock implementation of the IDP interface CreateUser method +func (m *MockIDP) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) { + if m.CreateUserFunc != nil { + return m.CreateUserFunc(email, name, accountID, invitedByEmail) + } + return nil, nil +} + +// GetUserByEmail is a mock implementation of the IDP interface GetUserByEmail method +func (m *MockIDP) GetUserByEmail(email string) ([]*UserData, error) { + if m.GetUserByEmailFunc != nil { + return m.GetUserByEmailFunc(email) + } + return nil, nil +} + +// InviteUserByID is a mock implementation of the IDP interface InviteUserByID method +func (m *MockIDP) InviteUserByID(userID string) error { + if m.InviteUserByIDFunc != nil { + return m.InviteUserByIDFunc(userID) + } + return nil +} + +// DeleteUser is a mock implementation of the IDP interface DeleteUser method +func (m *MockIDP) DeleteUser(userID string) error { + if m.DeleteUserFunc != nil { + return m.DeleteUserFunc(userID) + } + return nil +} diff --git a/management/server/idp/okta.go b/management/server/idp/okta.go index 67341a26f..d20ee7e48 100644 --- a/management/server/idp/okta.go +++ b/management/server/idp/okta.go @@ -256,7 +256,6 @@ func (om *OktaManager) InviteUserByID(_ string) error { func (om *OktaManager) DeleteUser(userID string) error { resp, err := om.client.User.DeactivateOrDeleteUser(context.Background(), userID, nil) if err != nil { - fmt.Println(err.Error()) return err } diff --git a/management/server/idp/zitadel.go b/management/server/idp/zitadel.go index 5325e51be..926f078b2 100644 --- a/management/server/idp/zitadel.go +++ b/management/server/idp/zitadel.go @@ -463,11 +463,9 @@ func (zp zitadelProfile) userData() *UserData { if zp.Human != nil { email = zp.Human.Email.Email name = zp.Human.Profile.DisplayName - } else { - if len(zp.LoginNames) > 0 { - email = zp.LoginNames[0] - name = zp.LoginNames[0] - } + } else if len(zp.LoginNames) > 0 { + email = zp.LoginNames[0] + name = zp.LoginNames[0] } return &UserData{ diff --git a/management/server/jwtclaims/extractor_test.go b/management/server/jwtclaims/extractor_test.go index f7eeb82e5..e9316b194 100644 --- a/management/server/jwtclaims/extractor_test.go +++ b/management/server/jwtclaims/extractor_test.go @@ -10,7 +10,8 @@ import ( "github.com/stretchr/testify/require" ) -func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audiance string) *http.Request { +func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audience string) *http.Request { + t.Helper() const layout = "2006-01-02T15:04:05.999Z" claimMaps := jwt.MapClaims{} @@ -18,16 +19,16 @@ func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audiance st claimMaps[UserIDClaim] = claims.UserId } if claims.AccountId != "" { - claimMaps[audiance+AccountIDSuffix] = claims.AccountId + claimMaps[audience+AccountIDSuffix] = claims.AccountId } if claims.Domain != "" { - claimMaps[audiance+DomainIDSuffix] = claims.Domain + claimMaps[audience+DomainIDSuffix] = claims.Domain } if claims.DomainCategory != "" { - claimMaps[audiance+DomainCategorySuffix] = claims.DomainCategory + claimMaps[audience+DomainCategorySuffix] = claims.DomainCategory } if claims.LastLogin != (time.Time{}) { - claimMaps[audiance+LastLoginSuffix] = claims.LastLogin.Format(layout) + claimMaps[audience+LastLoginSuffix] = claims.LastLogin.Format(layout) } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps) r, err := http.NewRequest(http.MethodGet, "http://localhost", nil) @@ -143,6 +144,7 @@ func TestExtractClaimsFromRequestContext(t *testing.T) { } func TestExtractClaimsSetOptions(t *testing.T) { + t.Helper() type test struct { name string extractor *ClaimsExtractor @@ -153,6 +155,7 @@ func TestExtractClaimsSetOptions(t *testing.T) { 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 @@ -172,6 +175,7 @@ func TestExtractClaimsSetOptions(t *testing.T) { 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 @@ -183,6 +187,7 @@ func TestExtractClaimsSetOptions(t *testing.T) { 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 @@ -199,6 +204,7 @@ func TestExtractClaimsSetOptions(t *testing.T) { } })), 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) diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 06fc6669d..21e9862f0 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -400,6 +400,7 @@ func TestServer_GetDeviceAuthorizationFlow(t *testing.T) { } func startManagement(t *testing.T, config *Config) (*grpc.Server, string, error) { + t.Helper() lis, err := net.Listen("tcp", "localhost:0") if err != nil { return nil, "", err @@ -409,7 +410,7 @@ func startManagement(t *testing.T, config *Config) (*grpc.Server, string, error) if err != nil { return nil, "", err } - peersUpdateManager := NewPeersUpdateManager() + peersUpdateManager := NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "", eventStore, false) diff --git a/management/server/management_test.go b/management/server/management_test.go index 375e7e634..ee9641a8c 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -501,7 +501,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) { if err != nil { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } - peersUpdateManager := server.NewPeersUpdateManager() + peersUpdateManager := server.NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", eventStore, false) diff --git a/management/server/metrics/selfhosted.go b/management/server/metrics/selfhosted.go index cf6b2e440..9a8c65b8e 100644 --- a/management/server/metrics/selfhosted.go +++ b/management/server/metrics/selfhosted.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "net/http" - "regexp" "sort" "strings" "time" @@ -201,14 +200,14 @@ func (w *Worker) generateProperties() properties { expirationEnabled++ } - groups = groups + len(account.Groups) - routes = routes + len(account.Routes) + groups += len(account.Groups) + routes += len(account.Routes) for _, route := range account.Routes { if len(route.PeerGroups) > 0 { routesWithRGGroups++ } } - nameservers = nameservers + len(account.NameServerGroups) + nameservers += len(account.NameServerGroups) for _, policy := range account.Policies { for _, rule := range policy.Rules { @@ -232,10 +231,10 @@ func (w *Worker) generateProperties() properties { } for _, key := range account.SetupKeys { - setupKeysUsage = setupKeysUsage + key.UsedTimes + setupKeysUsage += key.UsedTimes if key.Ephemeral { ephemeralPeersSKs++ - ephemeralPeersSKUsage = ephemeralPeersSKUsage + key.UsedTimes + ephemeralPeersSKUsage += key.UsedTimes } } @@ -381,29 +380,28 @@ func createPostRequest(ctx context.Context, endpoint string, payloadStr string) } func getMinMaxVersion(inputList []string) (string, string) { - reg, err := regexp.Compile(version.SemverRegexpRaw) - if err != nil { - return "", "" - } - versions := make([]*version.Version, 0) for _, raw := range inputList { - if raw != "" && reg.MatchString(raw) { + if raw != "" && nbversion.SemverRegexp.MatchString(raw) { v, err := version.NewVersion(raw) if err == nil { versions = append(versions, v) } } } - switch len(versions) { + + targetIndex := 1 + l := len(versions) + + switch l { case 0: return "", "" - case 1: - v := versions[0].String() + case targetIndex: + v := versions[targetIndex-1].String() return v, v default: sort.Sort(version.Collection(versions)) - return versions[0].String(), versions[len(versions)-1].String() + return versions[targetIndex-1].String(), versions[l-1].String() } } diff --git a/management/server/metrics/selfhosted_test.go b/management/server/metrics/selfhosted_test.go index 7717ff409..a577f8309 100644 --- a/management/server/metrics/selfhosted_test.go +++ b/management/server/metrics/selfhosted_test.go @@ -5,6 +5,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/route" ) @@ -37,12 +38,12 @@ func (mockDatasource) GetAllAccounts() []*server.Account { NameServerGroups: map[string]*nbdns.NameServerGroup{ "1": {}, }, - Peers: map[string]*server.Peer{ + Peers: map[string]*nbpeer.Peer{ "1": { ID: "1", UserID: "test", SSHEnabled: true, - Meta: server.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"}, + Meta: nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"}, }, }, Policies: []*server.Policy{ @@ -101,12 +102,12 @@ func (mockDatasource) GetAllAccounts() []*server.Account { NameServerGroups: map[string]*nbdns.NameServerGroup{ "1": {}, }, - Peers: map[string]*server.Peer{ + Peers: map[string]*nbpeer.Peer{ "1": { ID: "1", UserID: "test", SSHEnabled: true, - Meta: server.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"}, + Meta: nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"}, }, }, Policies: []*server.Policy{ diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index ea4a18f56..a349b35a9 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -10,6 +10,7 @@ import ( "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/jwtclaims" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/route" ) @@ -20,12 +21,13 @@ type MockAccountManager struct { GetSetupKeyFunc func(accountID, userID, keyID string) (*server.SetupKey, error) GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error) GetUserFunc func(claims jwtclaims.AuthorizationClaims) (*server.User, error) - GetPeersFunc func(accountID, userID string) ([]*server.Peer, error) + ListUsersFunc func(accountID string) ([]*server.User, error) + GetPeersFunc func(accountID, userID string) ([]*nbpeer.Peer, error) MarkPeerConnectedFunc func(peerKey string, connected bool) error DeletePeerFunc func(accountID, peerKey, userID string) error GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error) GetPeerNetworkFunc func(peerKey string) (*server.Network, error) - AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, *server.NetworkMap, error) + AddPeerFunc func(setupKey string, userId string, peer *nbpeer.Peer) (*nbpeer.Peer, *server.NetworkMap, error) GetGroupFunc func(accountID, groupID string) (*server.Group, error) SaveGroupFunc func(accountID, userID string, group *server.Group) error DeleteGroupFunc func(accountID, userId, groupID string) error @@ -43,9 +45,9 @@ type MockAccountManager struct { GetUsersFromAccountFunc func(accountID, userID string) ([]*server.UserInfo, error) GetAccountFromPATFunc func(pat string) (*server.Account, *server.User, *server.PersonalAccessToken, error) MarkPATUsedFunc func(pat string) error - UpdatePeerMetaFunc func(peerID string, meta server.PeerSystemMeta) error + UpdatePeerMetaFunc func(peerID string, meta nbpeer.PeerSystemMeta) error UpdatePeerSSHKeyFunc func(peerID string, sshKey string) error - UpdatePeerFunc func(accountID, userID string, peer *server.Peer) (*server.Peer, error) + UpdatePeerFunc func(accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) CreateRouteFunc func(accountID, prefix, peer string, peerGroups []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) GetRouteFunc func(accountID, routeID, userID string) (*route.Route, error) SaveRouteFunc func(accountID, userID string, route *route.Route) error @@ -54,6 +56,7 @@ type MockAccountManager struct { SaveSetupKeyFunc func(accountID string, key *server.SetupKey, userID string) (*server.SetupKey, error) ListSetupKeysFunc func(accountID, userID string) ([]*server.SetupKey, error) SaveUserFunc func(accountID, userID string, user *server.User) (*server.UserInfo, error) + SaveOrAddUserFunc func(accountID, userID string, user *server.User, addIfNotExists bool) (*server.UserInfo, error) DeleteUserFunc func(accountID string, initiatorUserID string, targetUserID string) error CreatePATFunc func(accountID string, initiatorUserID string, targetUserId string, tokenName string, expiresIn int) (*server.PersonalAccessTokenGenerated, error) DeletePATFunc func(accountID string, initiatorUserID string, targetUserId string, tokenID string) error @@ -66,16 +69,20 @@ type MockAccountManager struct { ListNameServerGroupsFunc func(accountID string) ([]*nbdns.NameServerGroup, error) CreateUserFunc func(accountID, userID string, key *server.UserInfo) (*server.UserInfo, error) GetAccountFromTokenFunc func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) + DeleteAccountFunc func(accountID, userID string) error GetDNSDomainFunc func() string + StoreEventFunc func(initiatorID, targetID, accountID string, activityID activity.Activity, meta map[string]any) GetEventsFunc func(accountID, userID string) ([]*activity.Event, error) GetDNSSettingsFunc func(accountID, userID string) (*server.DNSSettings, error) SaveDNSSettingsFunc func(accountID, userID string, dnsSettingsToSave *server.DNSSettings) error - GetPeerFunc func(accountID, peerID, userID string) (*server.Peer, error) + GetPeerFunc func(accountID, peerID, userID string) (*nbpeer.Peer, error) UpdateAccountSettingsFunc func(accountID, userID string, newSettings *server.Settings) (*server.Account, error) - LoginPeerFunc func(login server.PeerLogin) (*server.Peer, *server.NetworkMap, error) - SyncPeerFunc func(sync server.PeerSync) (*server.Peer, *server.NetworkMap, error) + LoginPeerFunc func(login server.PeerLogin) (*nbpeer.Peer, *server.NetworkMap, error) + SyncPeerFunc func(sync server.PeerSync) (*nbpeer.Peer, *server.NetworkMap, error) InviteUserFunc func(accountID string, initiatorUserID string, targetUserEmail string) error GetAllConnectedPeersFunc func() (map[string]struct{}, error) + HasConnectedChannelFunc func(peerID string) bool + GetExternalCacheManagerFunc func() server.ExternalCacheManager } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface @@ -153,6 +160,14 @@ func (am *MockAccountManager) GetAccountFromPAT(pat string) (*server.Account, *s return nil, nil, nil, status.Errorf(codes.Unimplemented, "method GetAccountFromPAT is not implemented") } +// DeleteAccount mock implementation of DeleteAccount from server.AccountManager interface +func (am *MockAccountManager) DeleteAccount(accountID, userID string) error { + if am.DeleteAccountFunc != nil { + return am.DeleteAccountFunc(accountID, userID) + } + return status.Errorf(codes.Unimplemented, "method DeleteAccount is not implemented") +} + // MarkPATUsed mock implementation of MarkPATUsed from server.AccountManager interface func (am *MockAccountManager) MarkPATUsed(pat string) error { if am.MarkPATUsedFunc != nil { @@ -213,8 +228,8 @@ func (am *MockAccountManager) GetPeerNetwork(peerKey string) (*server.Network, e func (am *MockAccountManager) AddPeer( setupKey string, userId string, - peer *server.Peer, -) (*server.Peer, *server.NetworkMap, error) { + peer *nbpeer.Peer, +) (*nbpeer.Peer, *server.NetworkMap, error) { if am.AddPeerFunc != nil { return am.AddPeerFunc(setupKey, userId, peer) } @@ -334,11 +349,11 @@ func (am *MockAccountManager) ListPolicies(accountID, userID string) ([]*server. } // UpdatePeerMeta mock implementation of UpdatePeerMeta from server.AccountManager interface -func (am *MockAccountManager) UpdatePeerMeta(peerID string, meta server.PeerSystemMeta) error { +func (am *MockAccountManager) UpdatePeerMeta(peerID string, meta nbpeer.PeerSystemMeta) error { if am.UpdatePeerMetaFunc != nil { return am.UpdatePeerMetaFunc(peerID, meta) } - return status.Errorf(codes.Unimplemented, "method UpdatePeerMetaFunc is not implemented") + return status.Errorf(codes.Unimplemented, "method UpdatePeerMeta is not implemented") } // GetUser mock implementation of GetUser from server.AccountManager interface @@ -346,7 +361,14 @@ func (am *MockAccountManager) GetUser(claims jwtclaims.AuthorizationClaims) (*se if am.GetUserFunc != nil { return am.GetUserFunc(claims) } - return nil, status.Errorf(codes.Unimplemented, "method IsUserGetUserAdmin is not implemented") + return nil, status.Errorf(codes.Unimplemented, "method GetUser is not implemented") +} + +func (am *MockAccountManager) ListUsers(accountID string) ([]*server.User, error) { + if am.ListUsersFunc != nil { + return am.ListUsers(accountID) + } + return nil, status.Errorf(codes.Unimplemented, "method ListUsers is not implemented") } // UpdatePeerSSHKey mocks UpdatePeerSSHKey function of the account manager @@ -354,15 +376,15 @@ func (am *MockAccountManager) UpdatePeerSSHKey(peerID string, sshKey string) err if am.UpdatePeerSSHKeyFunc != nil { return am.UpdatePeerSSHKeyFunc(peerID, sshKey) } - return status.Errorf(codes.Unimplemented, "method UpdatePeerSSHKey is is not implemented") + return status.Errorf(codes.Unimplemented, "method UpdatePeerSSHKey is not implemented") } // UpdatePeer mocks UpdatePeerFunc function of the account manager -func (am *MockAccountManager) UpdatePeer(accountID, userID string, peer *server.Peer) (*server.Peer, error) { +func (am *MockAccountManager) UpdatePeer(accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) { if am.UpdatePeerFunc != nil { return am.UpdatePeerFunc(accountID, userID, peer) } - return nil, status.Errorf(codes.Unimplemented, "method UpdatePeerFunc is is not implemented") + return nil, status.Errorf(codes.Unimplemented, "method UpdatePeer is not implemented") } // CreateRoute mock implementation of CreateRoute from server.AccountManager interface @@ -440,6 +462,14 @@ func (am *MockAccountManager) SaveUser(accountID, userID string, user *server.Us return nil, status.Errorf(codes.Unimplemented, "method SaveUser is not implemented") } +// SaveOrAddUser mocks SaveOrAddUser of the AccountManager interface +func (am *MockAccountManager) SaveOrAddUser(accountID, userID string, user *server.User, addIfNotExists bool) (*server.UserInfo, error) { + if am.SaveUserFunc != nil { + return am.SaveOrAddUserFunc(accountID, userID, user, addIfNotExists) + } + return nil, status.Errorf(codes.Unimplemented, "method SaveOrAddUser is not implemented") +} + // DeleteUser mocks DeleteUser of the AccountManager interface func (am *MockAccountManager) DeleteUser(accountID string, initiatorUserID string, targetUserID string) error { if am.DeleteUserFunc != nil { @@ -514,11 +544,11 @@ func (am *MockAccountManager) GetAccountFromToken(claims jwtclaims.Authorization } // GetPeers mocks GetPeers of the AccountManager interface -func (am *MockAccountManager) GetPeers(accountID, userID string) ([]*server.Peer, error) { +func (am *MockAccountManager) GetPeers(accountID, userID string) ([]*nbpeer.Peer, error) { if am.GetAccountFromTokenFunc != nil { return am.GetPeersFunc(accountID, userID) } - return nil, status.Errorf(codes.Unimplemented, "method GetAllPeers is not implemented") + return nil, status.Errorf(codes.Unimplemented, "method GetPeers is not implemented") } // GetDNSDomain mocks GetDNSDomain of the AccountManager interface @@ -534,7 +564,7 @@ func (am *MockAccountManager) GetEvents(accountID, userID string) ([]*activity.E if am.GetEventsFunc != nil { return am.GetEventsFunc(accountID, userID) } - return nil, status.Errorf(codes.Unimplemented, "method GetAllEvents is not implemented") + return nil, status.Errorf(codes.Unimplemented, "method GetEvents is not implemented") } // GetDNSSettings mocks GetDNSSettings of the AccountManager interface @@ -554,7 +584,7 @@ func (am *MockAccountManager) SaveDNSSettings(accountID string, userID string, d } // GetPeer mocks GetPeer of the AccountManager interface -func (am *MockAccountManager) GetPeer(accountID, peerID, userID string) (*server.Peer, error) { +func (am *MockAccountManager) GetPeer(accountID, peerID, userID string) (*nbpeer.Peer, error) { if am.GetPeerFunc != nil { return am.GetPeerFunc(accountID, peerID, userID) } @@ -570,7 +600,7 @@ func (am *MockAccountManager) UpdateAccountSettings(accountID, userID string, ne } // LoginPeer mocks LoginPeer of the AccountManager interface -func (am *MockAccountManager) LoginPeer(login server.PeerLogin) (*server.Peer, *server.NetworkMap, error) { +func (am *MockAccountManager) LoginPeer(login server.PeerLogin) (*nbpeer.Peer, *server.NetworkMap, error) { if am.LoginPeerFunc != nil { return am.LoginPeerFunc(login) } @@ -578,7 +608,7 @@ func (am *MockAccountManager) LoginPeer(login server.PeerLogin) (*server.Peer, * } // SyncPeer mocks SyncPeer of the AccountManager interface -func (am *MockAccountManager) SyncPeer(sync server.PeerSync) (*server.Peer, *server.NetworkMap, error) { +func (am *MockAccountManager) SyncPeer(sync server.PeerSync) (*nbpeer.Peer, *server.NetworkMap, error) { if am.SyncPeerFunc != nil { return am.SyncPeerFunc(sync) } @@ -592,3 +622,26 @@ func (am *MockAccountManager) GetAllConnectedPeers() (map[string]struct{}, error } return nil, status.Errorf(codes.Unimplemented, "method GetAllConnectedPeers is not implemented") } + +// HasconnectedChannel mocks HasConnectedChannel of the AccountManager interface +func (am *MockAccountManager) HasConnectedChannel(peerID string) bool { + if am.HasConnectedChannelFunc != nil { + return am.HasConnectedChannelFunc(peerID) + } + return false +} + +// StoreEvent mocks StoreEvent of the AccountManager interface +func (am *MockAccountManager) StoreEvent(initiatorID, targetID, accountID string, activityID activity.Activity, meta map[string]any) { + if am.StoreEventFunc != nil { + am.StoreEventFunc(initiatorID, targetID, accountID, activityID, meta) + } +} + +// GetExternalCacheManager mocks GetExternalCacheManager of the AccountManager interface +func (am *MockAccountManager) GetExternalCacheManager() server.ExternalCacheManager { + if am.GetExternalCacheManagerFunc() != nil { + return am.GetExternalCacheManagerFunc() + } + return nil +} diff --git a/management/server/nameserver.go b/management/server/nameserver.go index 8ae71dbae..1b8d59e29 100644 --- a/management/server/nameserver.go +++ b/management/server/nameserver.go @@ -76,7 +76,7 @@ func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, d am.updateAccountPeers(account) - am.storeEvent(userID, newNSGroup.ID, accountID, activity.NameserverGroupCreated, newNSGroup.EventMeta()) + am.StoreEvent(userID, newNSGroup.ID, accountID, activity.NameserverGroupCreated, newNSGroup.EventMeta()) return newNSGroup.Copy(), nil } @@ -111,7 +111,7 @@ func (am *DefaultAccountManager) SaveNameServerGroup(accountID, userID string, n am.updateAccountPeers(account) - am.storeEvent(userID, nsGroupToSave.ID, accountID, activity.NameserverGroupUpdated, nsGroupToSave.EventMeta()) + am.StoreEvent(userID, nsGroupToSave.ID, accountID, activity.NameserverGroupUpdated, nsGroupToSave.EventMeta()) return nil } @@ -141,7 +141,7 @@ func (am *DefaultAccountManager) DeleteNameServerGroup(accountID, nsGroupID, use am.updateAccountPeers(account) - am.storeEvent(userID, nsGroup.ID, accountID, activity.NameserverGroupDeleted, nsGroup.EventMeta()) + am.StoreEvent(userID, nsGroup.ID, accountID, activity.NameserverGroupDeleted, nsGroup.EventMeta()) return nil } @@ -267,8 +267,9 @@ func validateGroups(list []string, groups map[string]*Group) error { return nil } +var domainMatcher = regexp.MustCompile(domainPattern) + func validateDomain(domain string) error { - domainMatcher := regexp.MustCompile(domainPattern) if !domainMatcher.MatchString(domain) { return errors.New("domain should consists of only letters, numbers, and hyphens with no leading, trailing hyphens, or spaces") } diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 6210ae538..791dc5677 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -8,6 +8,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/activity" + nbpeer "github.com/netbirdio/netbird/management/server/peer" ) const ( @@ -741,15 +742,17 @@ func TestGetNameServerGroup(t *testing.T) { } func createNSManager(t *testing.T) (*DefaultAccountManager, error) { + t.Helper() store, err := createNSStore(t) if err != nil { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "", eventStore, false) } func createNSStore(t *testing.T) (Store, error) { + t.Helper() dataDir := t.TempDir() store, err := NewStoreFromJson(dataDir, nil) if err != nil { @@ -760,10 +763,11 @@ func createNSStore(t *testing.T) (Store, error) { } func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error) { - peer1 := &Peer{ + t.Helper() + peer1 := &nbpeer.Peer{ Key: nsGroupPeer1Key, Name: "test-host1@netbird.io", - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host1@netbird.io", GoOS: "linux", Kernel: "Linux", @@ -774,10 +778,10 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error UIVersion: "development", }, } - peer2 := &Peer{ + peer2 := &nbpeer.Peer{ Key: nsGroupPeer2Key, Name: "test-host2@netbird.io", - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host2@netbird.io", GoOS: "linux", Kernel: "Linux", diff --git a/management/server/network.go b/management/server/network.go index c5b165cae..ffe098c96 100644 --- a/management/server/network.go +++ b/management/server/network.go @@ -10,6 +10,7 @@ import ( "github.com/rs/xid" nbdns "github.com/netbirdio/netbird/dns" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/route" ) @@ -25,11 +26,11 @@ const ( ) type NetworkMap struct { - Peers []*Peer + Peers []*nbpeer.Peer Network *Network Routes []*route.Route DNSConfig nbdns.Config - OfflinePeers []*Peer + OfflinePeers []*nbpeer.Peer FirewallRules []*FirewallRule } @@ -66,7 +67,7 @@ func NewNetwork() *Network { func (n *Network) IncSerial() { n.mu.Lock() defer n.mu.Unlock() - n.Serial = n.Serial + 1 + n.Serial++ } // CurrentSerial returns the Network.Serial of the network (latest state id) diff --git a/management/server/peer.go b/management/server/peer.go index 33c9430fc..18d3f0e01 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -2,13 +2,14 @@ package server import ( "fmt" - "net" "strings" "time" + "github.com/netbirdio/management-integrations/additions" "github.com/rs/xid" "github.com/netbirdio/netbird/management/server/activity" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" log "github.com/sirupsen/logrus" @@ -16,38 +17,6 @@ import ( "github.com/netbirdio/netbird/management/proto" ) -// PeerSystemMeta is a metadata of a Peer machine system -type PeerSystemMeta struct { - Hostname string - GoOS string - Kernel string - Core string - Platform string - OS string - WtVersion string - UIVersion string -} - -func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { - return p.Hostname == other.Hostname && - p.GoOS == other.GoOS && - p.Kernel == other.Kernel && - p.Core == other.Core && - p.Platform == other.Platform && - p.OS == other.OS && - p.WtVersion == other.WtVersion && - p.UIVersion == other.UIVersion -} - -type PeerStatus struct { - // LastSeen is the last time peer was connected to the management service - LastSeen time.Time - // Connected indicates whether peer is connected to the management service or not - Connected bool - // LoginExpired - LoginExpired bool -} - // PeerSync used as a data object between the gRPC API and AccountManager on Sync request. type PeerSync struct { // WireGuardPubKey is a peers WireGuard public key @@ -61,146 +30,16 @@ type PeerLogin struct { // SSHKey is a peer's ssh key. Can be empty (e.g., old version do not provide it, or this feature is disabled) SSHKey string // Meta is the system information passed by peer, must be always present. - Meta PeerSystemMeta + Meta nbpeer.PeerSystemMeta // UserID indicates that JWT was used to log in, and it was valid. Can be empty when SetupKey is used or auth is not required. UserID string // SetupKey references to a server.SetupKey to log in. Can be empty when UserID is used or auth is not required. SetupKey string } -// Peer represents a machine connected to the network. -// The Peer is a WireGuard peer identified by a public key -type Peer struct { - // ID is an internal ID of the peer - ID string `gorm:"primaryKey"` - // AccountID is a reference to Account that this object belongs - AccountID string `json:"-" gorm:"index;uniqueIndex:idx_peers_account_id_ip"` - // WireGuard public key - Key string `gorm:"index"` - // A setup key this peer was registered with - SetupKey string - // IP address of the Peer - IP net.IP `gorm:"uniqueIndex:idx_peers_account_id_ip"` - // Meta is a Peer system meta data - Meta PeerSystemMeta `gorm:"embedded;embeddedPrefix:meta_"` - // Name is peer's name (machine name) - Name string - // DNSLabel 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 - // Status peer's management connection status - Status *PeerStatus `gorm:"embedded;embeddedPrefix:peer_status_"` - // The user ID that registered the peer - UserID string - // SSHKey is a public SSH key of the peer - SSHKey string - // SSHEnabled indicates whether SSH server is enabled on the peer - SSHEnabled bool - // LoginExpirationEnabled indicates whether peer's login expiration is enabled and once expired the peer has to re-login. - // Works with LastLogin - LoginExpirationEnabled bool - // LastLogin the time when peer performed last login operation - LastLogin time.Time - // Indicate ephemeral peer attribute - Ephemeral bool -} - -// AddedWithSSOLogin indicates whether this peer has been added with an SSO login by a user. -func (p *Peer) AddedWithSSOLogin() bool { - return p.UserID != "" -} - -// Copy copies Peer object -func (p *Peer) Copy() *Peer { - peerStatus := p.Status - if peerStatus != nil { - peerStatus = p.Status.Copy() - } - return &Peer{ - ID: p.ID, - AccountID: p.AccountID, - Key: p.Key, - SetupKey: p.SetupKey, - IP: p.IP, - Meta: p.Meta, - Name: p.Name, - DNSLabel: p.DNSLabel, - Status: peerStatus, - UserID: p.UserID, - SSHKey: p.SSHKey, - SSHEnabled: p.SSHEnabled, - LoginExpirationEnabled: p.LoginExpirationEnabled, - LastLogin: p.LastLogin, - Ephemeral: p.Ephemeral, - } -} - -// UpdateMetaIfNew updates peer's system metadata if new information is provided -// returns true if meta was updated, false otherwise -func (p *Peer) UpdateMetaIfNew(meta PeerSystemMeta) bool { - // Avoid overwriting UIVersion if the update was triggered sole by the CLI client - if meta.UIVersion == "" { - meta.UIVersion = p.Meta.UIVersion - } - - if p.Meta.isEqual(meta) { - return false - } - p.Meta = meta - return true -} - -// MarkLoginExpired marks peer's status expired or not -func (p *Peer) MarkLoginExpired(expired bool) { - newStatus := p.Status.Copy() - newStatus.LoginExpired = expired - if expired { - newStatus.Connected = false - } - p.Status = newStatus -} - -// LoginExpired indicates whether the peer's login has expired or not. -// If Peer.LastLogin plus the expiresIn duration has happened already; then login has expired. -// Return true if a login has expired, false otherwise, and time left to expiration (negative when expired). -// Login expiration can be disabled/enabled on a Peer level via Peer.LoginExpirationEnabled property. -// Login expiration can also be disabled/enabled globally on the Account level via Settings.PeerLoginExpirationEnabled. -// Only peers added by interactive SSO login can be expired. -func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) { - if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled { - return false, 0 - } - expiresAt := p.LastLogin.Add(expiresIn) - now := time.Now() - timeLeft := expiresAt.Sub(now) - return timeLeft <= 0, timeLeft -} - -// FQDN returns peers FQDN combined of the peer's DNS label and the system's DNS domain -func (p *Peer) FQDN(dnsDomain string) string { - if dnsDomain == "" { - return "" - } - return fmt.Sprintf("%s.%s", p.DNSLabel, dnsDomain) -} - -// EventMeta returns activity event meta related to the peer -func (p *Peer) EventMeta(dnsDomain string) map[string]any { - return map[string]any{"name": p.Name, "fqdn": p.FQDN(dnsDomain), "ip": p.IP} -} - -// Copy PeerStatus -func (p *PeerStatus) Copy() *PeerStatus { - return &PeerStatus{ - LastSeen: p.LastSeen, - Connected: p.Connected, - LoginExpired: p.LoginExpired, - } -} - // GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if // the current user is not an admin. -func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, error) { +func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*nbpeer.Peer, error) { account, err := am.Store.GetAccount(accountID) if err != nil { return nil, err @@ -211,10 +50,10 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, er return nil, err } - peers := make([]*Peer, 0) - peersMap := make(map[string]*Peer) + peers := make([]*nbpeer.Peer, 0) + peersMap := make(map[string]*nbpeer.Peer) for _, peer := range account.Peers { - if !user.IsAdmin() && user.Id != peer.UserID { + if !user.HasAdminPower() && user.Id != peer.UserID { // only display peers that belong to the current user if the current user is not an admin continue } @@ -231,7 +70,7 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, er } } - peers = make([]*Peer, 0, len(peersMap)) + peers = make([]*nbpeer.Peer, 0, len(peersMap)) for _, peer := range peersMap { peers = append(peers, peer) } @@ -290,7 +129,7 @@ func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected } // UpdatePeer updates peer. Only Peer.Name, Peer.SSHEnabled, and Peer.LoginExpirationEnabled can be updated. -func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Peer) (*Peer, error) { +func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *nbpeer.Peer) (*nbpeer.Peer, error) { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -304,13 +143,18 @@ func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Pe return nil, status.Errorf(status.NotFound, "peer %s not found", update.ID) } + update, err = additions.ValidatePeersUpdateRequest(update, peer, userID, accountID, am.eventStore, am.GetDNSDomain()) + if err != nil { + return nil, err + } + if peer.SSHEnabled != update.SSHEnabled { peer.SSHEnabled = update.SSHEnabled event := activity.PeerSSHEnabled if !update.SSHEnabled { event = activity.PeerSSHDisabled } - am.storeEvent(userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain())) + am.StoreEvent(userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain())) } if peer.Name != update.Name { @@ -325,7 +169,7 @@ func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Pe peer.DNSLabel = newLabel - am.storeEvent(userID, peer.ID, accountID, activity.PeerRenamed, peer.EventMeta(am.GetDNSDomain())) + am.StoreEvent(userID, peer.ID, accountID, activity.PeerRenamed, peer.EventMeta(am.GetDNSDomain())) } if peer.LoginExpirationEnabled != update.LoginExpirationEnabled { @@ -340,7 +184,7 @@ func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Pe if !update.LoginExpirationEnabled { event = activity.PeerLoginExpirationDisabled } - am.storeEvent(userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain())) + am.StoreEvent(userID, peer.IP.String(), accountID, event, peer.EventMeta(am.GetDNSDomain())) if peer.AddedWithSSOLogin() && peer.LoginExpirationEnabled && account.Settings.PeerLoginExpirationEnabled { am.checkAndSchedulePeerLoginExpiration(account) @@ -364,7 +208,7 @@ func (am *DefaultAccountManager) deletePeers(account *Account, peerIDs []string, // the first loop is needed to ensure all peers present under the account before modifying, otherwise // we might have some inconsistencies - peers := make([]*Peer, 0, len(peerIDs)) + peers := make([]*nbpeer.Peer, 0, len(peerIDs)) for _, peerID := range peerIDs { peer := account.GetPeer(peerID) @@ -394,7 +238,7 @@ func (am *DefaultAccountManager) deletePeers(account *Account, peerIDs []string, }, }) am.peersUpdateManager.CloseChannel(peer.ID) - am.storeEvent(userID, peer.ID, account.Id, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain())) + am.StoreEvent(userID, peer.ID, account.Id, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain())) } return nil @@ -456,7 +300,7 @@ func (am *DefaultAccountManager) GetPeerNetwork(peerID string) (*Network, error) // to it. We also add the User ID to the peer metadata to identify registrant. If no userID provided, then fail with status.PermissionDenied // Each new Peer will be assigned a new next net.IP from the Account.Network and Account.Network.LastIP will be updated (IP's are not reused). // The peer property is just a placeholder for the Peer properties to pass further -func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*Peer, *NetworkMap, error) { +func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, *NetworkMap, error) { if setupKey == "" && userID == "" { // no auth method provided => reject access return nil, nil, status.Errorf(status.Unauthenticated, "no peer auth method provided, please use a setup key or interactive SSO login") @@ -485,6 +329,15 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* return nil, nil, err } + if strings.ToLower(peer.Meta.Hostname) == "iphone" || strings.ToLower(peer.Meta.Hostname) == "ipad" && userID != "" { + if am.idpManager != nil { + userdata, err := am.lookupUserInCache(userID, account) + if err == nil { + peer.Meta.Hostname = fmt.Sprintf("%s-%s", peer.Meta.Hostname, strings.Split(userdata.Email, "@")[0]) + } + } + } + // This is a handling for the case when the same machine (with the same WireGuard pub key) tries to register twice. // Such case is possible when AddPeer function takes long time to finish after AcquireAccountLock (e.g., database is slow) // and the peer disconnects with a timeout and tries to register again. @@ -501,6 +354,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* } var ephemeral bool + setupKeyName := "" if !addedByUser { // validate the setup key if adding with a key sk, err := account.FindSetupKey(upperKey) @@ -516,6 +370,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* opEvent.InitiatorID = sk.Id opEvent.Activity = activity.PeerAddedWithSetupKey ephemeral = sk.Ephemeral + setupKeyName = sk.Name } else { opEvent.InitiatorID = userID opEvent.Activity = activity.PeerAddedByUser @@ -536,7 +391,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* return nil, nil, err } - newPeer := &Peer{ + newPeer := &nbpeer.Peer{ ID: xid.New().String(), Key: peer.Key, SetupKey: upperKey, @@ -545,7 +400,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* Name: peer.Meta.Hostname, DNSLabel: newLabel, UserID: userID, - Status: &PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, + Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, SSHEnabled: false, SSHKey: peer.SSHKey, LastLogin: time.Now().UTC(), @@ -553,6 +408,10 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* Ephemeral: ephemeral, } + if account.Settings.Extra != nil { + newPeer = additions.PreparePeer(newPeer, account.Settings.Extra) + } + // add peer to 'All' group group, err := account.GetGroupAll() if err != nil { @@ -590,7 +449,11 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* opEvent.TargetID = newPeer.ID opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain()) - am.storeEvent(opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta) + if !addedByUser { + opEvent.Meta["setup_key_name"] = setupKeyName + } + + am.StoreEvent(opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta) am.updateAccountPeers(account) @@ -599,7 +462,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (* } // SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible -func (am *DefaultAccountManager) SyncPeer(sync PeerSync) (*Peer, *NetworkMap, error) { +func (am *DefaultAccountManager) SyncPeer(sync PeerSync) (*nbpeer.Peer, *NetworkMap, error) { account, err := am.Store.GetAccountByPeerPubKey(sync.WireGuardPubKey) if err != nil { if errStatus, ok := status.FromError(err); ok && errStatus.Type() == status.NotFound { @@ -636,13 +499,14 @@ func (am *DefaultAccountManager) SyncPeer(sync PeerSync) (*Peer, *NetworkMap, er // LoginPeer logs in or registers a peer. // If peer doesn't exist the function checks whether a setup key or a user is present and registers a new peer if so. -func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*Peer, *NetworkMap, error) { +func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*nbpeer.Peer, *NetworkMap, error) { account, err := am.Store.GetAccountByPeerPubKey(login.WireGuardPubKey) + if err != nil { if errStatus, ok := status.FromError(err); ok && errStatus.Type() == status.NotFound { // we couldn't find this peer by its public key which can mean that peer hasn't been registered yet. // Try registering it. - return am.AddPeer(login.SetupKey, login.UserID, &Peer{ + return am.AddPeer(login.SetupKey, login.UserID, &nbpeer.Peer{ Key: login.WireGuardPubKey, Meta: login.Meta, SSHKey: login.SSHKey, @@ -686,7 +550,7 @@ func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*Peer, *NetworkMap, updateRemotePeers = true shouldStoreAccount = true - am.storeEvent(login.UserID, peer.ID, account.Id, activity.UserLoggedInPeer, peer.EventMeta(am.GetDNSDomain())) + am.StoreEvent(login.UserID, peer.ID, account.Id, activity.UserLoggedInPeer, peer.EventMeta(am.GetDNSDomain())) } peer, updated := updatePeerMeta(peer, login.Meta, account) @@ -712,7 +576,7 @@ func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*Peer, *NetworkMap, return peer, account.GetPeerNetworkMap(peer.ID, am.dnsDomain), nil } -func checkIfPeerOwnerIsBlocked(peer *Peer, account *Account) error { +func checkIfPeerOwnerIsBlocked(peer *nbpeer.Peer, account *Account) error { if peer.AddedWithSSOLogin() { user, err := account.FindUser(peer.UserID) if err != nil { @@ -725,7 +589,7 @@ func checkIfPeerOwnerIsBlocked(peer *Peer, account *Account) error { return nil } -func checkAuth(loginUserID string, peer *Peer) error { +func checkAuth(loginUserID string, peer *nbpeer.Peer) error { if loginUserID == "" { // absence of a user ID indicates that JWT wasn't provided. return status.Errorf(status.PermissionDenied, "peer login has expired, please log in once more") @@ -737,7 +601,7 @@ func checkAuth(loginUserID string, peer *Peer) error { return nil } -func peerLoginExpired(peer *Peer, account *Account) bool { +func peerLoginExpired(peer *nbpeer.Peer, account *Account) bool { expired, expiresIn := peer.LoginExpired(account.Settings.PeerLoginExpiration) expired = account.Settings.PeerLoginExpirationEnabled && expired if expired || peer.Status.LoginExpired { @@ -747,21 +611,12 @@ func peerLoginExpired(peer *Peer, account *Account) bool { return false } -func updatePeerLastLogin(peer *Peer, account *Account) { +func updatePeerLastLogin(peer *nbpeer.Peer, account *Account) { peer.UpdateLastLogin() account.UpdatePeer(peer) } -// UpdateLastLogin and set login expired false -func (p *Peer) UpdateLastLogin() *Peer { - p.LastLogin = time.Now().UTC() - newStatus := p.Status.Copy() - newStatus.LoginExpired = false - p.Status = newStatus - return p -} - -func (am *DefaultAccountManager) checkAndUpdatePeerSSHKey(peer *Peer, account *Account, newSSHKey string) (*Peer, error) { +func (am *DefaultAccountManager) checkAndUpdatePeerSSHKey(peer *nbpeer.Peer, account *Account, newSSHKey string) (*nbpeer.Peer, error) { if len(newSSHKey) == 0 { log.Debugf("no new SSH key provided for peer %s, skipping update", peer.ID) return peer, nil @@ -832,7 +687,7 @@ func (am *DefaultAccountManager) UpdatePeerSSHKey(peerID string, sshKey string) } // GetPeer for a given accountID, peerID and userID error if not found. -func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*Peer, error) { +func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*nbpeer.Peer, error) { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -852,7 +707,7 @@ func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*Pee } // if admin or user owns this peer, return peer - if user.IsAdmin() || peer.UserID == userID { + if user.HasAdminPower() || peer.UserID == userID { return peer, nil } @@ -875,7 +730,7 @@ func (am *DefaultAccountManager) GetPeer(accountID, peerID, userID string) (*Pee return nil, status.Errorf(status.Internal, "user %s has no access to peer %s under account %s", userID, peerID, accountID) } -func updatePeerMeta(peer *Peer, meta PeerSystemMeta, account *Account) (*Peer, bool) { +func updatePeerMeta(peer *nbpeer.Peer, meta nbpeer.PeerSystemMeta, account *Account) (*nbpeer.Peer, bool) { if peer.UpdateMetaIfNew(meta) { account.UpdatePeer(peer) return peer, true diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go new file mode 100644 index 000000000..a4e4cc3aa --- /dev/null +++ b/management/server/peer/peer.go @@ -0,0 +1,181 @@ +package peer + +import ( + "fmt" + "net" + "time" +) + +// Peer represents a machine connected to the network. +// The Peer is a WireGuard peer identified by a public key +type Peer struct { + // ID is an internal ID of the peer + ID string `gorm:"primaryKey"` + // AccountID is a reference to Account that this object belongs + AccountID string `json:"-" gorm:"index;uniqueIndex:idx_peers_account_id_ip"` + // WireGuard public key + Key string `gorm:"index"` + // A setup key this peer was registered with + SetupKey string + // IP address of the Peer + IP net.IP `gorm:"uniqueIndex:idx_peers_account_id_ip"` + // Meta is a Peer system meta data + Meta PeerSystemMeta `gorm:"embedded;embeddedPrefix:meta_"` + // Name is peer's name (machine name) + Name string + // DNSLabel 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 + // Status peer's management connection status + Status *PeerStatus `gorm:"embedded;embeddedPrefix:peer_status_"` + // The user ID that registered the peer + UserID string + // SSHKey is a public SSH key of the peer + SSHKey string + // SSHEnabled indicates whether SSH server is enabled on the peer + SSHEnabled bool + // LoginExpirationEnabled indicates whether peer's login expiration is enabled and once expired the peer has to re-login. + // Works with LastLogin + LoginExpirationEnabled bool + // LastLogin the time when peer performed last login operation + LastLogin time.Time + // Indicate ephemeral peer attribute + Ephemeral bool +} + +type PeerStatus struct { + // LastSeen is the last time peer was connected to the management service + LastSeen time.Time + // Connected indicates whether peer is connected to the management service or not + Connected bool + // LoginExpired + LoginExpired bool + // RequiresApproval indicates whether peer requires approval or not + RequiresApproval bool +} + +// PeerSystemMeta is a metadata of a Peer machine system +type PeerSystemMeta struct { + Hostname string + GoOS string + Kernel string + Core string + Platform string + OS string + WtVersion string + UIVersion string +} + +func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { + return p.Hostname == other.Hostname && + p.GoOS == other.GoOS && + p.Kernel == other.Kernel && + p.Core == other.Core && + p.Platform == other.Platform && + p.OS == other.OS && + p.WtVersion == other.WtVersion && + p.UIVersion == other.UIVersion +} + +// AddedWithSSOLogin indicates whether this peer has been added with an SSO login by a user. +func (p *Peer) AddedWithSSOLogin() bool { + return p.UserID != "" +} + +// Copy copies Peer object +func (p *Peer) Copy() *Peer { + peerStatus := p.Status + if peerStatus != nil { + peerStatus = p.Status.Copy() + } + return &Peer{ + ID: p.ID, + AccountID: p.AccountID, + Key: p.Key, + SetupKey: p.SetupKey, + IP: p.IP, + Meta: p.Meta, + Name: p.Name, + DNSLabel: p.DNSLabel, + Status: peerStatus, + UserID: p.UserID, + SSHKey: p.SSHKey, + SSHEnabled: p.SSHEnabled, + LoginExpirationEnabled: p.LoginExpirationEnabled, + LastLogin: p.LastLogin, + Ephemeral: p.Ephemeral, + } +} + +// UpdateMetaIfNew updates peer's system metadata if new information is provided +// returns true if meta was updated, false otherwise +func (p *Peer) UpdateMetaIfNew(meta PeerSystemMeta) bool { + // Avoid overwriting UIVersion if the update was triggered sole by the CLI client + if meta.UIVersion == "" { + meta.UIVersion = p.Meta.UIVersion + } + + if p.Meta.isEqual(meta) { + return false + } + p.Meta = meta + return true +} + +// MarkLoginExpired marks peer's status expired or not +func (p *Peer) MarkLoginExpired(expired bool) { + newStatus := p.Status.Copy() + newStatus.LoginExpired = expired + if expired { + newStatus.Connected = false + } + p.Status = newStatus +} + +// LoginExpired indicates whether the peer's login has expired or not. +// If Peer.LastLogin plus the expiresIn duration has happened already; then login has expired. +// Return true if a login has expired, false otherwise, and time left to expiration (negative when expired). +// Login expiration can be disabled/enabled on a Peer level via Peer.LoginExpirationEnabled property. +// Login expiration can also be disabled/enabled globally on the Account level via Settings.PeerLoginExpirationEnabled. +// Only peers added by interactive SSO login can be expired. +func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) { + if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled { + return false, 0 + } + expiresAt := p.LastLogin.Add(expiresIn) + now := time.Now() + timeLeft := expiresAt.Sub(now) + return timeLeft <= 0, timeLeft +} + +// FQDN returns peers FQDN combined of the peer's DNS label and the system's DNS domain +func (p *Peer) FQDN(dnsDomain string) string { + if dnsDomain == "" { + return "" + } + return fmt.Sprintf("%s.%s", p.DNSLabel, dnsDomain) +} + +// EventMeta returns activity event meta related to the peer +func (p *Peer) EventMeta(dnsDomain string) map[string]any { + return map[string]any{"name": p.Name, "fqdn": p.FQDN(dnsDomain), "ip": p.IP} +} + +// Copy PeerStatus +func (p *PeerStatus) Copy() *PeerStatus { + return &PeerStatus{ + LastSeen: p.LastSeen, + Connected: p.Connected, + LoginExpired: p.LoginExpired, + RequiresApproval: p.RequiresApproval, + } +} + +// UpdateLastLogin and set login expired false +func (p *Peer) UpdateLastLogin() *Peer { + p.LastLogin = time.Now().UTC() + newStatus := p.Status.Copy() + newStatus.LoginExpired = false + p.Status = newStatus + return p +} diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 9d5a8bfb9..ee84ea47d 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -8,6 +8,8 @@ import ( "github.com/rs/xid" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" ) func TestPeer_LoginExpired(t *testing.T) { @@ -52,7 +54,7 @@ func TestPeer_LoginExpired(t *testing.T) { for _, c := range tt { t.Run(c.name, func(t *testing.T) { - peer := &Peer{ + peer := &nbpeer.Peer{ LoginExpirationEnabled: c.expirationEnabled, LastLogin: c.lastLogin, UserID: userID, @@ -90,9 +92,9 @@ func TestAccountManager_GetNetworkMap(t *testing.T) { return } - peer1, _, err := manager.AddPeer(setupKey.Key, "", &Peer{ + peer1, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: peerKey1.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer-1"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-1"}, }) if err != nil { t.Errorf("expecting peer to be added, got failure %v", err) @@ -104,9 +106,9 @@ func TestAccountManager_GetNetworkMap(t *testing.T) { t.Fatal(err) return } - _, _, err = manager.AddPeer(setupKey.Key, "", &Peer{ + _, _, err = manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: peerKey2.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer-2"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-2"}, }) if err != nil { @@ -163,9 +165,9 @@ func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) { return } - peer1, _, err := manager.AddPeer(setupKey.Key, "", &Peer{ + peer1, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: peerKey1.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer-1"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-1"}, }) if err != nil { t.Errorf("expecting peer to be added, got failure %v", err) @@ -177,9 +179,9 @@ func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) { t.Fatal(err) return } - peer2, _, err := manager.AddPeer(setupKey.Key, "", &Peer{ + peer2, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: peerKey2.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer-2"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-2"}, }) if err != nil { t.Errorf("expecting peer to be added, got failure %v", err) @@ -339,9 +341,9 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) { return } - peer1, _, err := manager.AddPeer(setupKey.Key, "", &Peer{ + peer1, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: peerKey1.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer-1"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-1"}, }) if err != nil { t.Errorf("expecting peer to be added, got failure %v", err) @@ -353,9 +355,9 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) { t.Fatal(err) return } - _, _, err = manager.AddPeer(setupKey.Key, "", &Peer{ + _, _, err = manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: peerKey2.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer-2"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-2"}, }) if err != nil { @@ -409,9 +411,9 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) { return } - peer1, _, err := manager.AddPeer("", someUser, &Peer{ + peer1, _, err := manager.AddPeer("", someUser, &nbpeer.Peer{ Key: peerKey1.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer-2"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-2"}, }) if err != nil { t.Errorf("expecting peer to be added, got failure %v", err) @@ -425,9 +427,9 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) { } // the second peer added with a setup key - peer2, _, err := manager.AddPeer(setupKey.Key, "", &Peer{ + peer2, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{ Key: peerKey2.PublicKey().String(), - Meta: PeerSystemMeta{Hostname: "test-peer-2"}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-2"}, }) if err != nil { t.Fatal(err) diff --git a/management/server/policy.go b/management/server/policy.go index b7b5b331c..0eb2fb538 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -5,10 +5,12 @@ import ( "strconv" "strings" + "github.com/netbirdio/management-integrations/additions" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/status" ) @@ -205,7 +207,7 @@ type FirewallRule struct { // getPeerConnectionResources for a given peer // // This function returns the list of peers and firewall rules that are applicable to a given peer. -func (a *Account) getPeerConnectionResources(peerID string) ([]*Peer, []*FirewallRule) { +func (a *Account) getPeerConnectionResources(peerID string) ([]*nbpeer.Peer, []*FirewallRule) { generateResources, getAccumulatedResources := a.connResourcesGenerator() for _, policy := range a.Policies { if !policy.Enabled { @@ -219,6 +221,8 @@ func (a *Account) getPeerConnectionResources(peerID string) ([]*Peer, []*Firewal sourcePeers, peerInSources := getAllPeersFromGroups(a, rule.Sources, peerID) destinationPeers, peerInDestinations := getAllPeersFromGroups(a, rule.Destinations, peerID) + sourcePeers = additions.ValidatePeers(sourcePeers) + destinationPeers = additions.ValidatePeers(destinationPeers) if rule.Bidirectional { if peerInSources { @@ -247,11 +251,11 @@ func (a *Account) getPeerConnectionResources(peerID string) ([]*Peer, []*Firewal // The generator function is used to generate the list of peers and firewall rules that are applicable to a given peer. // It safe to call the generator function multiple times for same peer and different rules no duplicates will be // generated. The accumulator function returns the result of all the generator calls. -func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*Peer, int), func() ([]*Peer, []*FirewallRule)) { +func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*nbpeer.Peer, int), func() ([]*nbpeer.Peer, []*FirewallRule)) { rulesExists := make(map[string]struct{}) peersExists := make(map[string]struct{}) rules := make([]*FirewallRule, 0) - peers := make([]*Peer, 0) + peers := make([]*nbpeer.Peer, 0) all, err := a.GetGroupAll() if err != nil { @@ -259,7 +263,7 @@ func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*Peer, int), fun all = &Group{} } - return func(rule *PolicyRule, groupPeers []*Peer, direction int) { + return func(rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int) { isAll := (len(all.Peers) - 1) == len(groupPeers) for _, peer := range groupPeers { if peer == nil { @@ -299,7 +303,7 @@ func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*Peer, int), fun rules = append(rules, &pr) } } - }, func() ([]*Peer, []*FirewallRule) { + }, func() ([]*nbpeer.Peer, []*FirewallRule) { return peers, rules } } @@ -319,8 +323,8 @@ func (am *DefaultAccountManager) GetPolicy(accountID, policyID, userID string) ( return nil, err } - if !user.IsAdmin() { - return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view policies") + if !user.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view policies") } for _, policy := range account.Policies { @@ -353,7 +357,7 @@ func (am *DefaultAccountManager) SavePolicy(accountID, userID string, policy *Po if exists { action = activity.PolicyUpdated } - am.storeEvent(userID, policy.ID, accountID, action, policy.EventMeta()) + am.StoreEvent(userID, policy.ID, accountID, action, policy.EventMeta()) am.updateAccountPeers(account) @@ -380,7 +384,7 @@ func (am *DefaultAccountManager) DeletePolicy(accountID, policyID, userID string return err } - am.storeEvent(userID, policy.ID, accountID, activity.PolicyRemoved, policy.EventMeta()) + am.StoreEvent(userID, policy.ID, accountID, activity.PolicyRemoved, policy.EventMeta()) am.updateAccountPeers(account) @@ -402,11 +406,11 @@ func (am *DefaultAccountManager) ListPolicies(accountID, userID string) ([]*Poli return nil, err } - if !user.IsAdmin() { - return nil, status.Errorf(status.PermissionDenied, "Only Administrators can view policies") + if !user.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power can view policies") } - return account.Policies[:], nil + return account.Policies, nil } func (am *DefaultAccountManager) deletePolicy(account *Account, policyID string) (*Policy, error) { @@ -478,9 +482,9 @@ func toProtocolFirewallRules(update []*FirewallRule) []*proto.FirewallRule { // getAllPeersFromGroups for given peer ID and list of groups // // Returns list of peers and boolean indicating if peer is in any of the groups -func getAllPeersFromGroups(account *Account, groups []string, peerID string) ([]*Peer, bool) { +func getAllPeersFromGroups(account *Account, groups []string, peerID string) ([]*nbpeer.Peer, bool) { peerInGroups := false - filteredPeers := make([]*Peer, 0, len(groups)) + filteredPeers := make([]*nbpeer.Peer, 0, len(groups)) for _, g := range groups { group, ok := account.Groups[g] if !ok { diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 971bd27d9..3ed08f4e6 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -7,11 +7,13 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/exp/slices" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" ) func TestAccount_getPeersByPolicy(t *testing.T) { account := &Account{ - Peers: map[string]*Peer{ + Peers: map[string]*nbpeer.Peer{ "peerA": { ID: "peerA", IP: net.ParseIP("100.65.14.88"), @@ -255,7 +257,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { func TestAccount_getPeersByPolicyDirect(t *testing.T) { account := &Account{ - Peers: map[string]*Peer{ + Peers: map[string]*nbpeer.Peer{ "peerA": { ID: "peerA", IP: net.ParseIP("100.65.14.88"), diff --git a/management/server/route.go b/management/server/route.go index 6b5aa982d..5f7976fe4 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -4,11 +4,12 @@ import ( "net/netip" "unicode/utf8" + "github.com/rs/xid" + "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/route" - "github.com/rs/xid" ) // GetRoute gets a route object from account and route IDs @@ -26,8 +27,8 @@ func (am *DefaultAccountManager) GetRoute(accountID, routeID, userID string) (*r return nil, err } - if !user.IsAdmin() { - return nil, status.Errorf(status.PermissionDenied, "Only administrators can view Network Routes") + if !user.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power can view Network Routes") } wantedRoute, found := account.Routes[routeID] @@ -186,7 +187,7 @@ func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string, am.updateAccountPeers(account) - am.storeEvent(userID, newRoute.ID, accountID, activity.RouteCreated, newRoute.EventMeta()) + am.StoreEvent(userID, newRoute.ID, accountID, activity.RouteCreated, newRoute.EventMeta()) return &newRoute, nil } @@ -247,7 +248,7 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave am.updateAccountPeers(account) - am.storeEvent(userID, routeToSave.ID, accountID, activity.RouteUpdated, routeToSave.EventMeta()) + am.StoreEvent(userID, routeToSave.ID, accountID, activity.RouteUpdated, routeToSave.EventMeta()) return nil } @@ -273,7 +274,7 @@ func (am *DefaultAccountManager) DeleteRoute(accountID, routeID, userID string) return err } - am.storeEvent(userID, routy.ID, accountID, activity.RouteRemoved, routy.EventMeta()) + am.StoreEvent(userID, routy.ID, accountID, activity.RouteRemoved, routy.EventMeta()) am.updateAccountPeers(account) @@ -295,8 +296,8 @@ func (am *DefaultAccountManager) ListRoutes(accountID, userID string) ([]*route. return nil, err } - if !user.IsAdmin() { - return nil, status.Errorf(status.PermissionDenied, "Only administrators can view Network Routes") + if !user.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power can view Network Routes") } routes := make([]*route.Route, 0, len(account.Routes)) diff --git a/management/server/route_test.go b/management/server/route_test.go index efd73d6c2..94f169a9b 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/management/server/activity" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/route" ) @@ -1007,15 +1008,17 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { } func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { + t.Helper() store, err := createRouterStore(t) if err != nil { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "", eventStore, false) } func createRouterStore(t *testing.T) (Store, error) { + t.Helper() dataDir := t.TempDir() store, err := NewStoreFromJson(dataDir, nil) if err != nil { @@ -1043,13 +1046,13 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er return nil, err } - peer1 := &Peer{ + peer1 := &nbpeer.Peer{ IP: peer1IP, ID: peer1ID, Key: peer1Key, Name: "test-host1@netbird.io", UserID: userID, - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host1@netbird.io", GoOS: "linux", Kernel: "Linux", @@ -1068,13 +1071,13 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er return nil, err } - peer2 := &Peer{ + peer2 := &nbpeer.Peer{ IP: peer2IP, ID: peer2ID, Key: peer2Key, Name: "test-host2@netbird.io", UserID: userID, - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host2@netbird.io", GoOS: "linux", Kernel: "Linux", @@ -1093,13 +1096,13 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er return nil, err } - peer3 := &Peer{ + peer3 := &nbpeer.Peer{ IP: peer3IP, ID: peer3ID, Key: peer3Key, Name: "test-host3@netbird.io", UserID: userID, - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host3@netbird.io", GoOS: "darwin", Kernel: "Darwin", @@ -1118,13 +1121,13 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er return nil, err } - peer4 := &Peer{ + peer4 := &nbpeer.Peer{ IP: peer4IP, ID: peer4ID, Key: peer4Key, Name: "test-host4@netbird.io", UserID: userID, - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host4@netbird.io", GoOS: "linux", Kernel: "Linux", @@ -1143,13 +1146,13 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er return nil, err } - peer5 := &Peer{ + peer5 := &nbpeer.Peer{ IP: peer5IP, ID: peer5ID, Key: peer5Key, Name: "test-host4@netbird.io", UserID: userID, - Meta: PeerSystemMeta{ + Meta: nbpeer.PeerSystemMeta{ Hostname: "test-host4@netbird.io", GoOS: "linux", Kernel: "Linux", diff --git a/management/server/setupkey.go b/management/server/setupkey.go index a33f537a7..b557b07c8 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -137,7 +137,7 @@ func (key *SetupKey) HiddenCopy(length int) *SetupKey { // IncrementUsage makes a copy of a key, increments the UsedTimes by 1 and sets LastUsed to now func (key *SetupKey) IncrementUsage() *SetupKey { c := key.Copy() - c.UsedTimes = c.UsedTimes + 1 + c.UsedTimes++ c.LastUsed = time.Now().UTC() return c } @@ -235,12 +235,12 @@ func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string return nil, status.Errorf(status.Internal, "failed adding account key") } - am.storeEvent(userID, setupKey.Id, accountID, activity.SetupKeyCreated, setupKey.EventMeta()) + am.StoreEvent(userID, setupKey.Id, accountID, activity.SetupKeyCreated, setupKey.EventMeta()) for _, g := range setupKey.AutoGroups { group := account.GetGroup(g) if group != nil { - am.storeEvent(userID, setupKey.Id, accountID, activity.GroupAddedToSetupKey, + am.StoreEvent(userID, setupKey.Id, accountID, activity.GroupAddedToSetupKey, map[string]any{"group": group.Name, "group_id": group.ID, "setupkey": setupKey.Name}) } else { log.Errorf("group %s not found while saving setup key activity event of account %s", g, account.Id) @@ -292,7 +292,7 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup } if !oldKey.Revoked && newKey.Revoked { - am.storeEvent(userID, newKey.Id, accountID, activity.SetupKeyRevoked, newKey.EventMeta()) + am.StoreEvent(userID, newKey.Id, accountID, activity.SetupKeyRevoked, newKey.EventMeta()) } defer func() { @@ -301,7 +301,7 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup for _, g := range removedGroups { group := account.GetGroup(g) if group != nil { - am.storeEvent(userID, oldKey.Id, accountID, activity.GroupRemovedFromSetupKey, + am.StoreEvent(userID, oldKey.Id, accountID, activity.GroupRemovedFromSetupKey, map[string]any{"group": group.Name, "group_id": group.ID, "setupkey": newKey.Name}) } else { log.Errorf("group %s not found while saving setup key activity event of account %s", g, account.Id) @@ -312,7 +312,7 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup for _, g := range addedGroups { group := account.GetGroup(g) if group != nil { - am.storeEvent(userID, oldKey.Id, accountID, activity.GroupAddedToSetupKey, + am.StoreEvent(userID, oldKey.Id, accountID, activity.GroupAddedToSetupKey, map[string]any{"group": group.Name, "group_id": group.ID, "setupkey": newKey.Name}) } else { log.Errorf("group %s not found while saving setup key activity event of account %s", g, account.Id) @@ -342,7 +342,7 @@ func (am *DefaultAccountManager) ListSetupKeys(accountID, userID string) ([]*Set keys := make([]*SetupKey, 0, len(account.SetupKeys)) for _, key := range account.SetupKeys { var k *SetupKey - if !user.IsAdmin() { + if !user.HasAdminPower() { k = key.HiddenCopy(999) } else { k = key.Copy() @@ -384,7 +384,7 @@ func (am *DefaultAccountManager) GetSetupKey(accountID, userID, keyID string) (* foundKey.UpdatedAt = foundKey.CreatedAt } - if !user.IsAdmin() { + if !user.HasAdminPower() { foundKey = foundKey.HiddenCopy(999) } diff --git a/management/server/setupkey_test.go b/management/server/setupkey_test.go index 6da01bd82..b104a6959 100644 --- a/management/server/setupkey_test.go +++ b/management/server/setupkey_test.go @@ -237,6 +237,7 @@ func TestSetupKey_IsValid(t *testing.T) { func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke bool, expectedType string, expectedUsedTimes int, expectedCreatedAt time.Time, expectedExpiresAt time.Time, expectedID string, expectedUpdatedAt time.Time, expectedAutoGroups []string) { + t.Helper() if key.Name != expectedName { t.Errorf("expected setup key to have Name %v, got %v", expectedName, key.Name) } diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index ed473e143..1bc2db3f1 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -7,15 +7,18 @@ import ( "sync" "time" - nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/management/server/status" - "github.com/netbirdio/netbird/management/server/telemetry" - "github.com/netbirdio/netbird/route" log "github.com/sirupsen/logrus" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/clause" "gorm.io/gorm/logger" + + nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/server/account" + nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/telemetry" + "github.com/netbirdio/netbird/route" ) // SqliteStore represents an account storage backed by a Sqlite DB persisted to disk @@ -58,9 +61,9 @@ func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, sql.SetMaxOpenConns(conns) // TODO: make it configurable err = db.AutoMigrate( - &SetupKey{}, &Peer{}, &User{}, &PersonalAccessToken{}, &Group{}, &Rule{}, + &SetupKey{}, &nbpeer.Peer{}, &User{}, &PersonalAccessToken{}, &Group{}, &Rule{}, &Account{}, &Policy{}, &PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, - &installation{}, + &installation{}, &account.ExtraSettings{}, ) if err != nil { return nil, err @@ -202,6 +205,37 @@ func (s *SqliteStore) SaveAccount(account *Account) error { return err } +func (s *SqliteStore) DeleteAccount(account *Account) error { + start := time.Now() + + err := s.db.Transaction(func(tx *gorm.DB) error { + result := tx.Select(clause.Associations).Delete(account.Policies, "account_id = ?", account.Id) + if result.Error != nil { + return result.Error + } + + result = tx.Select(clause.Associations).Delete(account.UsersG, "account_id = ?", account.Id) + if result.Error != nil { + return result.Error + } + + result = tx.Select(clause.Associations).Delete(account) + if result.Error != nil { + return result.Error + } + + return nil + }) + + took := time.Since(start) + if s.metrics != nil { + s.metrics.StoreMetrics().CountPersistenceDuration(took) + } + log.Debugf("took %d ms to delete an account to the SQLite", took.Milliseconds()) + + return err +} + func (s *SqliteStore) SaveInstallationID(ID string) error { installation := installation{InstallationIDValue: ID} installation.ID = uint(s.installationPK) @@ -219,8 +253,8 @@ func (s *SqliteStore) GetInstallationID() string { return installation.InstallationIDValue } -func (s *SqliteStore) SavePeerStatus(accountID, peerID string, peerStatus PeerStatus) error { - var peer Peer +func (s *SqliteStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer.PeerStatus) error { + var peer nbpeer.Peer result := s.db.First(&peer, "account_id = ? and id = ?", accountID, peerID) if result.Error != nil { @@ -298,7 +332,7 @@ func (s *SqliteStore) GetUserByTokenID(tokenID string) (*User, error) { user.PATs = make(map[string]*PersonalAccessToken, len(user.PATsG)) for _, pat := range user.PATsG { - user.PATs[pat.ID] = &pat + user.PATs[pat.ID] = pat.Copy() } return &user, nil @@ -336,7 +370,7 @@ func (s *SqliteStore) GetAccount(accountID string) (*Account, error) { var rules []*PolicyRule err := s.db.Model(&PolicyRule{}).Find(&rules, "policy_id = ?", policy.ID).Error if err != nil { - return nil, status.Errorf(status.NotFound, "account not found") + return nil, status.Errorf(status.NotFound, "rule not found") } account.Policies[i].Rules = rules } @@ -347,7 +381,7 @@ func (s *SqliteStore) GetAccount(accountID string) (*Account, error) { } account.SetupKeysG = nil - account.Peers = make(map[string]*Peer, len(account.PeersG)) + account.Peers = make(map[string]*nbpeer.Peer, len(account.PeersG)) for _, peer := range account.PeersG { account.Peers[peer.ID] = peer.Copy() } @@ -405,7 +439,7 @@ func (s *SqliteStore) GetAccountByUser(userID string) (*Account, error) { } func (s *SqliteStore) GetAccountByPeerID(peerID string) (*Account, error) { - var peer Peer + var peer nbpeer.Peer result := s.db.Select("account_id").First(&peer, "id = ?", peerID) if result.Error != nil { return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") @@ -419,7 +453,7 @@ func (s *SqliteStore) GetAccountByPeerID(peerID string) (*Account, error) { } func (s *SqliteStore) GetAccountByPeerPubKey(peerKey string) (*Account, error) { - var peer Peer + var peer nbpeer.Peer result := s.db.Select("account_id").First(&peer, "key = ?", peerKey) if result.Error != nil { diff --git a/management/server/sqlite_store_test.go b/management/server/sqlite_store_test.go index 4a16e2525..e493368fa 100644 --- a/management/server/sqlite_store_test.go +++ b/management/server/sqlite_store_test.go @@ -9,9 +9,11 @@ import ( "time" "github.com/google/uuid" - "github.com/netbirdio/netbird/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/util" ) func TestSqlite_NewStore(t *testing.T) { @@ -36,13 +38,13 @@ func TestSqlite_SaveAccount(t *testing.T) { account := newAccountWithId("account_id", "testuser", "") setupKey := GenerateDefaultSetupKey() account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &Peer{ + account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, - Meta: PeerSystemMeta{}, + Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", - Status: &PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } err := store.SaveAccount(account) @@ -51,13 +53,13 @@ func TestSqlite_SaveAccount(t *testing.T) { account2 := newAccountWithId("account_id2", "testuser2", "") setupKey = GenerateDefaultSetupKey() account2.SetupKeys[setupKey.Key] = setupKey - account2.Peers["testpeer2"] = &Peer{ + account2.Peers["testpeer2"] = &nbpeer.Peer{ Key: "peerkey2", SetupKey: "peerkeysetupkey2", IP: net.IP{127, 0, 0, 2}, - Meta: PeerSystemMeta{}, + Meta: nbpeer.PeerSystemMeta{}, Name: "peer name 2", - Status: &PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } err = store.SaveAccount(account2) @@ -98,6 +100,80 @@ func TestSqlite_SaveAccount(t *testing.T) { } } +func TestSqlite_DeleteAccount(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + store := newSqliteStore(t) + + testUserID := "testuser" + user := NewAdminUser(testUserID) + user.PATs = map[string]*PersonalAccessToken{"testtoken": { + ID: "testtoken", + Name: "test token", + }} + + account := newAccountWithId("account_id", testUserID, "") + setupKey := GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + SetupKey: "peerkeysetupkey", + IP: net.IP{127, 0, 0, 1}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } + account.Users[testUserID] = user + + err := store.SaveAccount(account) + require.NoError(t, err) + + if len(store.GetAllAccounts()) != 1 { + t.Errorf("expecting 1 Accounts to be stored after SaveAccount()") + } + + err = store.DeleteAccount(account) + require.NoError(t, err) + + if len(store.GetAllAccounts()) != 0 { + t.Errorf("expecting 0 Accounts to be stored after DeleteAccount()") + } + + _, err = store.GetAccountByPeerPubKey("peerkey") + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer public key") + + _, err = store.GetAccountByUser("testuser") + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by user") + + _, err = store.GetAccountByPeerID("testpeer") + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer id") + + _, err = store.GetAccountBySetupKey(setupKey.Key) + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by setup key") + + _, err = store.GetAccount(account.Id) + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by id") + + for _, policy := range account.Policies { + var rules []*PolicyRule + err = store.db.Model(&PolicyRule{}).Find(&rules, "policy_id = ?", policy.ID).Error + require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for policy rules") + require.Len(t, rules, 0, "expecting no policy rules to be found after removing DeleteAccount") + + } + + for _, accountUser := range account.Users { + var pats []*PersonalAccessToken + err = store.db.Model(&PersonalAccessToken{}).Find(&pats, "user_id = ?", accountUser.Id).Error + require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for personal access token") + require.Len(t, pats, 0, "expecting no personal access token to be found after removing DeleteAccount") + + } + +} + func TestSqlite_SavePeerStatus(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") @@ -109,19 +185,19 @@ func TestSqlite_SavePeerStatus(t *testing.T) { require.NoError(t, err) // save status of non-existing peer - newStatus := PeerStatus{Connected: true, LastSeen: time.Now().UTC()} + newStatus := nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()} err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus) assert.Error(t, err) // save new status of existing peer - account.Peers["testpeer"] = &Peer{ + account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", ID: "testpeer", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, - Meta: PeerSystemMeta{}, + Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", - Status: &PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, + Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, } err = store.SaveAccount(account) @@ -216,13 +292,13 @@ func newAccount(store Store, id int) error { account := newAccountWithId(str, str+"-testuser", "example.com") setupKey := GenerateDefaultSetupKey() account.SetupKeys[setupKey.Key] = setupKey - account.Peers["p"+str] = &Peer{ + account.Peers["p"+str] = &nbpeer.Peer{ Key: "peerkey" + str, SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, - Meta: PeerSystemMeta{}, + Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", - Status: &PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } return store.SaveAccount(account) diff --git a/management/server/store.go b/management/server/store.go index 66b239f96..a482ca947 100644 --- a/management/server/store.go +++ b/management/server/store.go @@ -8,12 +8,14 @@ import ( log "github.com/sirupsen/logrus" + nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/telemetry" ) type Store interface { GetAllAccounts() []*Account GetAccount(accountID string) (*Account, error) + DeleteAccount(account *Account) error GetAccountByUser(userID string) (*Account, error) GetAccountByPeerPubKey(peerKey string) (*Account, error) GetAccountByPeerID(peerID string) (*Account, error) @@ -30,7 +32,7 @@ type Store interface { AcquireAccountLock(accountID string) func() // AcquireGlobalLock should attempt to acquire a global lock and return a function that releases the lock AcquireGlobalLock() func() - SavePeerStatus(accountID, peerID string, status PeerStatus) error + SavePeerStatus(accountID, peerID string, status nbpeer.PeerStatus) error SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error // Close should close the store persisting all unsaved data. Close() error diff --git a/management/server/store_test.go b/management/server/store_test.go index 72bbaf949..3f8c5d18b 100644 --- a/management/server/store_test.go +++ b/management/server/store_test.go @@ -14,11 +14,13 @@ type benchCase struct { } var newFs = func(b *testing.B) Store { + b.Helper() store, _ := NewFileStore(b.TempDir(), nil) return store } var newSqlite = func(b *testing.B) Store { + b.Helper() store, _ := NewSqliteStore(b.TempDir(), nil) return store } diff --git a/management/server/telemetry/app_metrics.go b/management/server/telemetry/app_metrics.go index de5d278b9..56f4fb9c8 100644 --- a/management/server/telemetry/app_metrics.go +++ b/management/server/telemetry/app_metrics.go @@ -20,13 +20,14 @@ const defaultEndpoint = "/metrics" // MockAppMetrics mocks the AppMetrics interface type MockAppMetrics struct { - GetMeterFunc func() metric2.Meter - CloseFunc func() error - ExposeFunc func(port int, endpoint string) error - IDPMetricsFunc func() *IDPMetrics - HTTPMiddlewareFunc func() *HTTPMiddleware - GRPCMetricsFunc func() *GRPCMetrics - StoreMetricsFunc func() *StoreMetrics + GetMeterFunc func() metric2.Meter + CloseFunc func() error + ExposeFunc func(port int, endpoint string) error + IDPMetricsFunc func() *IDPMetrics + HTTPMiddlewareFunc func() *HTTPMiddleware + GRPCMetricsFunc func() *GRPCMetrics + StoreMetricsFunc func() *StoreMetrics + UpdateChannelMetricsFunc func() *UpdateChannelMetrics } // GetMeter mocks the GetMeter function of the AppMetrics interface @@ -85,6 +86,14 @@ func (mock *MockAppMetrics) StoreMetrics() *StoreMetrics { return nil } +// UpdateChannelMetrics mocks the MockAppMetrics function of the UpdateChannelMetrics interface +func (mock *MockAppMetrics) UpdateChannelMetrics() *UpdateChannelMetrics { + if mock.UpdateChannelMetricsFunc != nil { + return mock.UpdateChannelMetricsFunc() + } + return nil +} + // AppMetrics is metrics interface type AppMetrics interface { GetMeter() metric2.Meter @@ -94,18 +103,20 @@ type AppMetrics interface { HTTPMiddleware() *HTTPMiddleware GRPCMetrics() *GRPCMetrics StoreMetrics() *StoreMetrics + UpdateChannelMetrics() *UpdateChannelMetrics } // defaultAppMetrics are core application metrics based on OpenTelemetry https://opentelemetry.io/ type defaultAppMetrics struct { // Meter can be used by different application parts to create counters and measure things - Meter metric2.Meter - listener net.Listener - ctx context.Context - idpMetrics *IDPMetrics - httpMiddleware *HTTPMiddleware - grpcMetrics *GRPCMetrics - storeMetrics *StoreMetrics + Meter metric2.Meter + listener net.Listener + ctx context.Context + idpMetrics *IDPMetrics + httpMiddleware *HTTPMiddleware + grpcMetrics *GRPCMetrics + storeMetrics *StoreMetrics + updateChannelMetrics *UpdateChannelMetrics } // IDPMetrics returns metrics for the idp package @@ -128,6 +139,11 @@ func (appMetrics *defaultAppMetrics) StoreMetrics() *StoreMetrics { return appMetrics.storeMetrics } +// UpdateChannelMetrics returns metrics for the updatechannel +func (appMetrics *defaultAppMetrics) UpdateChannelMetrics() *UpdateChannelMetrics { + return appMetrics.updateChannelMetrics +} + // Close stop application metrics HTTP handler and closes listener. func (appMetrics *defaultAppMetrics) Close() error { if appMetrics.listener == nil { @@ -199,6 +215,18 @@ func NewDefaultAppMetrics(ctx context.Context) (AppMetrics, error) { return nil, err } - return &defaultAppMetrics{Meter: meter, ctx: ctx, idpMetrics: idpMetrics, httpMiddleware: middleware, - grpcMetrics: grpcMetrics, storeMetrics: storeMetrics}, nil + updateChannelMetrics, err := NewUpdateChannelMetrics(ctx, meter) + if err != nil { + return nil, err + } + + return &defaultAppMetrics{ + Meter: meter, + ctx: ctx, + idpMetrics: idpMetrics, + httpMiddleware: middleware, + grpcMetrics: grpcMetrics, + storeMetrics: storeMetrics, + updateChannelMetrics: updateChannelMetrics, + }, nil } diff --git a/management/server/telemetry/grpc_metrics.go b/management/server/telemetry/grpc_metrics.go index 25789f5c7..8ad2f3bde 100644 --- a/management/server/telemetry/grpc_metrics.go +++ b/management/server/telemetry/grpc_metrics.go @@ -116,6 +116,6 @@ func (grpcMetrics *GRPCMetrics) RegisterConnectedStreams(producer func() int64) } // UpdateChannelQueueLength update the histogram that keep distribution of the update messages channel queue -func (metrics *GRPCMetrics) UpdateChannelQueueLength(len int) { - metrics.channelQueueLength.Record(metrics.ctx, int64(len)) +func (metrics *GRPCMetrics) UpdateChannelQueueLength(length int) { + metrics.channelQueueLength.Record(metrics.ctx, int64(length)) } diff --git a/management/server/telemetry/store_metrics.go b/management/server/telemetry/store_metrics.go index 98c13f12a..6415f765e 100644 --- a/management/server/telemetry/store_metrics.go +++ b/management/server/telemetry/store_metrics.go @@ -9,7 +9,7 @@ import ( "go.opentelemetry.io/otel/metric/instrument/syncint64" ) -// StoreMetrics represents all metrics related to the FileStore +// StoreMetrics represents all metrics related to the Store type StoreMetrics struct { globalLockAcquisitionDurationMicro syncint64.Histogram globalLockAcquisitionDurationMs syncint64.Histogram diff --git a/management/server/telemetry/updatechannel_metrics.go b/management/server/telemetry/updatechannel_metrics.go new file mode 100644 index 000000000..7abe34354 --- /dev/null +++ b/management/server/telemetry/updatechannel_metrics.go @@ -0,0 +1,113 @@ +package telemetry + +import ( + "context" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/instrument/syncint64" +) + +// UpdateChannelMetrics represents all metrics related to the UpdateChannel +type UpdateChannelMetrics struct { + createChannelDurationMicro syncint64.Histogram + closeChannelDurationMicro syncint64.Histogram + closeChannelsDurationMicro syncint64.Histogram + closeChannels syncint64.Histogram + sendUpdateDurationMicro syncint64.Histogram + getAllConnectedPeersDurationMicro syncint64.Histogram + getAllConnectedPeers syncint64.Histogram + hasChannelDurationMicro syncint64.Histogram + ctx context.Context +} + +// NewUpdateChannelMetrics creates an instance of UpdateChannel +func NewUpdateChannelMetrics(ctx context.Context, meter metric.Meter) (*UpdateChannelMetrics, error) { + createChannelDurationMicro, err := meter.SyncInt64().Histogram("management.updatechannel.create.duration.micro") + if err != nil { + return nil, err + } + + closeChannelDurationMicro, err := meter.SyncInt64().Histogram("management.updatechannel.close.one.duration.micro") + if err != nil { + return nil, err + } + + closeChannelsDurationMicro, err := meter.SyncInt64().Histogram("management.updatechannel.close.multiple.duration.micro") + if err != nil { + return nil, err + } + + closeChannels, err := meter.SyncInt64().Histogram("management.updatechannel.close.multiple.channels") + if err != nil { + return nil, err + } + + sendUpdateDurationMicro, err := meter.SyncInt64().Histogram("management.updatechannel.send.duration.micro") + if err != nil { + return nil, err + } + + getAllConnectedPeersDurationMicro, err := meter.SyncInt64().Histogram("management.updatechannel.get.all.duration.micro") + if err != nil { + return nil, err + } + + getAllConnectedPeers, err := meter.SyncInt64().Histogram("management.updatechannel.get.all.peers") + if err != nil { + return nil, err + } + + hasChannelDurationMicro, err := meter.SyncInt64().Histogram("management.updatechannel.haschannel.duration.micro") + if err != nil { + return nil, err + } + + return &UpdateChannelMetrics{ + createChannelDurationMicro: createChannelDurationMicro, + closeChannelDurationMicro: closeChannelDurationMicro, + closeChannelsDurationMicro: closeChannelsDurationMicro, + closeChannels: closeChannels, + sendUpdateDurationMicro: sendUpdateDurationMicro, + getAllConnectedPeersDurationMicro: getAllConnectedPeersDurationMicro, + getAllConnectedPeers: getAllConnectedPeers, + hasChannelDurationMicro: hasChannelDurationMicro, + ctx: ctx, + }, nil +} + +// CountCreateChannelDuration counts the duration of the CreateChannel method, +// closed indicates if existing channel was closed before creation of a new one +func (metrics *UpdateChannelMetrics) CountCreateChannelDuration(duration time.Duration, closed bool) { + metrics.createChannelDurationMicro.Record(metrics.ctx, duration.Microseconds(), attribute.Bool("closed", closed)) +} + +// CountCloseChannelDuration counts the duration of the CloseChannel method +func (metrics *UpdateChannelMetrics) CountCloseChannelDuration(duration time.Duration) { + metrics.closeChannelDurationMicro.Record(metrics.ctx, duration.Microseconds()) +} + +// CountCloseChannelsDuration counts the duration of the CloseChannels method and the number of channels have been closed +func (metrics *UpdateChannelMetrics) CountCloseChannelsDuration(duration time.Duration, channels int) { + metrics.closeChannelsDurationMicro.Record(metrics.ctx, duration.Microseconds()) + metrics.closeChannels.Record(metrics.ctx, int64(channels)) +} + +// CountSendUpdateDuration counts the duration of the SendUpdate method +// found indicates if peer had channel, dropped indicates if the message was dropped due channel buffer overload +func (metrics *UpdateChannelMetrics) CountSendUpdateDuration(duration time.Duration, found, dropped bool) { + attrs := []attribute.KeyValue{attribute.Bool("found", found), attribute.Bool("dropped", dropped)} + metrics.sendUpdateDurationMicro.Record(metrics.ctx, duration.Microseconds(), attrs...) +} + +// CountGetAllConnectedPeersDuration counts the duration of the GetAllConnectedPeers method and the number of peers have been returned +func (metrics *UpdateChannelMetrics) CountGetAllConnectedPeersDuration(duration time.Duration, peers int) { + metrics.getAllConnectedPeersDurationMicro.Record(metrics.ctx, duration.Microseconds()) + metrics.getAllConnectedPeers.Record(metrics.ctx, int64(peers)) +} + +// CountHasChannelDuration counts the duration of the HasChannel method +func (metrics *UpdateChannelMetrics) CountHasChannelDuration(duration time.Duration) { + metrics.hasChannelDurationMicro.Record(metrics.ctx, duration.Microseconds()) +} diff --git a/management/server/turncredentials_test.go b/management/server/turncredentials_test.go index c91176ac3..5066fdbe9 100644 --- a/management/server/turncredentials_test.go +++ b/management/server/turncredentials_test.go @@ -4,9 +4,10 @@ import ( "crypto/hmac" "crypto/sha1" "encoding/base64" - "github.com/netbirdio/netbird/util" "testing" "time" + + "github.com/netbirdio/netbird/util" ) var TurnTestHost = &Host{ @@ -19,7 +20,7 @@ var TurnTestHost = &Host{ func TestTimeBasedAuthSecretsManager_GenerateCredentials(t *testing.T) { ttl := util.Duration{Duration: time.Hour} secret := "some_secret" - peersManager := NewPeersUpdateManager() + peersManager := NewPeersUpdateManager(nil) tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{ CredentialsTTL: ttl, @@ -36,14 +37,14 @@ func TestTimeBasedAuthSecretsManager_GenerateCredentials(t *testing.T) { t.Errorf("expected generated TURN password not to be empty, got empty") } - validateMAC(credentials.Username, credentials.Password, []byte(secret), t) + validateMAC(t, credentials.Username, credentials.Password, []byte(secret)) } func TestTimeBasedAuthSecretsManager_SetupRefresh(t *testing.T) { ttl := util.Duration{Duration: 2 * time.Second} secret := "some_secret" - peersManager := NewPeersUpdateManager() + peersManager := NewPeersUpdateManager(nil) peer := "some_peer" updateChannel := peersManager.CreateChannel(peer) @@ -92,7 +93,7 @@ loop: func TestTimeBasedAuthSecretsManager_CancelRefresh(t *testing.T) { ttl := util.Duration{Duration: time.Hour} secret := "some_secret" - peersManager := NewPeersUpdateManager() + peersManager := NewPeersUpdateManager(nil) peer := "some_peer" tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{ @@ -112,7 +113,8 @@ func TestTimeBasedAuthSecretsManager_CancelRefresh(t *testing.T) { } } -func validateMAC(username string, actualMAC string, key []byte, t *testing.T) { +func validateMAC(t *testing.T, username string, actualMAC string, key []byte) { + t.Helper() mac := hmac.New(sha1.New, key) _, err := mac.Write([]byte(username)) diff --git a/management/server/updatechannel.go b/management/server/updatechannel.go index 5e6bcbb1c..f760c5a75 100644 --- a/management/server/updatechannel.go +++ b/management/server/updatechannel.go @@ -2,10 +2,12 @@ package server import ( "sync" + "time" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/management/server/telemetry" ) const channelBufferSize = 100 @@ -17,26 +19,41 @@ type UpdateMessage struct { type PeersUpdateManager struct { // peerChannels is an update channel indexed by Peer.ID peerChannels map[string]chan *UpdateMessage - channelsMux *sync.Mutex + // channelsMux keeps the mutex to access peerChannels + channelsMux *sync.Mutex + // metrics provides method to collect application metrics + metrics telemetry.AppMetrics } // NewPeersUpdateManager returns a new instance of PeersUpdateManager -func NewPeersUpdateManager() *PeersUpdateManager { +func NewPeersUpdateManager(metrics telemetry.AppMetrics) *PeersUpdateManager { return &PeersUpdateManager{ peerChannels: make(map[string]chan *UpdateMessage), channelsMux: &sync.Mutex{}, + metrics: metrics, } } // SendUpdate sends update message to the peer's channel func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) { + start := time.Now() + var found, dropped bool + p.channelsMux.Lock() - defer p.channelsMux.Unlock() + defer func() { + p.channelsMux.Unlock() + if p.metrics != nil { + p.metrics.UpdateChannelMetrics().CountSendUpdateDuration(time.Since(start), found, dropped) + } + }() + if channel, ok := p.peerChannels[peerID]; ok { + found = true select { case channel <- update: log.Debugf("update was sent to channel for peer %s", peerID) default: + dropped = true log.Warnf("channel for peer %s is %d full", peerID, len(channel)) } } else { @@ -46,10 +63,20 @@ func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) { // CreateChannel creates a go channel for a given peer used to deliver updates relevant to the peer. func (p *PeersUpdateManager) CreateChannel(peerID string) chan *UpdateMessage { + start := time.Now() + + closed := false + p.channelsMux.Lock() - defer p.channelsMux.Unlock() + defer func() { + p.channelsMux.Unlock() + if p.metrics != nil { + p.metrics.UpdateChannelMetrics().CountCreateChannelDuration(time.Since(start), closed) + } + }() if channel, ok := p.peerChannels[peerID]; ok { + closed = true delete(p.peerChannels, peerID) close(channel) } @@ -58,6 +85,7 @@ func (p *PeersUpdateManager) CreateChannel(peerID string) chan *UpdateMessage { p.peerChannels[peerID] = channel log.Debugf("opened updates channel for a peer %s", peerID) + return channel } @@ -72,8 +100,16 @@ func (p *PeersUpdateManager) closeChannel(peerID string) { // CloseChannels closes updates channel for each given peer func (p *PeersUpdateManager) CloseChannels(peerIDs []string) { + start := time.Now() + p.channelsMux.Lock() - defer p.channelsMux.Unlock() + defer func() { + p.channelsMux.Unlock() + if p.metrics != nil { + p.metrics.UpdateChannelMetrics().CountCloseChannelsDuration(time.Since(start), len(peerIDs)) + } + }() + for _, id := range peerIDs { p.closeChannel(id) } @@ -81,18 +117,55 @@ func (p *PeersUpdateManager) CloseChannels(peerIDs []string) { // CloseChannel closes updates channel of a given peer func (p *PeersUpdateManager) CloseChannel(peerID string) { + start := time.Now() + p.channelsMux.Lock() - defer p.channelsMux.Unlock() + defer func() { + p.channelsMux.Unlock() + if p.metrics != nil { + p.metrics.UpdateChannelMetrics().CountCloseChannelDuration(time.Since(start)) + } + }() + p.closeChannel(peerID) } // GetAllConnectedPeers returns a copy of the connected peers map func (p *PeersUpdateManager) GetAllConnectedPeers() map[string]struct{} { + start := time.Now() + p.channelsMux.Lock() - defer p.channelsMux.Unlock() + m := make(map[string]struct{}) + + defer func() { + p.channelsMux.Unlock() + if p.metrics != nil { + p.metrics.UpdateChannelMetrics().CountGetAllConnectedPeersDuration(time.Since(start), len(m)) + } + }() + for ID := range p.peerChannels { m[ID] = struct{}{} } + return m } + +// HasChannel returns true if peers has channel in update manager, otherwise false +func (p *PeersUpdateManager) HasChannel(peerID string) bool { + start := time.Now() + + p.channelsMux.Lock() + + defer func() { + p.channelsMux.Unlock() + if p.metrics != nil { + p.metrics.UpdateChannelMetrics().CountHasChannelDuration(time.Since(start)) + } + }() + + _, ok := p.peerChannels[peerID] + + return ok +} diff --git a/management/server/updatechannel_test.go b/management/server/updatechannel_test.go index 6cfb4d52f..187e404c5 100644 --- a/management/server/updatechannel_test.go +++ b/management/server/updatechannel_test.go @@ -1,16 +1,17 @@ package server import ( - "github.com/netbirdio/netbird/management/proto" "testing" "time" + + "github.com/netbirdio/netbird/management/proto" ) //var peersUpdater *PeersUpdateManager func TestCreateChannel(t *testing.T) { peer := "test-create" - peersUpdater := NewPeersUpdateManager() + peersUpdater := NewPeersUpdateManager(nil) defer peersUpdater.CloseChannel(peer) _ = peersUpdater.CreateChannel(peer) @@ -21,7 +22,7 @@ func TestCreateChannel(t *testing.T) { func TestSendUpdate(t *testing.T) { peer := "test-sendupdate" - peersUpdater := NewPeersUpdateManager() + peersUpdater := NewPeersUpdateManager(nil) update1 := &UpdateMessage{Update: &proto.SyncResponse{ NetworkMap: &proto.NetworkMap{ Serial: 0, @@ -65,7 +66,7 @@ func TestSendUpdate(t *testing.T) { func TestCloseChannel(t *testing.T) { peer := "test-close" - peersUpdater := NewPeersUpdateManager() + peersUpdater := NewPeersUpdateManager(nil) _ = peersUpdater.CreateChannel(peer) if _, ok := peersUpdater.peerChannels[peer]; !ok { t.Error("Error creating the channel") diff --git a/management/server/user.go b/management/server/user.go index 22edd2c2c..a84765cb1 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -11,10 +11,12 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "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" ) const ( + UserRoleOwner UserRole = "owner" UserRoleAdmin UserRole = "admin" UserRoleUser UserRole = "user" UserRoleUnknown UserRole = "unknown" @@ -30,6 +32,8 @@ const ( // StrRoleToUserRole returns UserRole for a given strRole or UserRoleUnknown if the specified role is unknown func StrRoleToUserRole(strRole string) UserRole { switch strings.ToLower(strRole) { + case "owner": + return UserRoleOwner case "admin": return UserRoleAdmin case "user": @@ -52,7 +56,14 @@ type IntegrationReference struct { } func (ir IntegrationReference) String() string { - return fmt.Sprintf("%d:%s", ir.ID, ir.IntegrationType) + return fmt.Sprintf("%s:%d", ir.IntegrationType, ir.ID) +} + +func (ir IntegrationReference) CacheKey(path ...string) string { + if len(path) == 0 { + return ir.String() + } + return fmt.Sprintf("%s:%s", ir.String(), strings.Join(path, ":")) } // User represents a user of the system @@ -62,6 +73,8 @@ type User struct { AccountID string `json:"-" gorm:"index"` Role UserRole IsServiceUser bool + // NonDeletable indicates whether the service user can be deleted + NonDeletable bool // ServiceUserName is only set if IsServiceUser is true ServiceUserName string // AutoGroups is a list of Group IDs to auto-assign to peers registered by this user @@ -88,9 +101,9 @@ func (u *User) LastDashboardLoginChanged(LastLogin time.Time) bool { return LastLogin.After(u.LastLogin) && !u.LastLogin.IsZero() } -// IsAdmin returns true if the user is an admin, false otherwise -func (u *User) IsAdmin() bool { - return u.Role == UserRoleAdmin +// HasAdminPower returns true if the user has admin or owner roles, false otherwise +func (u *User) HasAdminPower() bool { + return u.Role == UserRoleAdmin || u.Role == UserRoleOwner } // ToUserInfo converts a User object to a UserInfo object. @@ -151,6 +164,7 @@ func (u *User) Copy() *User { Role: u.Role, AutoGroups: autoGroups, IsServiceUser: u.IsServiceUser, + NonDeletable: u.NonDeletable, ServiceUserName: u.ServiceUserName, PATs: pats, Blocked: u.Blocked, @@ -161,11 +175,12 @@ func (u *User) Copy() *User { } // NewUser creates a new user -func NewUser(id string, role UserRole, isServiceUser bool, serviceUserName string, autoGroups []string, issued string) *User { +func NewUser(id string, role UserRole, isServiceUser bool, nonDeletable bool, serviceUserName string, autoGroups []string, issued string) *User { return &User{ Id: id, Role: role, IsServiceUser: isServiceUser, + NonDeletable: nonDeletable, ServiceUserName: serviceUserName, AutoGroups: autoGroups, Issued: issued, @@ -174,16 +189,21 @@ func NewUser(id string, role UserRole, isServiceUser bool, serviceUserName strin // NewRegularUser creates a new user with role UserRoleUser func NewRegularUser(id string) *User { - return NewUser(id, UserRoleUser, false, "", []string{}, UserIssuedAPI) + return NewUser(id, UserRoleUser, false, false, "", []string{}, UserIssuedAPI) } // NewAdminUser creates a new user with role UserRoleAdmin func NewAdminUser(id string) *User { - return NewUser(id, UserRoleAdmin, false, "", []string{}, UserIssuedAPI) + return NewUser(id, UserRoleAdmin, false, false, "", []string{}, UserIssuedAPI) +} + +// NewOwnerUser creates a new user with role UserRoleOwner +func NewOwnerUser(id string) *User { + return NewUser(id, UserRoleOwner, false, false, "", []string{}, UserIssuedAPI) } // createServiceUser creates a new service user under the given account. -func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUserID string, role UserRole, serviceUserName string, autoGroups []string) (*UserInfo, error) { +func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUserID string, role UserRole, serviceUserName string, nonDeletable bool, autoGroups []string) (*UserInfo, error) { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -196,12 +216,16 @@ func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUs if executingUser == nil { return nil, status.Errorf(status.NotFound, "user not found") } - if executingUser.Role != UserRoleAdmin { - return nil, status.Errorf(status.PermissionDenied, "only admins can create service users") + if !executingUser.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power can create service users") + } + + if role == UserRoleOwner { + return nil, status.Errorf(status.InvalidArgument, "can't create a service user with owner role") } newUserID := uuid.New().String() - newUser := NewUser(newUserID, role, true, serviceUserName, autoGroups, UserIssuedAPI) + newUser := NewUser(newUserID, role, true, nonDeletable, serviceUserName, autoGroups, UserIssuedAPI) log.Debugf("New User: %v", newUser) account.Users[newUserID] = newUser @@ -211,7 +235,7 @@ func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUs } meta := map[string]any{"name": newUser.ServiceUserName} - am.storeEvent(initiatorUserID, newUser.Id, accountID, activity.ServiceUserCreated, meta) + am.StoreEvent(initiatorUserID, newUser.Id, accountID, activity.ServiceUserCreated, meta) return &UserInfo{ ID: newUser.Id, @@ -229,7 +253,7 @@ func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUs // CreateUser creates a new user under the given account. Effectively this is a user invite. func (am *DefaultAccountManager) CreateUser(accountID, userID string, user *UserInfo) (*UserInfo, error) { if user.IsServiceUser { - return am.createServiceUser(accountID, userID, StrRoleToUserRole(user.Role), user.Name, user.AutoGroups) + return am.createServiceUser(accountID, userID, StrRoleToUserRole(user.Role), user.Name, user.NonDeletable, user.AutoGroups) } return am.inviteNewUser(accountID, userID, user) } @@ -247,6 +271,18 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite return nil, fmt.Errorf("provided user update is nil") } + invitedRole := StrRoleToUserRole(invite.Role) + + switch { + case invite.Name == "": + return nil, status.Errorf(status.InvalidArgument, "name can't be empty") + case invite.Email == "": + return nil, status.Errorf(status.InvalidArgument, "email can't be empty") + case invitedRole == UserRoleOwner: + return nil, status.Errorf(status.InvalidArgument, "can't invite a user with owner role") + default: + } + account, err := am.Store.GetAccount(accountID) if err != nil { return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID) @@ -292,10 +328,9 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite return nil, err } - role := StrRoleToUserRole(invite.Role) newUser := &User{ Id: idpUser.ID, - Role: role, + Role: invitedRole, AutoGroups: invite.AutoGroups, Issued: invite.Issued, IntegrationReference: invite.IntegrationReference, @@ -312,7 +347,7 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite return nil, err } - am.storeEvent(userID, newUser.Id, accountID, activity.UserInvited, nil) + am.StoreEvent(userID, newUser.Id, accountID, activity.UserInvited, nil) return newUser.ToUserInfo(idpUser) } @@ -349,15 +384,34 @@ func (am *DefaultAccountManager) GetUser(claims jwtclaims.AuthorizationClaims) ( if newLogin { meta := map[string]any{"timestamp": claims.LastLogin} - am.storeEvent(claims.UserId, claims.UserId, account.Id, activity.DashboardLogin, meta) + am.StoreEvent(claims.UserId, claims.UserId, account.Id, activity.DashboardLogin, meta) } return user, nil } +// ListUsers returns lists of all users under the account. +// It doesn't populate user information such as email or name. +func (am *DefaultAccountManager) ListUsers(accountID string) ([]*User, error) { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, err + } + + users := make([]*User, 0, len(account.Users)) + for _, item := range account.Users { + users = append(users, item) + } + + return users, nil +} + func (am *DefaultAccountManager) deleteServiceUser(account *Account, initiatorUserID string, targetUser *User) { meta := map[string]any{"name": targetUser.ServiceUserName} - am.storeEvent(initiatorUserID, targetUser.Id, account.Id, activity.ServiceUserDeleted, meta) + am.StoreEvent(initiatorUserID, targetUser.Id, account.Id, activity.ServiceUserDeleted, meta) delete(account.Users, targetUser.Id) } @@ -378,8 +432,8 @@ func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, t if executingUser == nil { return status.Errorf(status.NotFound, "user not found") } - if executingUser.Role != UserRoleAdmin { - return status.Errorf(status.PermissionDenied, "only admins can delete users") + if !executingUser.HasAdminPower() { + return status.Errorf(status.PermissionDenied, "only users with admin power can delete users") } targetUser := account.Users[targetUserID] @@ -387,12 +441,21 @@ func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, t return status.Errorf(status.NotFound, "target user not found") } - if targetUser.Issued == UserIssuedIntegration { - return status.Errorf(status.PermissionDenied, "only integration can delete this user") + if targetUser.Role == UserRoleOwner { + return status.Errorf(status.PermissionDenied, "unable to delete a user with owner role") + } + + // disable deleting integration user if the initiator is not admin service user + if targetUser.Issued == UserIssuedIntegration && !executingUser.IsServiceUser { + return status.Errorf(status.PermissionDenied, "only integration service user can delete this user") } // handle service user first and exit, no need to fetch extra data from IDP, etc if targetUser.IsServiceUser { + if targetUser.NonDeletable { + return status.Errorf(status.PermissionDenied, "service user is marked as non-deletable") + } + am.deleteServiceUser(account, initiatorUserID, targetUser) return am.Store.SaveAccount(account) } @@ -408,10 +471,17 @@ func (am *DefaultAccountManager) deleteRegularUser(account *Account, initiatorUs } if !isNil(am.idpManager) { - err = am.deleteUserFromIDP(targetUserID, account.Id) - if err != nil { - log.Debugf("failed to delete user from IDP: %s", targetUserID) - return err + // 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(targetUserID, idp.AppMetadata{WTAccountID: account.Id}) + if err == nil { + err = am.deleteUserFromIDP(targetUserID, account.Id) + if err != nil { + log.Debugf("failed to delete user from IDP: %s", targetUserID) + return err + } + } else { + log.Debugf("skipped deleting user %s from IDP, error: %v", targetUserID, err) } } @@ -427,7 +497,7 @@ func (am *DefaultAccountManager) deleteRegularUser(account *Account, initiatorUs } meta := map[string]any{"name": tuName, "email": tuEmail} - am.storeEvent(initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta) + am.StoreEvent(initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta) am.updateAccountPeers(account) @@ -483,7 +553,7 @@ func (am *DefaultAccountManager) InviteUser(accountID string, initiatorUserID st return err } - am.storeEvent(initiatorUserID, user.ID, accountID, activity.UserInvited, nil) + am.StoreEvent(initiatorUserID, user.ID, accountID, activity.UserInvited, nil) return nil } @@ -516,7 +586,7 @@ func (am *DefaultAccountManager) CreatePAT(accountID string, initiatorUserID str return nil, status.Errorf(status.NotFound, "user not found") } - if !(initiatorUserID == targetUserID || (executingUser.IsAdmin() && targetUser.IsServiceUser)) { + if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) { return nil, status.Errorf(status.PermissionDenied, "no permission to create PAT for this user") } @@ -533,7 +603,7 @@ func (am *DefaultAccountManager) CreatePAT(accountID string, initiatorUserID str } meta := map[string]any{"name": pat.Name, "is_service_user": targetUser.IsServiceUser, "user_name": targetUser.ServiceUserName} - am.storeEvent(initiatorUserID, targetUserID, accountID, activity.PersonalAccessTokenCreated, meta) + am.StoreEvent(initiatorUserID, targetUserID, accountID, activity.PersonalAccessTokenCreated, meta) return pat, nil } @@ -558,7 +628,7 @@ func (am *DefaultAccountManager) DeletePAT(accountID string, initiatorUserID str return status.Errorf(status.NotFound, "user not found") } - if !(initiatorUserID == targetUserID || (executingUser.IsAdmin() && targetUser.IsServiceUser)) { + if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) { return status.Errorf(status.PermissionDenied, "no permission to delete PAT for this user") } @@ -577,7 +647,7 @@ func (am *DefaultAccountManager) DeletePAT(accountID string, initiatorUserID str } meta := map[string]any{"name": pat.Name, "is_service_user": targetUser.IsServiceUser, "user_name": targetUser.ServiceUserName} - am.storeEvent(initiatorUserID, targetUserID, accountID, activity.PersonalAccessTokenDeleted, meta) + am.StoreEvent(initiatorUserID, targetUserID, accountID, activity.PersonalAccessTokenDeleted, meta) delete(targetUser.PATs, tokenID) @@ -608,7 +678,7 @@ func (am *DefaultAccountManager) GetPAT(accountID string, initiatorUserID string return nil, status.Errorf(status.NotFound, "user not found") } - if !(initiatorUserID == targetUserID || (executingUser.IsAdmin() && targetUser.IsServiceUser)) { + if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) { return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this userser") } @@ -640,7 +710,7 @@ func (am *DefaultAccountManager) GetAllPATs(accountID string, initiatorUserID st return nil, status.Errorf(status.NotFound, "user not found") } - if !(initiatorUserID == targetUserID || (executingUser.IsAdmin() && targetUser.IsServiceUser)) { + if !(initiatorUserID == targetUserID || (executingUser.HasAdminPower() && targetUser.IsServiceUser)) { return nil, status.Errorf(status.PermissionDenied, "no permission to get PAT for this user") } @@ -653,8 +723,13 @@ func (am *DefaultAccountManager) GetAllPATs(accountID string, initiatorUserID st } // SaveUser saves updates to the given user. If the user doesn't exit it will throw status.NotFound error. -// Only User.AutoGroups, User.Role, and User.Blocked fields are allowed to be updated for now. func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, update *User) (*UserInfo, error) { + return am.SaveOrAddUser(accountID, initiatorUserID, update, false) // false means do not create user and throw status.NotFound +} + +// SaveOrAddUser updates the given user. If addIfNotExists is set to true it will add user when no exist +// Only User.AutoGroups, User.Role, and User.Blocked fields are allowed to be updated for now. +func (am *DefaultAccountManager) SaveOrAddUser(accountID, initiatorUserID string, update *User, addIfNotExists bool) (*UserInfo, error) { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -672,27 +747,58 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd return nil, err } - if !initiatorUser.IsAdmin() || initiatorUser.IsBlocked() { - return nil, status.Errorf(status.PermissionDenied, "only admins are authorized to perform user update operations") + if !initiatorUser.HasAdminPower() || initiatorUser.IsBlocked() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power are authorized to perform user update operations") } oldUser := account.Users[update.Id] if oldUser == nil { - return nil, status.Errorf(status.NotFound, "user to update doesn't exist") + if !addIfNotExists { + return nil, status.Errorf(status.NotFound, "user to update doesn't exist") + } + // when addIfNotExists is set to true the newUser will use all fields from the update input + oldUser = update } - if initiatorUser.IsAdmin() && initiatorUserID == update.Id && oldUser.Blocked != update.Blocked { + if initiatorUser.HasAdminPower() && initiatorUserID == update.Id && oldUser.Blocked != update.Blocked { return nil, status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves") } - if initiatorUser.IsAdmin() && initiatorUserID == update.Id && update.Role != UserRoleAdmin { + if initiatorUser.HasAdminPower() && initiatorUserID == update.Id && update.Role != initiatorUser.Role { return nil, status.Errorf(status.PermissionDenied, "admins can't change their role") } - // only auto groups, revoked status, and name can be updated for now + if initiatorUser.Role == UserRoleAdmin && oldUser.Role == UserRoleOwner && update.Role != oldUser.Role { + return nil, status.Errorf(status.PermissionDenied, "only owners can remove owner role from their user") + } + + if initiatorUser.Role == UserRoleAdmin && oldUser.Role == UserRoleOwner && update.IsBlocked() && !oldUser.IsBlocked() { + return nil, status.Errorf(status.PermissionDenied, "unable to block owner user") + } + + if initiatorUser.Role == UserRoleAdmin && update.Role == UserRoleOwner && update.Role != oldUser.Role { + return nil, status.Errorf(status.PermissionDenied, "only owners can add owner role to other users") + } + + if oldUser.IsServiceUser && update.Role == UserRoleOwner { + return nil, status.Errorf(status.PermissionDenied, "can't update a service user with owner role") + } + + transferedOwnerRole := false + if initiatorUser.Role == UserRoleOwner && initiatorUserID != update.Id && update.Role == UserRoleOwner { + newInitiatorUser := initiatorUser.Copy() + newInitiatorUser.Role = UserRoleAdmin + account.Users[initiatorUserID] = newInitiatorUser + transferedOwnerRole = true + } + + // only auto groups, revoked status, and integration reference can be updated for now newUser := oldUser.Copy() newUser.Role = update.Role newUser.Blocked = update.Blocked + // these two fields can't be set via API, only via direct call to the method + newUser.Issued = update.Issued + newUser.IntegrationReference = update.IntegrationReference for _, newGroupID := range update.AutoGroups { if _, ok := account.Groups[newGroupID]; !ok { @@ -738,15 +844,18 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd defer func() { if oldUser.IsBlocked() != update.IsBlocked() { if update.IsBlocked() { - am.storeEvent(initiatorUserID, oldUser.Id, accountID, activity.UserBlocked, nil) + am.StoreEvent(initiatorUserID, oldUser.Id, accountID, activity.UserBlocked, nil) } else { - am.storeEvent(initiatorUserID, oldUser.Id, accountID, activity.UserUnblocked, nil) + am.StoreEvent(initiatorUserID, oldUser.Id, accountID, activity.UserUnblocked, nil) } } - // store activity logs - if oldUser.Role != newUser.Role { - am.storeEvent(initiatorUserID, oldUser.Id, accountID, activity.UserRoleUpdated, map[string]any{"role": newUser.Role}) + switch { + case transferedOwnerRole: + am.StoreEvent(initiatorUserID, oldUser.Id, accountID, activity.TransferredOwnerRole, nil) + case oldUser.Role != newUser.Role: + am.StoreEvent(initiatorUserID, oldUser.Id, accountID, activity.UserRoleUpdated, map[string]any{"role": newUser.Role}) + default: } if update.AutoGroups != nil { @@ -755,7 +864,7 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd for _, g := range removedGroups { group := account.GetGroup(g) if group != nil { - am.storeEvent(initiatorUserID, oldUser.Id, accountID, activity.GroupRemovedFromUser, + am.StoreEvent(initiatorUserID, oldUser.Id, accountID, activity.GroupRemovedFromUser, map[string]any{"group": group.Name, "group_id": group.ID, "is_service_user": newUser.IsServiceUser, "user_name": newUser.ServiceUserName}) } else { log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id) @@ -765,7 +874,7 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd for _, g := range addedGroups { group := account.GetGroup(g) if group != nil { - am.storeEvent(initiatorUserID, oldUser.Id, accountID, activity.GroupAddedToUser, + am.StoreEvent(initiatorUserID, oldUser.Id, accountID, activity.GroupAddedToUser, map[string]any{"group": group.Name, "group_id": group.ID, "is_service_user": newUser.IsServiceUser, "user_name": newUser.ServiceUserName}) } } @@ -778,7 +887,16 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd return nil, err } if userData == nil { - return nil, status.Errorf(status.NotFound, "user %s not found in the IdP", newUser.Id) + // lets check external cache + key := newUser.IntegrationReference.CacheKey(account.Id, newUser.Id) + log.Debugf("looking up user %s of account %s in external cache", key, account.Id) + info, err := am.externalCacheManager.Get(am.ctx, key) + if err != nil { + log.Infof("Get ExternalCache for key: %s, error: %s", key, err) + return nil, status.Errorf(status.NotFound, "user %s not found in the IdP", newUser.Id) + } + + return newUser.ToUserInfo(info) } return newUser.ToUserInfo(userData) } @@ -811,7 +929,7 @@ func (am *DefaultAccountManager) GetOrCreateAccountByUser(userID, domain string) userObj := account.Users[userID] - if account.Domain != lowerDomain && userObj.Role == UserRoleAdmin { + if account.Domain != lowerDomain && userObj.Role == UserRoleOwner { account.Domain = lowerDomain err = am.Store.SaveAccount(account) if err != nil { @@ -838,7 +956,19 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ( queriedUsers := make([]*idp.UserData, 0) if !isNil(am.idpManager) { users := make(map[string]struct{}, len(account.Users)) + usersFromIntegration := make([]*idp.UserData, 0) for _, user := range account.Users { + if user.Issued == UserIssuedIntegration { + key := user.IntegrationReference.CacheKey(accountID, user.Id) + info, err := am.externalCacheManager.Get(am.ctx, key) + if err != nil { + log.Infof("Get ExternalCache for key: %s, error: %s", key, err) + users[user.Id] = struct{}{} + continue + } + usersFromIntegration = append(usersFromIntegration, info) + continue + } if !user.IsServiceUser { users[user.Id] = struct{}{} } @@ -847,6 +977,9 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ( if err != nil { return nil, err } + log.Debugf("Got %d users from ExternalCache for account %s", len(usersFromIntegration), accountID) + log.Debugf("Got %d users from InternalCache for account %s", len(queriedUsers), accountID) + queriedUsers = append(queriedUsers, usersFromIntegration...) } userInfos := make([]*UserInfo, 0) @@ -854,7 +987,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ( // 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 account.Users { - if !user.IsAdmin() && user.Id != accountUser.Id { + if !user.HasAdminPower() && user.Id != accountUser.Id { // if user is not an admin then show only current user and do not show other users continue } @@ -868,7 +1001,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ( } for _, localUser := range account.Users { - if !user.IsAdmin() && user.Id != localUser.Id { + if !user.HasAdminPower() && user.Id != localUser.Id { // if user is not an admin then show only current user and do not show other users continue } @@ -892,6 +1025,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ( AutoGroups: localUser.AutoGroups, Status: string(UserStatusActive), IsServiceUser: localUser.IsServiceUser, + NonDeletable: localUser.NonDeletable, } } userInfos = append(userInfos, info) @@ -901,7 +1035,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ( } // expireAndUpdatePeers expires all peers of the given user and updates them in the account -func (am *DefaultAccountManager) expireAndUpdatePeers(account *Account, peers []*Peer) error { +func (am *DefaultAccountManager) expireAndUpdatePeers(account *Account, peers []*nbpeer.Peer) error { var peerIDs []string for _, peer := range peers { if peer.Status.LoginExpired { @@ -913,7 +1047,7 @@ func (am *DefaultAccountManager) expireAndUpdatePeers(account *Account, peers [] if err := am.Store.SavePeerStatus(account.Id, peer.ID, *peer.Status); err != nil { return err } - am.storeEvent( + am.StoreEvent( peer.UserID, peer.ID, account.Id, activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()), ) diff --git a/management/server/user_test.go b/management/server/user_test.go index f1b997186..d445ade62 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -1,16 +1,21 @@ package server import ( + "context" "fmt" "reflect" "testing" "time" + "github.com/eko/gocache/v3/cache" + cacheStore "github.com/eko/gocache/v3/store" "github.com/google/go-cmp/cmp" + gocache "github.com/patrickmn/go-cache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" ) @@ -327,7 +332,7 @@ func TestUser_CreateServiceUser(t *testing.T) { eventStore: &activity.InMemoryEventStore{}, } - user, err := am.createServiceUser(mockAccountID, mockUserID, mockRole, mockServiceUserName, []string{"group1", "group2"}) + user, err := am.createServiceUser(mockAccountID, mockUserID, mockRole, mockServiceUserName, false, []string{"group1", "group2"}) if err != nil { t.Fatalf("Error when creating service user: %s", err) } @@ -343,6 +348,11 @@ func TestUser_CreateServiceUser(t *testing.T) { assert.Zero(t, user.Email) assert.True(t, user.IsServiceUser) assert.Equal(t, "active", user.Status) + + _, err = am.createServiceUser(mockAccountID, mockUserID, UserRoleOwner, mockServiceUserName, false, nil) + if err == nil { + t.Fatal("should return error when creating service user with owner role") + } } func TestUser_CreateUser_ServiceUser(t *testing.T) { @@ -407,14 +417,9 @@ func TestUser_CreateUser_RegularUser(t *testing.T) { assert.Errorf(t, err, "Not configured IDP will throw error but right path used") } -func TestUser_DeleteUser_ServiceUser(t *testing.T) { +func TestUser_InviteNewUser(t *testing.T) { store := newStore(t) account := newAccountWithId(mockAccountID, mockUserID, "") - account.Users[mockServiceUserID] = &User{ - Id: mockServiceUserID, - IsServiceUser: true, - ServiceUserName: mockServiceUserName, - } err := store.SaveAccount(account) if err != nil { @@ -422,17 +427,122 @@ func TestUser_DeleteUser_ServiceUser(t *testing.T) { } am := DefaultAccountManager{ - Store: store, - eventStore: &activity.InMemoryEventStore{}, + Store: store, + eventStore: &activity.InMemoryEventStore{}, + cacheLoading: map[string]chan struct{}{}, } - err = am.DeleteUser(mockAccountID, mockUserID, mockServiceUserID) - if err != nil { - t.Fatalf("Error when deleting user: %s", err) + goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute) + goCacheStore := cacheStore.NewGoCache(goCacheClient) + am.cacheManager = cache.NewLoadable[[]*idp.UserData](am.loadAccount, cache.New[[]*idp.UserData](goCacheStore)) + + mockData := []*idp.UserData{ + { + Email: "user@test.com", + Name: "user", + ID: mockUserID, + }, } - assert.Equal(t, 1, len(store.Accounts[mockAccountID].Users)) - assert.Nil(t, store.Accounts[mockAccountID].Users[mockServiceUserID]) + idpMock := idp.MockIDP{ + CreateUserFunc: func(email, name, accountID, invitedByEmail string) (*idp.UserData, error) { + newData := &idp.UserData{ + Email: email, + Name: name, + ID: "id", + } + + mockData = append(mockData, newData) + + return newData, nil + }, + GetAccountFunc: func(accountId string) ([]*idp.UserData, error) { + return mockData, nil + }, + } + + am.idpManager = &idpMock + + // test if new invite with regular role works + _, err = am.inviteNewUser(mockAccountID, mockUserID, &UserInfo{ + Name: mockServiceUserName, + Role: mockRole, + Email: "test@teste.com", + IsServiceUser: false, + AutoGroups: []string{"group1", "group2"}, + }) + + assert.NoErrorf(t, err, "Invite user should not throw error") + + // test if new invite with owner role fails + _, err = am.inviteNewUser(mockAccountID, mockUserID, &UserInfo{ + Name: mockServiceUserName, + Role: string(UserRoleOwner), + Email: "test2@teste.com", + IsServiceUser: false, + AutoGroups: []string{"group1", "group2"}, + }) + + assert.Errorf(t, err, "Invite user with owner role should throw error") +} + +func TestUser_DeleteUser_ServiceUser(t *testing.T) { + tests := []struct { + name string + serviceUser *User + assertErrFunc assert.ErrorAssertionFunc + assertErrMessage string + }{ + { + name: "Can delete service user", + serviceUser: &User{ + Id: mockServiceUserID, + IsServiceUser: true, + ServiceUserName: mockServiceUserName, + }, + assertErrFunc: assert.NoError, + }, + { + name: "Cannot delete non-deletable service user", + serviceUser: &User{ + Id: mockServiceUserID, + IsServiceUser: true, + ServiceUserName: mockServiceUserName, + NonDeletable: true, + }, + assertErrFunc: assert.Error, + assertErrMessage: "service user is marked as non-deletable", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store := newStore(t) + account := newAccountWithId(mockAccountID, mockUserID, "") + account.Users[mockServiceUserID] = tt.serviceUser + + err := store.SaveAccount(account) + if err != nil { + t.Fatalf("Error when saving account: %s", err) + } + + am := DefaultAccountManager{ + Store: store, + eventStore: &activity.InMemoryEventStore{}, + } + + err = am.DeleteUser(mockAccountID, mockUserID, mockServiceUserID) + tt.assertErrFunc(t, err, tt.assertErrMessage) + + if err != nil { + assert.Equal(t, 2, len(store.Accounts[mockAccountID].Users)) + assert.NotNil(t, store.Accounts[mockAccountID].Users[mockServiceUserID]) + } else { + assert.Equal(t, 1, len(store.Accounts[mockAccountID].Users)) + assert.Nil(t, store.Accounts[mockAccountID].Users[mockServiceUserID]) + } + }) + } } func TestUser_DeleteUser_SelfDelete(t *testing.T) { @@ -478,6 +588,14 @@ func TestUser_DeleteUser_regularUser(t *testing.T) { Issued: UserIssuedIntegration, } + targetId = "user5" + account.Users[targetId] = &User{ + Id: targetId, + IsServiceUser: false, + Issued: UserIssuedAPI, + Role: UserRoleOwner, + } + err := store.SaveAccount(account) if err != nil { t.Fatalf("Error when saving account: %s", err) @@ -508,7 +626,13 @@ func TestUser_DeleteUser_regularUser(t *testing.T) { name: "Delete integration regular user permission denied ", userID: "user4", assertErrFunc: assert.Error, - assertErrMessage: "only integration can delete this user", + assertErrMessage: "only admin service user can delete this user", + }, + { + name: "Delete user with owner role should return permission denied ", + userID: "user5", + assertErrFunc: assert.Error, + assertErrMessage: "unable to delete a user with owner role", }, } @@ -545,17 +669,106 @@ func TestDefaultAccountManager_GetUser(t *testing.T) { } assert.Equal(t, mockUserID, user.Id) - assert.True(t, user.IsAdmin()) + assert.True(t, user.HasAdminPower()) assert.False(t, user.IsBlocked()) } +func TestDefaultAccountManager_ListUsers(t *testing.T) { + store := newStore(t) + account := newAccountWithId(mockAccountID, mockUserID, "") + account.Users["normal_user1"] = NewRegularUser("normal_user1") + account.Users["normal_user2"] = NewRegularUser("normal_user2") + + err := store.SaveAccount(account) + if err != nil { + t.Fatalf("Error when saving account: %s", err) + } + + am := DefaultAccountManager{ + Store: store, + eventStore: &activity.InMemoryEventStore{}, + } + + users, err := am.ListUsers(mockAccountID) + if err != nil { + t.Fatalf("Error when checking user role: %s", err) + } + + admins := 0 + regular := 0 + for _, user := range users { + if user.HasAdminPower() { + admins++ + continue + } + regular++ + } + assert.Equal(t, 3, len(users)) + assert.Equal(t, 1, admins) + assert.Equal(t, 2, regular) +} + +func TestDefaultAccountManager_ExternalCache(t *testing.T) { + store := newStore(t) + account := newAccountWithId(mockAccountID, mockUserID, "") + externalUser := &User{ + Id: "externalUser", + Role: UserRoleUser, + Issued: UserIssuedIntegration, + IntegrationReference: IntegrationReference{ + ID: 1, + IntegrationType: "external", + }, + } + account.Users[externalUser.Id] = externalUser + + err := store.SaveAccount(account) + if err != nil { + t.Fatalf("Error when saving account: %s", err) + } + + am := DefaultAccountManager{ + Store: store, + eventStore: &activity.InMemoryEventStore{}, + idpManager: &idp.GoogleWorkspaceManager{}, // empty manager + cacheLoading: map[string]chan struct{}{}, + cacheManager: cache.New[[]*idp.UserData]( + cacheStore.NewGoCache(gocache.New(CacheExpirationMax, 30*time.Minute)), + ), + externalCacheManager: cache.New[*idp.UserData]( + cacheStore.NewGoCache(gocache.New(CacheExpirationMax, 30*time.Minute)), + ), + } + + // pretend that we receive mockUserID from IDP + err = am.cacheManager.Set(am.ctx, mockAccountID, []*idp.UserData{{Name: mockUserID, ID: mockUserID}}) + assert.NoError(t, err) + + cacheManager := am.GetExternalCacheManager() + cacheKey := externalUser.IntegrationReference.CacheKey(mockAccountID, externalUser.Id) + err = cacheManager.Set(context.Background(), cacheKey, &idp.UserData{ID: externalUser.Id, Name: "Test User", Email: "user@example.com"}) + assert.NoError(t, err) + + infos, err := am.GetUsersFromAccount(mockAccountID, mockUserID) + assert.NoError(t, err) + assert.Equal(t, 2, len(infos)) + var user *UserInfo + for _, info := range infos { + if info.ID == externalUser.Id { + user = info + } + } + assert.NotNil(t, user) + assert.Equal(t, "user@example.com", user.Email) +} + func TestUser_IsAdmin(t *testing.T) { user := NewAdminUser(mockUserID) - assert.True(t, user.IsAdmin()) + assert.True(t, user.HasAdminPower()) user = NewRegularUser(mockUserID) - assert.False(t, user.IsAdmin()) + assert.False(t, user.HasAdminPower()) } func TestUser_GetUsersFromAccount_ForAdmin(t *testing.T) { @@ -621,26 +834,39 @@ func TestDefaultAccountManager_SaveUser(t *testing.T) { } regularUserID := "regularUser" + serviceUserID := "serviceUser" + adminUserID := "adminUser" + ownerUserID := "ownerUser" tt := []struct { - name string - adminInitiator bool - update *User - expectedErr bool + name string + initiatorID string + update *User + expectedErr bool }{ { - name: "Should_Fail_To_Update_Admin_Role", - expectedErr: true, - adminInitiator: true, + name: "Should_Fail_To_Update_Admin_Role", + expectedErr: true, + initiatorID: adminUserID, update: &User{ - Id: userID, + Id: adminUserID, Role: UserRoleUser, Blocked: false, }, }, { - name: "Should_Fail_When_Admin_Blocks_Themselves", - expectedErr: true, - adminInitiator: true, + name: "Should_Fail_When_Admin_Blocks_Themselves", + expectedErr: true, + initiatorID: adminUserID, + update: &User{ + Id: adminUserID, + Role: UserRoleAdmin, + Blocked: true, + }, + }, + { + name: "Should_Fail_To_Update_Non_Existing_User", + expectedErr: true, + initiatorID: adminUserID, update: &User{ Id: userID, Role: UserRoleAdmin, @@ -648,67 +874,125 @@ func TestDefaultAccountManager_SaveUser(t *testing.T) { }, }, { - name: "Should_Fail_To_Update_Non_Existing_User", - expectedErr: true, - adminInitiator: true, + name: "Should_Fail_To_Update_When_Initiator_Is_Not_An_Admin", + expectedErr: true, + initiatorID: regularUserID, update: &User{ - Id: userID, + Id: adminUserID, Role: UserRoleAdmin, Blocked: true, }, }, { - name: "Should_Fail_To_Update_When_Initiator_Is_Not_An_Admin", - expectedErr: true, - adminInitiator: false, - update: &User{ - Id: userID, - Role: UserRoleAdmin, - Blocked: true, - }, - }, - { - name: "Should_Update_User", - expectedErr: false, - adminInitiator: true, + name: "Should_Update_User", + expectedErr: false, + initiatorID: adminUserID, update: &User{ Id: regularUserID, Role: UserRoleAdmin, Blocked: true, }, }, + { + name: "Should_Transfer_Owner_Role_To_User", + expectedErr: false, + initiatorID: ownerUserID, + update: &User{ + Id: adminUserID, + Role: UserRoleAdmin, + Blocked: false, + }, + }, + { + name: "Should_Fail_To_Transfer_Owner_Role_To_Service_User", + expectedErr: true, + initiatorID: ownerUserID, + update: &User{ + Id: serviceUserID, + Role: UserRoleOwner, + Blocked: false, + }, + }, + { + name: "Should_Fail_To_Update_Owner_User_Role_By_Admin", + expectedErr: true, + initiatorID: adminUserID, + update: &User{ + Id: ownerUserID, + Role: UserRoleAdmin, + Blocked: false, + }, + }, + { + name: "Should_Fail_To_Update_Owner_User_Role_By_User", + expectedErr: true, + initiatorID: regularUserID, + update: &User{ + Id: ownerUserID, + Role: UserRoleAdmin, + Blocked: false, + }, + }, + { + name: "Should_Fail_To_Update_Owner_User_Role_By_Service_User", + expectedErr: true, + initiatorID: serviceUserID, + update: &User{ + Id: ownerUserID, + Role: UserRoleAdmin, + Blocked: false, + }, + }, + { + name: "Should_Fail_To_Update_Owner_Role_By_Admin", + expectedErr: true, + initiatorID: adminUserID, + update: &User{ + Id: regularUserID, + Role: UserRoleOwner, + Blocked: false, + }, + }, + { + name: "Should_Fail_To_Block_Owner_Role_By_Admin", + expectedErr: true, + initiatorID: adminUserID, + update: &User{ + Id: ownerUserID, + Role: UserRoleOwner, + Blocked: true, + }, + }, } for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { - // create an account and an admin user - account, err := manager.GetOrCreateAccountByUser(userID, "netbird.io") - if err != nil { - t.Fatal(err) - } + // create an account and an admin user + account, err := manager.GetOrCreateAccountByUser(ownerUserID, "netbird.io") + if err != nil { + t.Fatal(err) + } - // create a regular user - account.Users[regularUserID] = NewRegularUser(regularUserID) - err = manager.Store.SaveAccount(account) - if err != nil { - t.Fatal(err) - } + // create other users + account.Users[regularUserID] = NewRegularUser(regularUserID) + account.Users[adminUserID] = NewAdminUser(adminUserID) + account.Users[serviceUserID] = &User{IsServiceUser: true, Id: serviceUserID, Role: UserRoleAdmin, ServiceUserName: "service"} + err = manager.Store.SaveAccount(account) + if err != nil { + t.Fatal(err) + } - initiatorID := userID - if !tc.adminInitiator { - initiatorID = regularUserID - } + updated, err := manager.SaveUser(account.Id, tc.initiatorID, tc.update) + if tc.expectedErr { + require.Errorf(t, err, "expecting SaveUser to throw an error") + } else { + require.NoError(t, err, "expecting SaveUser not to throw an error") + assert.NotNil(t, updated) - updated, err := manager.SaveUser(account.Id, initiatorID, tc.update) - if tc.expectedErr { - require.Errorf(t, err, "expecting SaveUser to throw an error") - } else { - require.NoError(t, err, "expecting SaveUser not to throw an error") - assert.NotNil(t, updated) - - assert.Equal(t, string(tc.update.Role), updated.Role) - assert.Equal(t, tc.update.IsBlocked(), updated.IsBlocked) - } + assert.Equal(t, string(tc.update.Role), updated.Role) + assert.Equal(t, tc.update.IsBlocked(), updated.IsBlocked) + } + }) } - } diff --git a/release_files/systemd/env b/release_files/systemd/env new file mode 100644 index 000000000..9e7f2e138 --- /dev/null +++ b/release_files/systemd/env @@ -0,0 +1,3 @@ +# Extra flags you might want to pass to the daemon +FLAGS="" + diff --git a/release_files/systemd/netbird-management.service b/release_files/systemd/netbird-management.service new file mode 100644 index 000000000..7fc0aa9ed --- /dev/null +++ b/release_files/systemd/netbird-management.service @@ -0,0 +1,41 @@ +[Unit] +Description=Netbird Management +Documentation=https://netbird.io/docs +After=network-online.target syslog.target +Wants=network-online.target + +[Service] +Type=simple +EnvironmentFile=-/etc/default/netbird-management +ExecStart=/usr/bin/netbird-mgmt management $FLAGS +Restart=on-failure +RestartSec=5 +TimeoutStopSec=10 +CacheDirectory=netbird +ConfigurationDirectory=netbird +LogDirectory=netbird +RuntimeDirectory=netbird +StateDirectory=netbird + +# sandboxing +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateMounts=yes +PrivateTmp=yes +ProtectClock=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectSystem=yes +RemoveIPC=yes +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes + +[Install] +WantedBy=multi-user.target + diff --git a/release_files/systemd/netbird-signal.service b/release_files/systemd/netbird-signal.service new file mode 100644 index 000000000..c7e775f49 --- /dev/null +++ b/release_files/systemd/netbird-signal.service @@ -0,0 +1,41 @@ +[Unit] +Description=Netbird Signal +Documentation=https://netbird.io/docs +After=network-online.target syslog.target +Wants=network-online.target + +[Service] +Type=simple +EnvironmentFile=-/etc/default/netbird-signal +ExecStart=/usr/bin/netbird-signal run $FLAGS +Restart=on-failure +RestartSec=5 +TimeoutStopSec=10 +CacheDirectory=netbird +ConfigurationDirectory=netbird +LogDirectory=netbird +RuntimeDirectory=netbird +StateDirectory=netbird + +# sandboxing +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateMounts=yes +PrivateTmp=yes +ProtectClock=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectSystem=yes +RemoveIPC=yes +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes + +[Install] +WantedBy=multi-user.target + diff --git a/release_files/systemd/netbird@.service b/release_files/systemd/netbird@.service new file mode 100644 index 000000000..39e3b6b23 --- /dev/null +++ b/release_files/systemd/netbird@.service @@ -0,0 +1,41 @@ +[Unit] +Description=Netbird Client (%i) +Documentation=https://netbird.io/docs +After=network-online.target syslog.target NetworkManager.service +Wants=network-online.target + +[Service] +Type=simple +EnvironmentFile=-/etc/default/netbird +ExecStart=/usr/bin/netbird service run --log-file /var/log/netbird/client-%i.log --config /etc/netbird/%i.json --daemon-addr unix:///var/run/netbird/%i.sock $FLAGS +Restart=on-failure +RestartSec=5 +TimeoutStopSec=10 +CacheDirectory=netbird +ConfigurationDirectory=netbird +LogDirectory=netbird +RuntimeDirectory=netbird +StateDirectory=netbird + +# sandboxing +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateMounts=yes +PrivateTmp=yes +ProtectClock=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=no # needed to load wg module for kernel-mode WireGuard +ProtectKernelTunables=no +ProtectSystem=yes +RemoveIPC=yes +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes + +[Install] +WantedBy=multi-user.target + diff --git a/sharedsock/example/main.go b/sharedsock/example/main.go index 7c879b4c9..9384d2b1c 100644 --- a/sharedsock/example/main.go +++ b/sharedsock/example/main.go @@ -2,10 +2,11 @@ package main import ( "context" - "github.com/netbirdio/netbird/sharedsock" - log "github.com/sirupsen/logrus" "os" "os/signal" + + "github.com/netbirdio/netbird/sharedsock" + log "github.com/sirupsen/logrus" ) func main() { @@ -16,7 +17,7 @@ func main() { panic(err) } - log.Infof("attached to to the raw socket on port %d", port) + log.Infof("attached to the raw socket on port %d", port) ctx, cancel := context.WithCancel(context.Background()) // read packets diff --git a/sharedsock/sock_linux.go b/sharedsock/sock_linux.go index c9e35dfa2..656fdc8ca 100644 --- a/sharedsock/sock_linux.go +++ b/sharedsock/sock_linux.go @@ -248,7 +248,7 @@ func (s *SharedSocket) ReadFrom(b []byte) (n int, addr net.Addr, err error) { decodedLayers := make([]gopacket.LayerType, 0, 3) - err = parser.DecodeLayers(pkt.buf[:], &decodedLayers) + err = parser.DecodeLayers(pkt.buf, &decodedLayers) if err != nil { return 0, nil, err } @@ -262,7 +262,7 @@ func (s *SharedSocket) ReadFrom(b []byte) (n int, addr net.Addr, err error) { return int(udp.Length), remoteAddr, nil } -// WriteTo builds a UDP packet and writes it using the specific IP version writter +// WriteTo builds a UDP packet and writes it using the specific IP version writer func (s *SharedSocket) WriteTo(buf []byte, rAddr net.Addr) (n int, err error) { rUDPAddr, ok := rAddr.(*net.UDPAddr) if !ok { diff --git a/signal/client/grpc.go b/signal/client/grpc.go index fef443173..7aa9f9ce9 100644 --- a/signal/client/grpc.go +++ b/signal/client/grpc.go @@ -260,7 +260,7 @@ func (c *GrpcClient) SendToStream(msg *proto.EncryptedMessage) error { return fmt.Errorf("no connection to signal") } if c.stream == nil { - return fmt.Errorf("connection to the Signal Exchnage has not been established yet. Please call GrpcClient.Receive before sending messages") + return fmt.Errorf("connection to the Signal Exchange has not been established yet. Please call GrpcClient.Receive before sending messages") } err := c.stream.Send(msg) @@ -354,16 +354,17 @@ func (c *GrpcClient) receive(stream proto.SignalExchange_ConnectStreamClient, for { msg, err := stream.Recv() - if s, ok := status.FromError(err); ok && s.Code() == codes.Canceled { + switch s, ok := status.FromError(err); { + case ok && s.Code() == codes.Canceled: log.Debugf("stream canceled (usually indicates shutdown)") return err - } else if s.Code() == codes.Unavailable { + case s.Code() == codes.Unavailable: log.Debugf("Signal Service is unavailable") return err - } else if err == io.EOF { + case err == io.EOF: log.Debugf("Signal Service stream closed by server") return err - } else if err != nil { + case err != nil: return err } log.Tracef("received a new message from Peer [fingerprint: %s]", msg.Key) diff --git a/signal/cmd/root.go b/signal/cmd/root.go index 479579c7e..7fa75d923 100644 --- a/signal/cmd/root.go +++ b/signal/cmd/root.go @@ -47,7 +47,7 @@ func init() { } rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "") - rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the the log will be output to stdout") + rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout") rootCmd.AddCommand(runCmd) } diff --git a/util/retry.go b/util/retry.go index 3bffcf288..2d5fbf6cf 100644 --- a/util/retry.go +++ b/util/retry.go @@ -15,7 +15,7 @@ func Retry(attempts int, sleep time.Duration, toExec func() error, onError func( if attempts--; attempts > 0 { jitter := time.Duration(rand.Int63n(int64(sleep))) - sleep = sleep + jitter/2 + sleep += jitter / 2 onError(err) time.Sleep(sleep) diff --git a/version/version.go b/version/version.go index d9c119f90..d70a5effa 100644 --- a/version/version.go +++ b/version/version.go @@ -1,8 +1,19 @@ package version +import ( + "regexp" + + v "github.com/hashicorp/go-version" +) + // will be replaced with the release version when using goreleaser var version = "development" +var ( + VersionRegexp = regexp.MustCompile("^" + v.VersionRegexpRaw + "$") + SemverRegexp = regexp.MustCompile("^" + v.SemverRegexpRaw + "$") +) + // NetbirdVersion returns the Netbird version func NetbirdVersion() string { return version