Compare commits

...

22 Commits

Author SHA1 Message Date
Misha Bragin
d0c6d88971 Simplified Store Interface (#545)
This PR simplifies Store and FileStore
by keeping just the Get and Save account methods.

The AccountManager operates mostly around
a single account, so it makes sense to fetch
the whole account object from the store.
2022-11-07 12:10:56 +01:00
Misha Bragin
4321b71984 Hide content based on user role (#541) 2022-11-05 10:24:50 +01:00
Maycon Santos
e8d82c1bd3 Feature/dns-server (#537)
Adding DNS server for client

Updated the API with new fields

Added custom zone object for peer's DNS resolution
2022-11-03 18:39:37 +01:00
Misha Bragin
6aa7a2c5e1 Hide setup key from non-admin users (#539) 2022-11-03 17:02:31 +01:00
Rui Lopes
2e0bf61e9a correctly set the windows application icon on windows (#535)
the icon format is not really supported, so this uses a png instead.

this closes https://github.com/netbirdio/netbird/issues/534.
2022-11-01 00:34:30 +01:00
Maycon Santos
126af9dffc Return gateway address if not nil (#533)
If the gateway address would be nil which is
the case on macOS, we return the preferredSrc

added tests for getExistingRIBRouteGateway function

update log message
2022-10-31 11:54:34 +01:00
Maycon Santos
4cdf2df660 Update sign pipeline version to 0.0.4 (#531)
This version has a fix for the
macOS UI client architecture
2022-10-31 11:03:42 +01:00
Maycon Santos
9a4c9aa286 Add active peers count per OS (#526)
* Add active peers count per OS

* increase iface tests timeout
2022-10-26 14:48:40 +02:00
Rui Lopes
5ed61700ff Set the application icon, settings window title and systray tooltip (#523) 2022-10-26 14:34:30 +02:00
Misha Bragin
84117a9fb7 Update WireGuard trademark note 2022-10-23 11:47:42 +02:00
Misha Bragin
92b612eba4 Update demo video link 2022-10-22 16:55:49 +02:00
Misha Bragin
aeeaa21eed Update README.md (#524) 2022-10-22 16:19:16 +02:00
Misha Bragin
d228cd0cb1 Remove release note 2022-10-22 15:10:09 +02:00
Misha Bragin
b41f36fccd Add gRPC metrics (#522) 2022-10-22 15:06:54 +02:00
Misha Bragin
d2cde4a040 Add IdP metrics (#521) 2022-10-22 13:29:39 +02:00
Misha Bragin
84879a356b Extract app metrics to a separate struct (#520) 2022-10-22 11:50:21 +02:00
Misha Bragin
ed2214f9a9 Add HTTP request/response totals to metrics (#519) 2022-10-22 10:07:13 +02:00
braginini
4388dcc20b Listen metrics on all interfaces 2022-10-21 16:50:06 +02:00
Misha Bragin
4f1f0df7d2 Add Open-telemetry support (#517)
This PR brings open-telemetry metrics to the
Management service.
The Management service exposes new HTTP endpoint
/metrics on 8081 port by default.
The port can be changed by specifying
--metrics-port PORT flag when starting the service.
2022-10-21 16:24:13 +02:00
Misha Bragin
08ddf04c5f Fix IdP tests (#516) 2022-10-19 18:36:10 +02:00
Misha Bragin
b5ee2174a8 Do not set wt_pending_invite when unnecessary (#515)
wt_pending_invite property is set for every user on IdP.
Avoid setting it when unnecessary.
2022-10-19 17:51:41 +02:00
Misha Bragin
7218a3d563 Management single account mode (#511) 2022-10-19 17:43:28 +02:00
79 changed files with 3047 additions and 1066 deletions

View File

@@ -25,7 +25,6 @@ jobs:
needs: pre
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v2

View File

@@ -9,7 +9,7 @@ on:
pull_request:
env:
SIGN_PIPE_VER: "v0.0.3"
SIGN_PIPE_VER: "v0.0.4"
GORELEASER_VER: "v1.6.3"
jobs:

View File

@@ -1,6 +1,6 @@
<p align="center">
<strong>:hatching_chick: New release! NetBird Easy SSH</strong>.
<a href="https://github.com/netbirdio/netbird/releases/tag/v0.8.0">
<strong>:hatching_chick: New Release! User Invites.</strong>
<a href="https://github.com/netbirdio/netbird/releases">
Learn more
</a>
</p>
@@ -62,9 +62,8 @@ NetBird creates an overlay peer-to-peer network connecting machines automaticall
- \[ ] Network Activity Monitoring.
### Secure peer-to-peer VPN with SSO and MFA in minutes
<p float="left" align="middle">
<img src="docs/media/netbird-sso-mfa-demo.gif" width="800"/>
</p>
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
@@ -104,5 +103,5 @@ See a complete [architecture overview](https://netbird.io/docs/overview/architec
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
### Legal
[WireGuard](https://wireguard.com/) is a registered trademark of Jason A. Donenfeld.
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.

View File

@@ -68,12 +68,12 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
}
peersUpdateManager := mgmt.NewPeersUpdateManager()
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil)
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "")
if err != nil {
t.Fatal(err)
}
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -0,0 +1,56 @@
package dns
import (
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
log "github.com/sirupsen/logrus"
"sync"
)
type localResolver struct {
registeredMap registrationMap
records sync.Map
}
// ServeDNS handles a DNS request
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
log.Tracef("received question: %#v\n", r.Question[0])
response := d.lookupRecord(r)
if response == nil {
log.Debugf("got empty response for question: %#v\n", r.Question[0])
return
}
replyMessage := &dns.Msg{}
replyMessage.SetReply(r)
replyMessage.Answer = append(replyMessage.Answer, response)
err := w.WriteMsg(replyMessage)
if err != nil {
log.Debugf("got an error while writing the local resolver response, error: %v", err)
}
}
func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR {
record, found := d.records.Load(r.Question[0].Name)
if !found {
return nil
}
return record.(dns.RR)
}
func (d *localResolver) registerRecord(record nbdns.SimpleRecord) error {
fullRecord, err := dns.NewRR(record.String())
if err != nil {
return err
}
d.records.Store(fullRecord.Header().Name, fullRecord)
return nil
}
func (d *localResolver) deleteRecord(recordKey string) {
d.records.Delete(dns.Fqdn(recordKey))
}

View File

@@ -0,0 +1,86 @@
package dns
import (
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
"strings"
"testing"
)
func TestLocalResolver_ServeDNS(t *testing.T) {
recordA := nbdns.SimpleRecord{
Name: "peera.netbird.cloud.",
Type: 1,
Class: nbdns.DefaultClass,
TTL: 300,
RData: "1.2.3.4",
}
recordCNAME := nbdns.SimpleRecord{
Name: "peerb.netbird.cloud.",
Type: 5,
Class: nbdns.DefaultClass,
TTL: 300,
RData: "www.netbird.io",
}
testCases := []struct {
name string
inputRecord nbdns.SimpleRecord
inputMSG *dns.Msg
responseShouldBeNil bool
}{
{
name: "Should Resolve A Record",
inputRecord: recordA,
inputMSG: new(dns.Msg).SetQuestion(recordA.Name, dns.TypeA),
},
{
name: "Should Resolve CNAME Record",
inputRecord: recordCNAME,
inputMSG: new(dns.Msg).SetQuestion(recordCNAME.Name, dns.TypeCNAME),
},
{
name: "Should Not Write When Not Found A Record",
inputRecord: recordA,
inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
responseShouldBeNil: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
resolver := &localResolver{
registeredMap: make(registrationMap),
}
_ = resolver.registerRecord(testCase.inputRecord)
var responseMSG *dns.Msg
responseWriter := &mockResponseWriter{
WriteMsgFunc: func(m *dns.Msg) error {
responseMSG = m
return nil
},
}
resolver.ServeDNS(responseWriter, testCase.inputMSG)
if responseMSG == nil {
if testCase.responseShouldBeNil {
return
}
t.Fatalf("should write a response message")
}
answerString := responseMSG.Answer[0].String()
if !strings.Contains(answerString, testCase.inputRecord.Name) {
t.Fatalf("answer doesn't contain the same domain name: \nWant: %s\nGot:%s", testCase.name, answerString)
}
if !strings.Contains(answerString, dns.Type(testCase.inputRecord.Type).String()) {
t.Fatalf("answer doesn't contain the correct type: \nWant: %s\nGot:%s", dns.Type(testCase.inputRecord.Type).String(), answerString)
}
if !strings.Contains(answerString, testCase.inputRecord.RData) {
t.Fatalf("answer doesn't contain the same address: \nWant: %s\nGot:%s", testCase.inputRecord.RData, answerString)
}
})
}
}

View File

@@ -0,0 +1,25 @@
package dns
import (
"github.com/miekg/dns"
"net"
)
type mockResponseWriter struct {
WriteMsgFunc func(m *dns.Msg) error
}
func (rw *mockResponseWriter) WriteMsg(m *dns.Msg) error {
if rw.WriteMsgFunc != nil {
return rw.WriteMsgFunc(m)
}
return nil
}
func (rw *mockResponseWriter) LocalAddr() net.Addr { return nil }
func (rw *mockResponseWriter) RemoteAddr() net.Addr { return nil }
func (rw *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
func (rw *mockResponseWriter) Close() error { return nil }
func (rw *mockResponseWriter) TsigStatus() error { return nil }
func (rw *mockResponseWriter) TsigTimersOnly(bool) {}
func (rw *mockResponseWriter) Hijack() {}

View File

@@ -0,0 +1,270 @@
package dns
import (
"context"
"fmt"
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
log "github.com/sirupsen/logrus"
"sync"
"time"
)
const (
port = 5053
defaultIP = "0.0.0.0"
)
// Server dns server object
type Server struct {
ctx context.Context
stop context.CancelFunc
mux sync.Mutex
server *dns.Server
dnsMux *dns.ServeMux
dnsMuxMap registrationMap
localResolver *localResolver
updateSerial uint64
listenerIsRunning bool
}
type registrationMap map[string]struct{}
type muxUpdate struct {
domain string
handler dns.Handler
}
// NewServer returns a new dns server
func NewServer(ctx context.Context) *Server {
mux := dns.NewServeMux()
dnsServer := &dns.Server{
Addr: fmt.Sprintf("%s:%d", defaultIP, port),
Net: "udp",
Handler: mux,
UDPSize: 65535,
}
ctx, stop := context.WithCancel(ctx)
return &Server{
ctx: ctx,
stop: stop,
server: dnsServer,
dnsMux: mux,
dnsMuxMap: make(registrationMap),
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
}
}
// Start runs the listener in a go routine
func (s *Server) Start() {
log.Debugf("starting dns on %s:%d", defaultIP, port)
go func() {
s.setListenerStatus(true)
defer s.setListenerStatus(false)
err := s.server.ListenAndServe()
if err != nil {
log.Errorf("dns server returned an error: %v", err)
}
}()
}
func (s *Server) setListenerStatus(running bool) {
s.listenerIsRunning = running
}
// Stop stops the server
func (s *Server) Stop() {
s.stop()
err := s.stopListener()
if err != nil {
log.Error(err)
}
}
func (s *Server) stopListener() error {
if !s.listenerIsRunning {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := s.server.ShutdownContext(ctx)
if err != nil {
return fmt.Errorf("stopping dns server listener returned an error: %v", err)
}
return nil
}
// UpdateDNSServer processes an update received from the management service
func (s *Server) UpdateDNSServer(serial uint64, update nbdns.Update) error {
select {
case <-s.ctx.Done():
log.Infof("not updating DNS server as context is closed")
return s.ctx.Err()
default:
if serial < s.updateSerial {
return fmt.Errorf("not applying dns update, error: "+
"network update is %d behind the last applied update", s.updateSerial-serial)
}
s.mux.Lock()
defer s.mux.Unlock()
// is the service should be disabled, we stop the listener
// and proceed with a regular update to clean up the handlers and records
if !update.ServiceEnable {
err := s.stopListener()
if err != nil {
log.Error(err)
}
} else if !s.listenerIsRunning {
s.Start()
}
localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...)
s.updateMux(muxUpdates)
s.updateLocalResolver(localRecords)
s.updateSerial = serial
return nil
}
}
func (s *Server) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) {
var muxUpdates []muxUpdate
localRecords := make(map[string]nbdns.SimpleRecord, 0)
for _, customZone := range customZones {
if len(customZone.Records) == 0 {
return nil, nil, fmt.Errorf("received an empty list of records")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: customZone.Domain,
handler: s.localResolver,
})
for _, record := range customZone.Records {
localRecords[record.Name] = record
}
}
return muxUpdates, localRecords, nil
}
func (s *Server) buildUpstreamHandlerUpdate(nameServerGroups []nbdns.NameServerGroup) ([]muxUpdate, error) {
var muxUpdates []muxUpdate
for _, nsGroup := range nameServerGroups {
if len(nsGroup.NameServers) == 0 {
return nil, fmt.Errorf("received a nameserver group with empty nameserver list")
}
handler := &upstreamResolver{
parentCTX: s.ctx,
upstreamClient: &dns.Client{},
upstreamTimeout: defaultUpstreamTimeout,
}
for _, ns := range nsGroup.NameServers {
if ns.NSType != nbdns.UDPNameServerType {
log.Warnf("skiping nameserver %s with type %s, this peer supports only %s",
ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String())
continue
}
handler.upstreamServers = append(handler.upstreamServers, getNSHostPort(ns))
}
if len(handler.upstreamServers) == 0 {
log.Errorf("received a nameserver group with an invalid nameserver list")
continue
}
if nsGroup.Primary {
muxUpdates = append(muxUpdates, muxUpdate{
domain: nbdns.RootZone,
handler: handler,
})
continue
}
if len(nsGroup.Domains) == 0 {
return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list")
}
for _, domain := range nsGroup.Domains {
if domain == "" {
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: domain,
handler: handler,
})
}
}
return muxUpdates, nil
}
func (s *Server) updateMux(muxUpdates []muxUpdate) {
muxUpdateMap := make(registrationMap)
for _, update := range muxUpdates {
s.registerMux(update.domain, update.handler)
muxUpdateMap[update.domain] = struct{}{}
}
for key := range s.dnsMuxMap {
_, found := muxUpdateMap[key]
if !found {
s.deregisterMux(key)
}
}
s.dnsMuxMap = muxUpdateMap
}
func (s *Server) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
for key := range s.localResolver.registeredMap {
_, found := update[key]
if !found {
s.localResolver.deleteRecord(key)
}
}
updatedMap := make(registrationMap)
for key, record := range update {
err := s.localResolver.registerRecord(record)
if err != nil {
log.Warnf("got an error while registering the record (%s), error: %v", record.String(), err)
}
updatedMap[key] = struct{}{}
}
s.localResolver.registeredMap = updatedMap
}
func getNSHostPort(ns nbdns.NameServer) string {
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
}
func (s *Server) registerMux(pattern string, handler dns.Handler) {
s.dnsMux.Handle(pattern, handler)
}
func (s *Server) deregisterMux(pattern string) {
s.dnsMux.HandleRemove(pattern)
}

View File

@@ -0,0 +1,285 @@
package dns
import (
"context"
"fmt"
nbdns "github.com/netbirdio/netbird/dns"
"net"
"net/netip"
"os"
"runtime"
"testing"
"time"
)
var zoneRecords = []nbdns.SimpleRecord{
{
Name: "peera.netbird.cloud",
Type: 1,
Class: nbdns.DefaultClass,
TTL: 300,
RData: "1.2.3.4",
},
}
func TestUpdateDNSServer(t *testing.T) {
nameServers := []nbdns.NameServer{
{
IP: netip.MustParseAddr("8.8.8.8"),
NSType: nbdns.UDPNameServerType,
Port: 53,
},
{
IP: netip.MustParseAddr("8.8.4.4"),
NSType: nbdns.UDPNameServerType,
Port: 53,
},
}
testCases := []struct {
name string
initUpstreamMap registrationMap
initLocalMap registrationMap
initSerial uint64
inputSerial uint64
inputUpdate nbdns.Update
shouldFail bool
expectedUpstreamMap registrationMap
expectedLocalMap registrationMap
}{
{
name: "Initial Update Should Succeed",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Update{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []nbdns.NameServerGroup{
{
Domains: []string{"netbird.io"},
NameServers: nameServers,
},
{
NameServers: nameServers,
Primary: true,
},
},
},
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}, nbdns.RootZone: struct{}{}},
expectedLocalMap: registrationMap{zoneRecords[0].Name: struct{}{}},
},
{
name: "New Update Should Succeed",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registrationMap{zoneRecords[0].Name: struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Update{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []nbdns.NameServerGroup{
{
Domains: []string{"netbird.io"},
NameServers: nameServers,
},
},
},
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}},
expectedLocalMap: registrationMap{zoneRecords[0].Name: struct{}{}},
},
{
name: "Smaller Update Serial Should Be Skipped",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 2,
inputSerial: 1,
shouldFail: true,
},
{
name: "Empty NS Group Domain Or Not Primary Element Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Update{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []nbdns.NameServerGroup{
{
NameServers: nameServers,
},
},
},
shouldFail: true,
},
{
name: "Invalid NS Group Nameservers list Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Update{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []nbdns.NameServerGroup{
{
NameServers: nameServers,
},
},
},
shouldFail: true,
},
{
name: "Invalid Custom Zone Records list Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Update{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
},
},
NameServerGroups: []nbdns.NameServerGroup{
{
NameServers: nameServers,
Primary: true,
},
},
},
shouldFail: true,
},
{
name: "Empty Update Should Succeed and Clean Maps",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registrationMap{zoneRecords[0].Name: struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Update{ServiceEnable: true},
expectedUpstreamMap: make(registrationMap),
expectedLocalMap: make(registrationMap),
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctx := context.Background()
dnsServer := NewServer(ctx)
dnsServer.dnsMuxMap = testCase.initUpstreamMap
dnsServer.localResolver.registeredMap = testCase.initLocalMap
dnsServer.updateSerial = testCase.initSerial
dnsServer.listenerIsRunning = true
err := dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate)
if err != nil {
if testCase.shouldFail {
return
}
t.Fatalf("update dns server should not fail, got error: %v", err)
}
if len(dnsServer.dnsMuxMap) != len(testCase.expectedUpstreamMap) {
t.Fatalf("update upstream failed, map size is different than expected, want %d, got %d", len(testCase.expectedUpstreamMap), len(dnsServer.dnsMuxMap))
}
for key := range testCase.expectedUpstreamMap {
_, found := dnsServer.dnsMuxMap[key]
if !found {
t.Fatalf("update upstream failed, key %s was not found in the dnsMuxMap: %#v", key, dnsServer.dnsMuxMap)
}
}
if len(dnsServer.localResolver.registeredMap) != len(testCase.expectedLocalMap) {
t.Fatalf("update local failed, registered map size is different than expected, want %d, got %d", len(testCase.expectedLocalMap), len(dnsServer.localResolver.registeredMap))
}
for key := range testCase.expectedLocalMap {
_, found := dnsServer.localResolver.registeredMap[key]
if !found {
t.Fatalf("update local failed, key %s was not found in the localResolver.registeredMap: %#v", key, dnsServer.localResolver.registeredMap)
}
}
})
}
}
func TestDNSServerStartStop(t *testing.T) {
ctx := context.Background()
dnsServer := NewServer(ctx)
if runtime.GOOS == "windows" && os.Getenv("CI") == "true" {
// todo review why this test is not working only on github actions workflows
t.Skip("skipping test in Windows CI workflows.")
}
dnsServer.Start()
err := dnsServer.localResolver.registerRecord(zoneRecords[0])
if err != nil {
t.Error(err)
}
dnsServer.dnsMux.Handle("netbird.cloud", dnsServer.localResolver)
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Second * 5,
}
addr := fmt.Sprintf("127.0.0.1:%d", port)
conn, err := d.DialContext(ctx, network, addr)
if err != nil {
t.Log(err)
// retry test before exit, for slower systems
return d.DialContext(ctx, network, addr)
}
return conn, nil
},
}
ips, err := resolver.LookupHost(context.Background(), zoneRecords[0].Name)
if err != nil {
t.Fatalf("failed to connect to the server, error: %v", err)
}
t.Log(ips)
if ips[0] != zoneRecords[0].RData {
t.Fatalf("got a different IP from the server: want %s, got %s", zoneRecords[0].RData, ips[0])
}
dnsServer.Stop()
ctx, cancel := context.WithTimeout(ctx, time.Second*1)
defer cancel()
_, err = resolver.LookupHost(ctx, zoneRecords[0].Name)
if err == nil {
t.Fatalf("we should encounter an error when querying a stopped server")
}
}

View File

@@ -0,0 +1,67 @@
package dns
import (
"context"
"errors"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
"net"
"time"
)
const defaultUpstreamTimeout = 15 * time.Second
type upstreamResolver struct {
parentCTX context.Context
upstreamClient *dns.Client
upstreamServers []string
upstreamTimeout time.Duration
}
// ServeDNS handles a DNS request
func (u *upstreamResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
log.Tracef("received an upstream question: %#v", r.Question[0])
select {
case <-u.parentCTX.Done():
return
default:
}
for _, upstream := range u.upstreamServers {
ctx, cancel := context.WithTimeout(u.parentCTX, u.upstreamTimeout)
rm, t, err := u.upstreamClient.ExchangeContext(ctx, r, upstream)
cancel()
if err != nil {
if err == context.DeadlineExceeded || isTimeout(err) {
log.Warnf("got an error while connecting to upstream %s, error: %v", upstream, err)
continue
}
log.Errorf("got an error while querying the upstream %s, error: %v", upstream, err)
return
}
log.Tracef("took %s to query the upstream %s", t, upstream)
err = w.WriteMsg(rm)
if err != nil {
log.Errorf("got an error while writing the upstream resolver response, error: %v", err)
}
return
}
log.Errorf("all queries to the upstream nameservers failed with timeout")
}
// isTimeout returns true if the given error is a network timeout error.
//
// Copied from k8s.io/apimachinery/pkg/util/net.IsTimeout
func isTimeout(err error) bool {
var neterr net.Error
if errors.As(err, &neterr) {
return neterr != nil && neterr.Timeout()
}
return false
}

View File

@@ -0,0 +1,110 @@
package dns
import (
"context"
"github.com/miekg/dns"
"strings"
"testing"
"time"
)
func TestUpstreamResolver_ServeDNS(t *testing.T) {
testCases := []struct {
name string
inputMSG *dns.Msg
responseShouldBeNil bool
InputServers []string
timeout time.Duration
cancelCTX bool
expectedAnswer string
}{
{
name: "Should Resolve A Record",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.8.8.8:53", "8.8.4.4:53"},
timeout: defaultUpstreamTimeout,
expectedAnswer: "1.1.1.1",
},
{
name: "Should Resolve If First Upstream Times Out",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.0.0.0:53", "8.8.4.4:53"},
timeout: 2 * time.Second,
expectedAnswer: "1.1.1.1",
},
{
name: "Should Not Resolve If Can't Connect To Both Servers",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"},
timeout: 200 * time.Millisecond,
responseShouldBeNil: true,
},
{
name: "Should Not Resolve If Parent Context Is Canceled",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.0.0.0:53", "8.8.4.4:53"},
cancelCTX: true,
timeout: defaultUpstreamTimeout,
responseShouldBeNil: true,
},
//{
// name: "Should Resolve CNAME Record",
// inputMSG: new(dns.Msg).SetQuestion("one.one.one.one", dns.TypeCNAME),
//},
//{
// name: "Should Not Write When Not Found A Record",
// inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
// responseShouldBeNil: true,
//},
}
// should resolve if first upstream times out
// should not write when both fails
// should not resolve if parent context is canceled
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO())
resolver := &upstreamResolver{
parentCTX: ctx,
upstreamClient: &dns.Client{},
upstreamServers: testCase.InputServers,
upstreamTimeout: testCase.timeout,
}
if testCase.cancelCTX {
cancel()
} else {
defer cancel()
}
var responseMSG *dns.Msg
responseWriter := &mockResponseWriter{
WriteMsgFunc: func(m *dns.Msg) error {
responseMSG = m
return nil
},
}
resolver.ServeDNS(responseWriter, testCase.inputMSG)
if responseMSG == nil {
if testCase.responseShouldBeNil {
return
}
t.Fatalf("should write a response message")
}
foundAnswer := false
for _, answer := range responseMSG.Answer {
if strings.Contains(answer.String(), testCase.expectedAnswer) {
foundAnswer = true
break
}
}
if !foundAnswer {
t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer)
}
})
}
}

View File

@@ -3,6 +3,7 @@ package internal
import (
"context"
"fmt"
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/routemanager"
nbssh "github.com/netbirdio/netbird/client/ssh"
nbstatus "github.com/netbirdio/netbird/client/status"
@@ -103,6 +104,8 @@ type Engine struct {
statusRecorder *nbstatus.Status
routeManager routemanager.Manager
dnsServer *dns.Server
}
// Peer is an instance of the Connection Peer
@@ -130,6 +133,7 @@ func NewEngine(
networkSerial: 0,
sshServerFunc: nbssh.DefaultSSHServer,
statusRecorder: statusRecorder,
dnsServer: dns.NewServer(ctx),
}
}
@@ -190,6 +194,10 @@ func (e *Engine) Stop() error {
e.routeManager.Stop()
}
if e.dnsServer != nil {
e.dnsServer.Stop()
}
log.Infof("stopped Netbird Engine")
return nil

View File

@@ -761,12 +761,12 @@ func startManagement(port int, dataDir string) (*grpc.Server, error) {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
peersUpdateManager := server.NewPeersUpdateManager()
accountManager, err := server.BuildManager(store, peersUpdateManager, nil)
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "")
if err != nil {
return nil, err
}
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
if err != nil {
return nil, err
}

View File

@@ -21,7 +21,7 @@ func addToRouteTableIfNoExists(prefix netip.Prefix, addr string) error {
}
if prefixGateway != nil && !prefixGateway.Equal(gateway) {
log.Warnf("route for network %s already exist and is pointing to the gateway: %s, won't add another one", prefix, prefixGateway)
log.Warnf("skipping adding a new route for network %s because it already exists and is pointing to the non default gateway: %s", prefix, prefixGateway)
return nil
}
return addToRouteTable(prefix, addr)
@@ -45,11 +45,14 @@ func getExistingRIBRouteGateway(prefix netip.Prefix) (net.IP, error) {
if err != nil {
return nil, err
}
_, _, localGatewayAddress, err := r.Route(prefix.Addr().AsSlice())
_, gateway, preferredSrc, err := r.Route(prefix.Addr().AsSlice())
if err != nil {
log.Errorf("getting routes returned an error: %v", err)
return nil, errRouteNotFound
}
if gateway == nil {
return preferredSrc, nil
}
return localGatewayAddress, nil
return gateway, nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/netbirdio/netbird/iface"
"github.com/stretchr/testify/require"
"net"
"net/netip"
"testing"
)
@@ -66,3 +67,45 @@ func TestAddRemoveRoutes(t *testing.T) {
})
}
}
func TestGetExistingRIBRouteGateway(t *testing.T) {
gateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0"))
if err != nil {
t.Fatal("shouldn't return error when fetching the gateway: ", err)
}
if gateway == nil {
t.Fatal("should return a gateway")
}
addresses, err := net.InterfaceAddrs()
if err != nil {
t.Fatal("shouldn't return error when fetching interface addresses: ", err)
}
var testingIP string
var testingPrefix netip.Prefix
for _, address := range addresses {
if address.Network() != "ip+net" {
continue
}
prefix := netip.MustParsePrefix(address.String())
if !prefix.Addr().IsLoopback() && prefix.Addr().Is4() {
testingIP = prefix.Addr().String()
testingPrefix = prefix.Masked()
break
}
}
localIP, err := getExistingRIBRouteGateway(testingPrefix)
if err != nil {
t.Fatal("shouldn't return error: ", err)
}
if localIP == nil {
t.Fatal("should return a gateway for local network")
}
if localIP.String() == gateway.String() {
t.Fatal("local ip should not match with gateway IP")
}
if localIP.String() != testingIP {
t.Fatalf("local ip should match with testing IP: want %s got %s", testingIP, localIP.String())
}
}

View File

@@ -8,7 +8,6 @@ import (
"context"
"flag"
"fmt"
"github.com/netbirdio/netbird/client/system"
"os"
"os/exec"
"path"
@@ -18,6 +17,8 @@ import (
"syscall"
"time"
"github.com/netbirdio/netbird/client/system"
"github.com/cenkalti/backoff/v4"
_ "embed"
@@ -61,6 +62,8 @@ func main() {
flag.Parse()
a := app.New()
a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnectedPNG))
client := newServiceClient(daemonAddr, a, showSettings)
if showSettings {
a.Run()
@@ -113,7 +116,7 @@ type serviceClient struct {
iLogFile *widget.Entry
iPreSharedKey *widget.Entry
// observable settings over correspondign iMngURL and iPreSharedKey values.
// observable settings over corresponding iMngURL and iPreSharedKey values.
managementURL string
preSharedKey string
adminURL string
@@ -121,7 +124,7 @@ type serviceClient struct {
// newServiceClient instance constructor
//
// This constructor olso build UI elements for settings window.
// This constructor also builds the UI elements for the settings window.
func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient {
s := &serviceClient{
ctx: context.Background(),
@@ -149,7 +152,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient
func (s *serviceClient) showUIElements() {
// add settings window UI elements.
s.wSettings = s.app.NewWindow("Settings")
s.wSettings = s.app.NewWindow("NetBird Settings")
s.iMngURL = widget.NewEntry()
s.iAdminURL = widget.NewEntry()
s.iConfigFile = widget.NewEntry()
@@ -326,11 +329,13 @@ func (s *serviceClient) updateStatus() error {
if status.Status == string(internal.StatusConnected) && !s.mUp.Disabled() {
systray.SetIcon(s.icConnected)
systray.SetTooltip("NetBird (Connected)")
s.mStatus.SetTitle("Connected")
s.mUp.Disable()
s.mDown.Enable()
} else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() {
systray.SetIcon(s.icDisconnected)
systray.SetTooltip("NetBird (Disconnected)")
s.mStatus.SetTitle("Disconnected")
s.mDown.Disable()
s.mUp.Enable()
@@ -355,6 +360,7 @@ func (s *serviceClient) updateStatus() error {
func (s *serviceClient) onTrayReady() {
systray.SetIcon(s.icDisconnected)
systray.SetTooltip("NetBird")
// setup systray menu items
s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected")

View File

@@ -2,5 +2,55 @@
// to parse and normalize dns records and configuration
package dns
// DefaultDNSPort well-known port number
const DefaultDNSPort = 53
import (
"fmt"
"github.com/miekg/dns"
)
const (
// DefaultDNSPort well-known port number
DefaultDNSPort = 53
// RootZone is a string representation of the root zone
RootZone = "."
// DefaultClass is the class supported by the system
DefaultClass = "IN"
)
// Update represents a dns update that is exchanged between management and peers
type Update struct {
// ServiceEnable indicates if the service should be enabled
ServiceEnable bool
// NameServerGroups contains a list of nameserver group
NameServerGroups []NameServerGroup
// CustomZones contains a list of custom zone
CustomZones []CustomZone
}
// CustomZone represents a custom zone to be resolved by the dns server
type CustomZone struct {
// Domain is the zone's domain
Domain string
// Records custom zone records
Records []SimpleRecord
}
// SimpleRecord provides a simple DNS record specification for CNAME, A and AAAA records
type SimpleRecord struct {
// Name domain name
Name string
// Type of record, 1 for A, 5 for CNAME, 28 for AAAA. see https://pkg.go.dev/github.com/miekg/dns@v1.1.41#pkg-constants
Type int
// Class dns class, currently use the DefaultClass for all records
Class string
// TTL time-to-live for the record
TTL int
// RData is the actual value resolved in a dns query
RData string
}
// String returns a string of the simple record formatted as:
// <Name> <TTL> <Class> <Type> <RDATA>
func (s SimpleRecord) String() string {
fqdn := dns.Fqdn(s.Name)
return fmt.Sprintf("%s %d %s %s %s", fqdn, s.TTL, s.Class, dns.Type(s.Type).String(), s.RData)
}

View File

@@ -9,8 +9,6 @@ import (
)
const (
// MaxGroupNameChar maximum group name size
MaxGroupNameChar = 40
// InvalidNameServerType invalid nameserver type
InvalidNameServerType NameServerType = iota
// UDPNameServerType udp nameserver type
@@ -18,6 +16,8 @@ const (
)
const (
// MaxGroupNameChar maximum group name size
MaxGroupNameChar = 40
// InvalidNameServerTypeString invalid nameserver type as string
InvalidNameServerTypeString = "invalid"
// UDPNameServerTypeString udp nameserver type as string
@@ -59,6 +59,10 @@ type NameServerGroup struct {
NameServers []NameServer
// Groups list of peer group IDs to distribute the nameservers information
Groups []string
// Primary indicates that the nameserver group is the primary resolver for any dns query
Primary bool
// Domains indicate the dns query domains to use with this nameserver group
Domains []string
// Enabled group status
Enabled bool
}
@@ -128,6 +132,8 @@ func (g *NameServerGroup) Copy() *NameServerGroup {
NameServers: g.NameServers,
Groups: g.Groups,
Enabled: g.Enabled,
Primary: g.Primary,
Domains: g.Domains,
}
}
@@ -136,8 +142,10 @@ func (g *NameServerGroup) IsEqual(other *NameServerGroup) bool {
return other.ID == g.ID &&
other.Name == g.Name &&
other.Description == g.Description &&
other.Primary == g.Primary &&
compareNameServerList(g.NameServers, other.NameServers) &&
compareGroupsList(g.Groups, other.Groups)
compareGroupsList(g.Groups, other.Groups) &&
compareGroupsList(g.Domains, other.Domains)
}
func compareNameServerList(list, other []NameServer) bool {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 887 KiB

23
go.mod
View File

@@ -18,12 +18,12 @@ require (
github.com/spf13/pflag v1.0.5
github.com/vishvananda/netlink v1.1.0
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8
golang.zx2c4.com/wireguard v0.0.0-20211209221555-9c9e7e272434
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211215182854-7a385b3431de
golang.zx2c4.com/wireguard/windows v0.5.1
google.golang.org/grpc v1.43.0
google.golang.org/protobuf v1.28.0
google.golang.org/protobuf v1.28.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
@@ -38,10 +38,15 @@ require (
github.com/google/nftables v0.0.0-20220808154552-2eca00135732
github.com/libp2p/go-netroute v0.2.0
github.com/magiconair/properties v1.8.5
github.com/miekg/dns v1.1.41
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.13.0
github.com/rs/xid v1.3.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.8.0
go.opentelemetry.io/otel/exporters/prometheus v0.33.0
go.opentelemetry.io/otel/metric v0.33.0
go.opentelemetry.io/otel/sdk/metric v0.33.0
golang.org/x/net v0.0.0-20220630215102-69896b714898
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
)
@@ -65,11 +70,13 @@ require (
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/godbus/dbus/v5 v5.0.4 // indirect
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
@@ -89,20 +96,22 @@ require (
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.12.2 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.33.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
github.com/yuin/goldmark v1.4.1 // indirect
go.opentelemetry.io/otel v1.11.1 // indirect
go.opentelemetry.io/otel/sdk v1.11.1 // indirect
go.opentelemetry.io/otel/trace v1.11.1 // indirect
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect

44
go.sum
View File

@@ -202,6 +202,11 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -278,8 +283,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -450,6 +455,7 @@ github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb h1:2dC7L10LmTqlyMV
github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
@@ -542,8 +548,8 @@ github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -554,15 +560,16 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.33.0 h1:rHgav/0a6+uYgGdNt3jwz8FNSesO/Hsang3O0T9A5SE=
github.com/prometheus/common v0.33.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
@@ -648,6 +655,18 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
go.opentelemetry.io/otel/exporters/prometheus v0.33.0 h1:xXhPj7SLKWU5/Zd4Hxmd+X1C4jdmvc0Xy+kvjFx2z60=
go.opentelemetry.io/otel/exporters/prometheus v0.33.0/go.mod h1:ZSmYfKdYWEdSDBB4njLBIwTf4AU2JNsH3n2quVQDebI=
go.opentelemetry.io/otel/metric v0.33.0 h1:xQAyl7uGEYvrLAiV/09iTJlp1pZnQ9Wl793qbVvED1E=
go.opentelemetry.io/otel/metric v0.33.0/go.mod h1:QlTYc+EnYNq/M2mNk1qDDMRLpqCOj2f/r5c7Fd5FYaI=
go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZpKxs=
go.opentelemetry.io/otel/sdk v1.11.1/go.mod h1:/l3FE4SupHJ12TduVjUkZtlfFqDCQJlOlithYrdktys=
go.opentelemetry.io/otel/sdk/metric v0.33.0 h1:oTqyWfksgKoJmbrs2q7O7ahkJzt+Ipekihf8vhpa9qo=
go.opentelemetry.io/otel/sdk/metric v0.33.0/go.mod h1:xdypMeA21JBOvjjzDUtD0kzIcHO/SPez+a8HOzJPGp0=
go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
@@ -809,8 +828,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -915,8 +935,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
@@ -1162,8 +1182,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -89,7 +89,6 @@ func getIfaceAddrs(ifaceName string) ([]net.Addr, error) {
return addrs, nil
}
//
func Test_CreateInterface(t *testing.T) {
ifaceName := fmt.Sprintf("utun%d", WgIntNumber+1)
wgIP := "10.99.99.1/32"
@@ -369,8 +368,8 @@ func Test_ConnectPeers(t *testing.T) {
if err != nil {
t.Fatal(err)
}
timeout := 10 * time.Second
// todo: investigate why in some tests execution we need 30s
timeout := 30 * time.Second
timeoutChannel := time.After(timeout)
for {
select {

View File

@@ -214,8 +214,9 @@ func isBuiltinModule(name string) (bool, error) {
}
// /proc/modules
// name | memory size | reference count | references | state: <Live|Loading|Unloading>
// macvlan 28672 1 macvtap, Live 0x0000000000000000
//
// name | memory size | reference count | references | state: <Live|Loading|Unloading>
// macvlan 28672 1 macvtap, Live 0x0000000000000000
func moduleStatus(name string) (status, error) {
state := unknown
f, err := os.Open("/proc/modules")

View File

@@ -10,6 +10,8 @@ NETBIRD_MGMT_API_ENDPOINT=https://$NETBIRD_DOMAIN:$NETBIRD_MGMT_API_PORT
NETBIRD_MGMT_API_CERT_FILE="/etc/letsencrypt/live/$NETBIRD_DOMAIN/fullchain.pem"
# Management Certficate key file path.
NETBIRD_MGMT_API_CERT_KEY_FILE="/etc/letsencrypt/live/$NETBIRD_DOMAIN/privkey.pem"
# By default Management single account mode is enabled and domain set to $NETBIRD_DOMAIN, you may want to set this to your user's email domain
NETBIRD_MGMT_SINGLE_ACCOUNT_MODE_DOMAIN=$NETBIRD_DOMAIN
# Turn credentials

View File

@@ -48,7 +48,7 @@ services:
# # port and command for Let's Encrypt validation without dashboard container
# - 443:443
# command: ["--letsencrypt-domain", "$NETBIRD_DOMAIN", "--log-file", "console"]
command: ["--port", "443", "--log-file", "console", "--disable-anonymous-metrics=$NETBIRD_DISABLE_ANONYMOUS_METRICS"]
command: ["--port", "443", "--log-file", "console", "--disable-anonymous-metrics=$NETBIRD_DISABLE_ANONYMOUS_METRICS", "--single-account-mode-domain=$NETBIRD_MGMT_SINGLE_ACCOUNT_MODE_DOMAIN"]
# Coturn
coturn:
image: coturn/coturn

View File

@@ -55,12 +55,12 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
}
peersUpdateManager := mgmt.NewPeersUpdateManager()
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil)
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "")
if err != nil {
t.Fatal(err)
}
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/google/uuid"
httpapi "github.com/netbirdio/netbird/management/server/http"
"github.com/netbirdio/netbird/management/server/metrics"
"github.com/netbirdio/netbird/management/server/telemetry"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
@@ -41,11 +42,13 @@ import (
const ManagementLegacyPort = 33073
var (
mgmtPort int
mgmtLetsencryptDomain string
certFile string
certKey string
config *server.Config
mgmtPort int
mgmtMetricsPort int
mgmtLetsencryptDomain string
mgmtSingleAccModeDomain string
certFile string
certKey string
config *server.Config
kaep = keepalive.EnforcementPolicy{
MinTime: 15 * time.Second,
@@ -113,15 +116,27 @@ var (
}
peersUpdateManager := server.NewPeersUpdateManager()
appMetrics, err := telemetry.NewDefaultAppMetrics(cmd.Context())
if err != nil {
return err
}
err = appMetrics.Expose(mgmtMetricsPort, "/metrics")
if err != nil {
return err
}
var idpManager idp.Manager
if config.IdpManagerConfig != nil {
idpManager, err = idp.NewManager(*config.IdpManagerConfig)
idpManager, err = idp.NewManager(*config.IdpManagerConfig, appMetrics)
if err != nil {
return fmt.Errorf("failed retrieving a new idp manager with err: %v", err)
}
}
accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager)
if disableSingleAccMode {
mgmtSingleAccModeDomain = ""
}
accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain)
if err != nil {
return fmt.Errorf("failed to build default manager: %v", err)
}
@@ -151,14 +166,14 @@ var (
tlsEnabled = true
}
httpAPIHandler, err := httpapi.APIHandler(accountManager,
config.HttpConfig.AuthIssuer, config.HttpConfig.AuthAudience, config.HttpConfig.AuthKeysLocation)
httpAPIHandler, err := httpapi.APIHandler(accountManager, config.HttpConfig.AuthIssuer,
config.HttpConfig.AuthAudience, config.HttpConfig.AuthKeysLocation, appMetrics)
if err != nil {
return fmt.Errorf("failed creating HTTP API handler: %v", err)
}
gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
srv, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
srv, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, appMetrics)
if err != nil {
return fmt.Errorf("failed creating gRPC API handler: %v", err)
}
@@ -170,8 +185,6 @@ var (
return err
}
fmt.Println("metrics ", disableMetrics)
if !disableMetrics {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -225,6 +238,7 @@ var (
SetupCloseHandler()
<-stopCh
_ = appMetrics.Close()
_ = listener.Close()
if certManager != nil {
_ = certManager.Listener().Close()
@@ -427,7 +441,7 @@ func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {
return nil, err
}
// Create the credentials and return it
// NewDefaultAppMetrics the credentials and return it
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.NoClientCert,

View File

@@ -13,21 +13,23 @@ const (
)
var (
defaultMgmtConfigDir string
defaultMgmtDataDir string
defaultMgmtConfig string
defaultLogDir string
defaultLogFile string
oldDefaultMgmtConfigDir string
oldDefaultMgmtDataDir string
oldDefaultMgmtConfig string
oldDefaultLogDir string
oldDefaultLogFile string
mgmtDataDir string
mgmtConfig string
logLevel string
logFile string
disableMetrics bool
defaultMgmtConfigDir string
defaultMgmtDataDir string
defaultMgmtConfig string
defaultSingleAccModeDomain string
defaultLogDir string
defaultLogFile string
oldDefaultMgmtConfigDir string
oldDefaultMgmtDataDir string
oldDefaultMgmtConfig string
oldDefaultLogDir string
oldDefaultLogFile string
mgmtDataDir string
mgmtConfig string
logLevel string
logFile string
disableMetrics bool
disableSingleAccMode bool
rootCmd = &cobra.Command{
Use: "netbird-mgmt",
@@ -48,6 +50,7 @@ func init() {
stopCh = make(chan int)
defaultMgmtDataDir = "/var/lib/netbird/"
defaultSingleAccModeDomain = "netbird.selfhosted"
defaultMgmtConfigDir = "/etc/netbird"
defaultLogDir = "/var/log/netbird"
@@ -62,9 +65,12 @@ func init() {
oldDefaultLogFile = oldDefaultLogDir + "/management.log"
mgmtCmd.Flags().IntVar(&mgmtPort, "port", 80, "server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
mgmtCmd.Flags().IntVar(&mgmtMetricsPort, "metrics-port", 8081, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
mgmtCmd.Flags().StringVar(&mgmtDataDir, "datadir", defaultMgmtDataDir, "server data directory location")
mgmtCmd.Flags().StringVar(&mgmtConfig, "config", defaultMgmtConfig, "Netbird config file location. Config params specified via command line (e.g. datadir) have a precedence over configuration from this file")
mgmtCmd.Flags().StringVar(&mgmtLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
mgmtCmd.Flags().StringVar(&mgmtSingleAccModeDomain, "single-account-mode-domain", defaultSingleAccModeDomain, "Enables single account mode. This means that all the users will be under the same account grouped by the specified domain. If the installation has more than one account, the property is ineffective. Enabled by default with the default domain "+defaultSingleAccModeDomain)
mgmtCmd.Flags().BoolVar(&disableSingleAccMode, "disable-single-account-mode", false, "If set to true, disables single account mode. The --single-account-mode-domain property will be ignored and every new user will have a separate NetBird account.")
mgmtCmd.Flags().StringVar(&certFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
mgmtCmd.Flags().StringVar(&certKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
mgmtCmd.Flags().BoolVar(&disableMetrics, "disable-anonymous-metrics", false, "disables push of anonymous usage metrics to NetBird")

View File

@@ -15,6 +15,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"math/rand"
"net/netip"
"reflect"
"regexp"
"strings"
@@ -47,17 +48,17 @@ type AccountManager interface {
) (*SetupKey, error)
SaveSetupKey(accountID string, key *SetupKey) (*SetupKey, error)
CreateUser(accountID string, key *UserInfo) (*UserInfo, error)
ListSetupKeys(accountID string) ([]*SetupKey, error)
ListSetupKeys(accountID, userID string) ([]*SetupKey, error)
SaveUser(accountID string, key *User) (*UserInfo, error)
GetSetupKey(accountID, keyID string) (*SetupKey, error)
GetSetupKey(accountID, userID, keyID string) (*SetupKey, error)
GetAccountById(accountId string) (*Account, error)
GetAccountByUserOrAccountId(userId, accountId, domain string) (*Account, error)
GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*Account, error)
IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error)
AccountExists(accountId string) (*bool, error)
GetPeer(peerKey string) (*Peer, error)
GetPeers(accountID, userID string) ([]*Peer, error)
MarkPeerConnected(peerKey string, connected bool) error
RenamePeer(accountId string, peerKey string, newName string) (*Peer, error)
DeletePeer(accountId string, peerKey string) (*Peer, error)
GetPeerByIP(accountId string, peerIP string) (*Peer, error)
UpdatePeer(accountID string, peer *Peer) (*Peer, error)
@@ -66,7 +67,7 @@ type AccountManager interface {
AddPeer(setupKey string, userId string, peer *Peer) (*Peer, error)
UpdatePeerMeta(peerKey string, meta PeerSystemMeta) error
UpdatePeerSSHKey(peerKey string, sshKey string) error
GetUsersFromAccount(accountId string) ([]*UserInfo, error)
GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error)
GetGroup(accountId, groupID string) (*Group, error)
SaveGroup(accountId string, group *Group) error
UpdateGroup(accountID string, groupID string, operations []GroupUpdateOperation) (*Group, error)
@@ -75,19 +76,19 @@ type AccountManager interface {
GroupAddPeer(accountId, groupID, peerKey string) error
GroupDeletePeer(accountId, groupID, peerKey string) error
GroupListPeers(accountId, groupID string) ([]*Peer, error)
GetRule(accountId, ruleID string) (*Rule, error)
GetRule(accountID, ruleID, userID string) (*Rule, error)
SaveRule(accountID string, rule *Rule) error
UpdateRule(accountID string, ruleID string, operations []RuleUpdateOperation) (*Rule, error)
DeleteRule(accountId, ruleID string) error
ListRules(accountId string) ([]*Rule, error)
GetRoute(accountID, routeID string) (*route.Route, error)
ListRules(accountID, userID string) ([]*Rule, error)
GetRoute(accountID, routeID, userID string) (*route.Route, error)
CreateRoute(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error)
SaveRoute(accountID string, route *route.Route) error
UpdateRoute(accountID string, routeID string, operations []RouteUpdateOperation) (*route.Route, error)
DeleteRoute(accountID, routeID string) error
ListRoutes(accountID string) ([]*route.Route, error)
ListRoutes(accountID, userID string) ([]*route.Route, error)
GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error)
CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error)
CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool) (*nbdns.NameServerGroup, error)
SaveNameServerGroup(accountID string, nsGroupToSave *nbdns.NameServerGroup) error
UpdateNameServerGroup(accountID, nsGroupID string, operations []NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error)
DeleteNameServerGroup(accountID, nsGroupID string) error
@@ -106,6 +107,13 @@ type DefaultAccountManager struct {
idpManager idp.Manager
cacheManager cache.CacheInterface[[]*idp.UserData]
ctx context.Context
// singleAccountMode indicates whether the instance has a single account.
// If true, then every new user will end up under the same account.
// This value will be set to false if management service has more than one account.
singleAccountMode bool
// singleAccountModeDomain is a domain to use in singleAccountMode setup
singleAccountModeDomain string
}
// Account represents a unique account of the system
@@ -135,6 +143,142 @@ type UserInfo struct {
Status string `json:"-"`
}
// GetPeersRoutes returns all active routes of provided peers
func (a *Account) GetPeersRoutes(givenPeers []*Peer) []*route.Route {
//TODO Peer.ID migration: we will need to replace search by Peer.ID here
routes := make([]*route.Route, 0)
for _, peer := range givenPeers {
peerRoutes := a.GetPeerRoutes(peer.Key)
activeRoutes := make([]*route.Route, 0)
for _, pr := range peerRoutes {
if pr.Enabled {
activeRoutes = append(activeRoutes, pr)
}
}
if len(activeRoutes) > 0 {
routes = append(routes, activeRoutes...)
}
}
return routes
}
// GetPeerRoutes returns a list of routes of a given peer
func (a *Account) GetPeerRoutes(peerPubKey string) []*route.Route {
//TODO Peer.ID migration: we will need to replace search by Peer.ID here
var routes []*route.Route
for _, r := range a.Routes {
if r.Peer == peerPubKey {
routes = append(routes, r)
continue
}
}
return routes
}
// GetRoutesByPrefix return list of routes by account and route prefix
func (a *Account) GetRoutesByPrefix(prefix netip.Prefix) []*route.Route {
var routes []*route.Route
for _, r := range a.Routes {
if r.Network.String() == prefix.String() {
routes = append(routes, r)
}
}
return routes
}
// GetPeerRules returns a list of source or destination rules of a given peer.
func (a *Account) GetPeerRules(peerPubKey string) (srcRules []*Rule, dstRules []*Rule) {
// Rules are group based so there is no direct access to peers.
// First, find all groups that the given peer belongs to
peerGroups := make(map[string]struct{})
for s, group := range a.Groups {
for _, peer := range group.Peers {
if peerPubKey == peer {
peerGroups[s] = struct{}{}
break
}
}
}
// Second, find all rules that have discovered source and destination groups
srcRulesMap := make(map[string]*Rule)
dstRulesMap := make(map[string]*Rule)
for _, rule := range a.Rules {
for _, g := range rule.Source {
if _, ok := peerGroups[g]; ok && srcRulesMap[rule.ID] == nil {
srcRules = append(srcRules, rule)
srcRulesMap[rule.ID] = rule
}
}
for _, g := range rule.Destination {
if _, ok := peerGroups[g]; ok && dstRulesMap[rule.ID] == nil {
dstRules = append(dstRules, rule)
dstRulesMap[rule.ID] = rule
}
}
}
return srcRules, dstRules
}
// GetPeers returns a list of all Account peers
func (a *Account) GetPeers() []*Peer {
var peers []*Peer
for _, peer := range a.Peers {
peers = append(peers, peer)
}
return peers
}
// UpdatePeer saves new or replaces existing peer
func (a *Account) UpdatePeer(update *Peer) {
//TODO Peer.ID migration: we will need to replace search by Peer.ID here
a.Peers[update.Key] = update
}
// DeletePeer deletes peer from the account cleaning up all the references
func (a *Account) DeletePeer(peerPubKey string) {
// TODO Peer.ID migration: we will need to replace search by Peer.ID here
// delete peer from groups
for _, g := range a.Groups {
for i, pk := range g.Peers {
if pk == peerPubKey {
g.Peers = append(g.Peers[:i], g.Peers[i+1:]...)
break
}
}
}
delete(a.Peers, peerPubKey)
a.Network.IncSerial()
}
// 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) {
for _, peer := range a.Peers {
if peer.Key == peerPubKey {
return peer.Copy(), nil
}
}
return nil, status.Errorf(codes.NotFound, "peer with the public key %s not found", peerPubKey)
}
// FindUser looks for a given user in the Account or returns error if user wasn't found.
func (a *Account) FindUser(userID string) (*User, error) {
user := a.Users[userID]
if user == nil {
return nil, Errorf(UserNotFound, "user %s not found", userID)
}
return user, nil
}
func (a *Account) Copy() *Account {
peers := map[string]*Peer{}
for id, peer := range a.Peers {
@@ -172,16 +316,19 @@ func (a *Account) Copy() *Account {
}
return &Account{
Id: a.Id,
CreatedBy: a.CreatedBy,
SetupKeys: setupKeys,
Network: a.Network.Copy(),
Peers: peers,
Users: users,
Groups: groups,
Rules: rules,
Routes: routes,
NameServerGroups: nsGroups,
Id: a.Id,
CreatedBy: a.CreatedBy,
Domain: a.Domain,
DomainCategory: a.DomainCategory,
IsDomainPrimaryAccount: a.IsDomainPrimaryAccount,
SetupKeys: setupKeys,
Network: a.Network.Copy(),
Peers: peers,
Users: users,
Groups: groups,
Rules: rules,
Routes: routes,
NameServerGroups: nsGroups,
}
}
@@ -195,9 +342,8 @@ func (a *Account) GetGroupAll() (*Group, error) {
}
// BuildManager creates a new DefaultAccountManager with a provided Store
func BuildManager(
store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager,
) (*DefaultAccountManager, error) {
func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager,
singleAccountModeDomain string) (*DefaultAccountManager, error) {
am := &DefaultAccountManager{
Store: store,
mux: sync.Mutex{},
@@ -207,11 +353,20 @@ func BuildManager(
cacheMux: sync.Mutex{},
cacheLoading: map[string]chan struct{}{},
}
allAccounts := store.GetAllAccounts()
// enable single account mode only if configured by user and number of existing accounts is not grater than 1
am.singleAccountMode = singleAccountModeDomain != "" && len(allAccounts) <= 1
if am.singleAccountMode {
am.singleAccountModeDomain = singleAccountModeDomain
log.Infof("single account mode enabled, accounts number %d", len(allAccounts))
} else {
log.Infof("single account mode disabled, accounts number %d", len(allAccounts))
}
// if account has not default group
// if account doesn't have a default group
// we create 'all' group and add all peers into it
// also we create default rule with source as destination
for _, account := range store.GetAllAccounts() {
for _, account := range allAccounts {
_, err := account.GetGroupAll()
if err != nil {
addAllGroup(account)
@@ -221,10 +376,10 @@ func BuildManager(
}
}
gocacheClient := gocache.New(CacheExpirationMax, 30*time.Minute)
gocacheStore := cacheStore.NewGoCache(gocacheClient)
goCacheClient := gocache.New(CacheExpirationMax, 30*time.Minute)
goCacheStore := cacheStore.NewGoCache(goCacheClient)
am.cacheManager = cache.NewLoadable[[]*idp.UserData](am.loadAccount, cache.New[[]*idp.UserData](gocacheStore))
am.cacheManager = cache.NewLoadable[[]*idp.UserData](am.loadAccount, cache.New[[]*idp.UserData](goCacheStore))
if !isNil(am.idpManager) {
go func() {
@@ -585,7 +740,7 @@ func (am *DefaultAccountManager) redeemInvite(account *Account, userID string) e
return status.Errorf(codes.NotFound, "user %s not found in the IdP", userID)
}
if user.AppMetadata.WTPendingInvite {
if user.AppMetadata.WTPendingInvite != nil && *user.AppMetadata.WTPendingInvite {
log.Infof("redeeming invite for user %s account %s", userID, account.Id)
// User has already logged in, meaning that IdP should have set wt_pending_invite to false.
// Our job is to just reload cache.
@@ -604,6 +759,15 @@ func (am *DefaultAccountManager) redeemInvite(account *Account, userID string) e
// GetAccountFromToken returns an account associated with this token
func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*Account, error) {
if am.singleAccountMode && am.singleAccountModeDomain != "" {
// This section is mostly related to self-hosted installations.
// We override incoming domain claims to group users under a single account.
claims.Domain = am.singleAccountModeDomain
claims.DomainCategory = PrivateCategory
log.Infof("overriding JWT Domain and DomainCategory claims since single account mode is enabled")
}
account, err := am.getAccountWithAuthorizationClaims(claims)
if err != nil {
return nil, err
@@ -664,7 +828,7 @@ func (am *DefaultAccountManager) getAccountWithAuthorizationClaims(
return nil, err
}
account, err := am.Store.GetUserAccount(claims.UserId)
account, err := am.Store.GetAccountByUser(claims.UserId)
if err == nil {
err = am.handleExistingUserAccount(account, domainAccount, claims)
if err != nil {

View File

@@ -1,6 +1,7 @@
package server
import (
"github.com/netbirdio/netbird/route"
"net"
"sync"
"testing"
@@ -874,7 +875,7 @@ func TestGetUsersFromAccount(t *testing.T) {
account.Users[user.Id] = user
}
userInfos, err := manager.GetUsersFromAccount(accountId)
userInfos, err := manager.GetUsersFromAccount(accountId, "1")
if err != nil {
t.Fatal(err)
}
@@ -957,12 +958,173 @@ func TestAccountManager_UpdatePeerMeta(t *testing.T) {
assert.Equal(t, newMeta, p.Meta)
}
func TestAccount_GetPeerRules(t *testing.T) {
groups := map[string]*Group{
"group_1": {
ID: "group_1",
Name: "group_1",
Peers: []string{"peer-1", "peer-2"},
},
"group_2": {
ID: "group_2",
Name: "group_2",
Peers: []string{"peer-2", "peer-3"},
},
"group_3": {
ID: "group_3",
Name: "group_3",
Peers: []string{"peer-4"},
},
"group_4": {
ID: "group_4",
Name: "group_4",
Peers: []string{"peer-1"},
},
"group_5": {
ID: "group_5",
Name: "group_5",
Peers: []string{"peer-1"},
},
}
rules := map[string]*Rule{
"rule-1": {
ID: "rule-1",
Name: "rule-1",
Description: "rule-1",
Disabled: false,
Source: []string{"group_1", "group_5"},
Destination: []string{"group_2"},
Flow: 0,
},
"rule-2": {
ID: "rule-2",
Name: "rule-2",
Description: "rule-2",
Disabled: false,
Source: []string{"group_1"},
Destination: []string{"group_1"},
Flow: 0,
},
"rule-3": {
ID: "rule-3",
Name: "rule-3",
Description: "rule-3",
Disabled: false,
Source: []string{"group_3"},
Destination: []string{"group_3"},
Flow: 0,
},
}
account := &Account{
Groups: groups,
Rules: rules,
}
srcRules, dstRules := account.GetPeerRules("peer-1")
assert.Equal(t, 2, len(srcRules))
assert.Equal(t, 1, len(dstRules))
}
func TestFileStore_GetRoutesByPrefix(t *testing.T) {
_, prefix, err := route.ParseNetwork("192.168.64.0/24")
if err != nil {
t.Fatal(err)
}
account := &Account{
Routes: map[string]*route.Route{
"route-1": {
ID: "route-1",
Network: prefix,
NetID: "network-1",
Description: "network-1",
Peer: "peer-1",
NetworkType: 0,
Masquerade: false,
Metric: 999,
Enabled: true,
},
"route-2": {
ID: "route-2",
Network: prefix,
NetID: "network-1",
Description: "network-1",
Peer: "peer-2",
NetworkType: 0,
Masquerade: false,
Metric: 999,
Enabled: true,
},
},
}
routes := account.GetRoutesByPrefix(prefix)
assert.Len(t, routes, 2)
routeIDs := make(map[string]struct{}, 2)
for _, r := range routes {
routeIDs[r.ID] = struct{}{}
}
assert.Contains(t, routeIDs, "route-1")
assert.Contains(t, routeIDs, "route-2")
}
func TestAccount_GetPeersRoutes(t *testing.T) {
_, prefix, err := route.ParseNetwork("192.168.64.0/24")
if err != nil {
t.Fatal(err)
}
account := &Account{
Peers: map[string]*Peer{
"peer-1": {Key: "peer-1"}, "peer-2": {Key: "peer-2"}, "peer-3": {Key: "peer-1"},
},
Routes: map[string]*route.Route{
"route-1": {
ID: "route-1",
Network: prefix,
NetID: "network-1",
Description: "network-1",
Peer: "peer-1",
NetworkType: 0,
Masquerade: false,
Metric: 999,
Enabled: true,
},
"route-2": {
ID: "route-2",
Network: prefix,
NetID: "network-1",
Description: "network-1",
Peer: "peer-2",
NetworkType: 0,
Masquerade: false,
Metric: 999,
Enabled: true,
},
},
}
routes := account.GetPeersRoutes([]*Peer{{Key: "peer-1"}, {Key: "peer-2"}, {Key: "non-existing-peer"}})
assert.Len(t, routes, 2)
routeIDs := make(map[string]struct{}, 2)
for _, r := range routes {
routeIDs[r.ID] = struct{}{}
}
assert.Contains(t, routeIDs, "route-1")
assert.Contains(t, routeIDs, "route-2")
}
func createManager(t *testing.T) (*DefaultAccountManager, error) {
store, err := createStore(t)
if err != nil {
return nil, err
}
return BuildManager(store, NewPeersUpdateManager(), nil)
return BuildManager(store, NewPeersUpdateManager(), nil, "")
}
func createStore(t *testing.T) (Store, error) {

View File

@@ -11,6 +11,12 @@ const (
AccountNotFound ErrorType = iota
// PreconditionFailed indicates that some pre-condition for the operation hasn't been fulfilled
PreconditionFailed ErrorType = iota
// UserNotFound indicates that user wasn't found in the system (or under a given Account)
UserNotFound ErrorType = iota
// PermissionDenied indicates that user has no permissions to view data
PermissionDenied ErrorType = iota
)
// ErrorType is a type of the Error

View File

@@ -1,9 +1,6 @@
package server
import (
"fmt"
"github.com/netbirdio/netbird/route"
"net/netip"
"os"
"path/filepath"
"strings"
@@ -21,14 +18,10 @@ const storeFileName = "store.json"
// FileStore represents an account storage backed by a file persisted to disk
type FileStore struct {
Accounts map[string]*Account
SetupKeyId2AccountId map[string]string `json:"-"`
PeerKeyId2AccountId map[string]string `json:"-"`
UserId2AccountId map[string]string `json:"-"`
PrivateDomain2AccountId map[string]string `json:"-"`
PeerKeyId2SrcRulesId map[string]map[string]struct{} `json:"-"`
PeerKeyId2DstRulesId map[string]map[string]struct{} `json:"-"`
PeerKeyID2RouteIDs map[string]map[string]struct{} `json:"-"`
AccountPrefix2RouteIDs map[string]map[string][]string `json:"-"`
SetupKeyID2AccountID map[string]string `json:"-"`
PeerKeyID2AccountID map[string]string `json:"-"`
UserID2AccountID map[string]string `json:"-"`
PrivateDomain2AccountID map[string]string `json:"-"`
InstallationID string
// mutex to synchronise Store read/write operations
@@ -43,7 +36,7 @@ func NewStore(dataDir string) (*FileStore, error) {
return restore(filepath.Join(dataDir, storeFileName))
}
// restore restores the state of the store from the file.
// restore the state of the store from the file.
// Creates a new empty store file if doesn't exist
func restore(file string) (*FileStore, error) {
if _, err := os.Stat(file); os.IsNotExist(err) {
@@ -51,14 +44,10 @@ func restore(file string) (*FileStore, error) {
s := &FileStore{
Accounts: make(map[string]*Account),
mux: sync.Mutex{},
SetupKeyId2AccountId: make(map[string]string),
PeerKeyId2AccountId: make(map[string]string),
UserId2AccountId: make(map[string]string),
PrivateDomain2AccountId: make(map[string]string),
PeerKeyId2SrcRulesId: make(map[string]map[string]struct{}),
PeerKeyID2RouteIDs: make(map[string]map[string]struct{}),
PeerKeyId2DstRulesId: make(map[string]map[string]struct{}),
AccountPrefix2RouteIDs: make(map[string]map[string][]string),
SetupKeyID2AccountID: make(map[string]string),
PeerKeyID2AccountID: make(map[string]string),
UserID2AccountID: make(map[string]string),
PrivateDomain2AccountID: make(map[string]string),
storeFile: file,
}
@@ -77,287 +66,78 @@ func restore(file string) (*FileStore, error) {
store := read.(*FileStore)
store.storeFile = file
store.SetupKeyId2AccountId = make(map[string]string)
store.PeerKeyId2AccountId = make(map[string]string)
store.UserId2AccountId = make(map[string]string)
store.PrivateDomain2AccountId = make(map[string]string)
store.PeerKeyId2SrcRulesId = make(map[string]map[string]struct{})
store.PeerKeyId2DstRulesId = make(map[string]map[string]struct{})
store.PeerKeyID2RouteIDs = make(map[string]map[string]struct{})
store.AccountPrefix2RouteIDs = make(map[string]map[string][]string)
store.SetupKeyID2AccountID = make(map[string]string)
store.PeerKeyID2AccountID = make(map[string]string)
store.UserID2AccountID = make(map[string]string)
store.PrivateDomain2AccountID = make(map[string]string)
for accountId, account := range store.Accounts {
for accountID, account := range store.Accounts {
for setupKeyId := range account.SetupKeys {
store.SetupKeyId2AccountId[strings.ToUpper(setupKeyId)] = accountId
}
for _, rule := range account.Rules {
for _, groupID := range rule.Source {
if group, ok := account.Groups[groupID]; ok {
for _, peerID := range group.Peers {
rules := store.PeerKeyId2SrcRulesId[peerID]
if rules == nil {
rules = map[string]struct{}{}
store.PeerKeyId2SrcRulesId[peerID] = rules
}
rules[rule.ID] = struct{}{}
}
}
}
for _, groupID := range rule.Destination {
if group, ok := account.Groups[groupID]; ok {
for _, peerID := range group.Peers {
rules := store.PeerKeyId2DstRulesId[peerID]
if rules == nil {
rules = map[string]struct{}{}
store.PeerKeyId2DstRulesId[peerID] = rules
}
rules[rule.ID] = struct{}{}
}
}
}
store.SetupKeyID2AccountID[strings.ToUpper(setupKeyId)] = accountID
}
for _, peer := range account.Peers {
store.PeerKeyId2AccountId[peer.Key] = accountId
store.PeerKeyID2AccountID[peer.Key] = accountID
// reset all peers to status = Disconnected
if peer.Status != nil && peer.Status.Connected {
peer.Status.Connected = false
}
}
for _, user := range account.Users {
store.UserId2AccountId[user.Id] = accountId
store.UserID2AccountID[user.Id] = accountID
}
for _, user := range account.Users {
store.UserId2AccountId[user.Id] = accountId
}
for _, route := range account.Routes {
if route.Peer == "" {
continue
}
if store.PeerKeyID2RouteIDs[route.Peer] == nil {
store.PeerKeyID2RouteIDs[route.Peer] = make(map[string]struct{})
}
store.PeerKeyID2RouteIDs[route.Peer][route.ID] = struct{}{}
if store.AccountPrefix2RouteIDs[account.Id] == nil {
store.AccountPrefix2RouteIDs[account.Id] = make(map[string][]string)
}
if _, ok := store.AccountPrefix2RouteIDs[account.Id][route.Network.String()]; !ok {
store.AccountPrefix2RouteIDs[account.Id][route.Network.String()] = make([]string, 0)
}
store.AccountPrefix2RouteIDs[account.Id][route.Network.String()] = append(
store.AccountPrefix2RouteIDs[account.Id][route.Network.String()],
route.ID,
)
store.UserID2AccountID[user.Id] = accountID
}
if account.Domain != "" && account.DomainCategory == PrivateCategory &&
account.IsDomainPrimaryAccount {
store.PrivateDomain2AccountId[account.Domain] = accountId
store.PrivateDomain2AccountID[account.Domain] = accountID
}
}
// we need this persist to apply changes we made to account.Peers (we set them to Disconnected)
err = store.persist(store.storeFile)
if err != nil {
return nil, err
}
return store, nil
}
// persist persists account data to a file
// persist account data to a file
// It is recommended to call it with locking FileStore.mux
func (s *FileStore) persist(file string) error {
return util.WriteJson(file, s)
}
// SavePeer saves updated peer
func (s *FileStore) SavePeer(accountId string, peer *Peer) error {
s.mux.Lock()
defer s.mux.Unlock()
account, err := s.GetAccount(accountId)
if err != nil {
return err
}
// if it is new peer, add it to default 'All' group
allGroup, err := account.GetGroupAll()
if err != nil {
return err
}
ind := -1
for i, pid := range allGroup.Peers {
if pid == peer.Key {
ind = i
break
}
}
if ind < 0 {
allGroup.Peers = append(allGroup.Peers, peer.Key)
}
account.Peers[peer.Key] = peer
return s.persist(s.storeFile)
}
// DeletePeer deletes peer from the Store
func (s *FileStore) DeletePeer(accountId string, peerKey string) (*Peer, error) {
s.mux.Lock()
defer s.mux.Unlock()
account, err := s.GetAccount(accountId)
if err != nil {
return nil, err
}
peer := account.Peers[peerKey]
if peer == nil {
return nil, status.Errorf(codes.NotFound, "peer not found")
}
peerRoutes := s.PeerKeyID2RouteIDs[peerKey]
delete(account.Peers, peerKey)
delete(s.PeerKeyId2AccountId, peerKey)
delete(s.PeerKeyId2DstRulesId, peerKey)
delete(s.PeerKeyId2SrcRulesId, peerKey)
delete(s.PeerKeyID2RouteIDs, peerKey)
// cleanup groups
for _, g := range account.Groups {
var peers []string
for _, p := range g.Peers {
if p != peerKey {
peers = append(peers, p)
}
}
g.Peers = peers
}
for routeID := range peerRoutes {
account.Routes[routeID].Enabled = false
account.Routes[routeID].Peer = ""
}
err = s.persist(s.storeFile)
if err != nil {
return nil, err
}
return peer, nil
}
// GetPeer returns a peer from a Store
func (s *FileStore) GetPeer(peerKey string) (*Peer, error) {
s.mux.Lock()
defer s.mux.Unlock()
accountId, accountIdFound := s.PeerKeyId2AccountId[peerKey]
if !accountIdFound {
return nil, status.Errorf(codes.NotFound, "peer not found")
}
account, err := s.GetAccount(accountId)
if err != nil {
return nil, err
}
if peer, ok := account.Peers[peerKey]; ok {
return peer, nil
}
return nil, status.Errorf(codes.NotFound, "peer not found")
}
// SaveAccount updates an existing account or adds a new one
func (s *FileStore) SaveAccount(account *Account) error {
s.mux.Lock()
defer s.mux.Unlock()
accountCopy := account.Copy()
// todo will override, handle existing keys
s.Accounts[account.Id] = account
s.Accounts[accountCopy.Id] = accountCopy
// todo check that account.Id and keyId are not exist already
// because if keyId exists for other accounts this can be bad
for keyId := range account.SetupKeys {
s.SetupKeyId2AccountId[strings.ToUpper(keyId)] = account.Id
for keyID := range accountCopy.SetupKeys {
s.SetupKeyID2AccountID[strings.ToUpper(keyID)] = accountCopy.Id
}
// enforce peer to account index and delete peer to route indexes for rebuild
for _, peer := range account.Peers {
s.PeerKeyId2AccountId[peer.Key] = account.Id
delete(s.PeerKeyID2RouteIDs, peer.Key)
for _, peer := range accountCopy.Peers {
s.PeerKeyID2AccountID[peer.Key] = accountCopy.Id
}
delete(s.AccountPrefix2RouteIDs, account.Id)
// remove all peers related to account from rules indexes
cleanIDs := make([]string, 0)
for key := range s.PeerKeyId2SrcRulesId {
if accountID, ok := s.PeerKeyId2AccountId[key]; ok && accountID == account.Id {
cleanIDs = append(cleanIDs, key)
}
}
for _, key := range cleanIDs {
delete(s.PeerKeyId2SrcRulesId, key)
}
cleanIDs = cleanIDs[:0]
for key := range s.PeerKeyId2DstRulesId {
if accountID, ok := s.PeerKeyId2AccountId[key]; ok && accountID == account.Id {
cleanIDs = append(cleanIDs, key)
}
}
for _, key := range cleanIDs {
delete(s.PeerKeyId2DstRulesId, key)
for _, user := range accountCopy.Users {
s.UserID2AccountID[user.Id] = accountCopy.Id
}
// rebuild rule indexes
for _, rule := range account.Rules {
for _, gid := range rule.Source {
g, ok := account.Groups[gid]
if !ok {
break
}
for _, pid := range g.Peers {
rules := s.PeerKeyId2SrcRulesId[pid]
if rules == nil {
rules = map[string]struct{}{}
s.PeerKeyId2SrcRulesId[pid] = rules
}
rules[rule.ID] = struct{}{}
}
}
for _, gid := range rule.Destination {
g, ok := account.Groups[gid]
if !ok {
break
}
for _, pid := range g.Peers {
rules := s.PeerKeyId2DstRulesId[pid]
if rules == nil {
rules = map[string]struct{}{}
s.PeerKeyId2DstRulesId[pid] = rules
}
rules[rule.ID] = struct{}{}
}
}
}
for _, route := range account.Routes {
if route.Peer == "" {
continue
}
if s.PeerKeyID2RouteIDs[route.Peer] == nil {
s.PeerKeyID2RouteIDs[route.Peer] = make(map[string]struct{})
}
s.PeerKeyID2RouteIDs[route.Peer][route.ID] = struct{}{}
if s.AccountPrefix2RouteIDs[account.Id] == nil {
s.AccountPrefix2RouteIDs[account.Id] = make(map[string][]string)
}
if _, ok := s.AccountPrefix2RouteIDs[account.Id][route.Network.String()]; !ok {
s.AccountPrefix2RouteIDs[account.Id][route.Network.String()] = make([]string, 0)
}
s.AccountPrefix2RouteIDs[account.Id][route.Network.String()] = append(
s.AccountPrefix2RouteIDs[account.Id][route.Network.String()],
route.ID,
)
}
for _, user := range account.Users {
s.UserId2AccountId[user.Id] = account.Id
}
if account.DomainCategory == PrivateCategory && account.IsDomainPrimaryAccount {
s.PrivateDomain2AccountId[account.Domain] = account.Id
if accountCopy.DomainCategory == PrivateCategory && accountCopy.IsDomainPrimaryAccount {
s.PrivateDomain2AccountID[accountCopy.Domain] = accountCopy.Id
}
return s.persist(s.storeFile)
@@ -365,53 +145,25 @@ func (s *FileStore) SaveAccount(account *Account) error {
// GetAccountByPrivateDomain returns account by private domain
func (s *FileStore) GetAccountByPrivateDomain(domain string) (*Account, error) {
accountId, accountIdFound := s.PrivateDomain2AccountId[strings.ToLower(domain)]
if !accountIdFound {
accountID, accountIDFound := s.PrivateDomain2AccountID[strings.ToLower(domain)]
if !accountIDFound {
return nil, status.Errorf(
codes.NotFound,
"provided domain is not registered or is not private",
"account not found: provided domain is not registered or is not private",
)
}
account, err := s.GetAccount(accountId)
if err != nil {
return nil, err
}
return account, nil
return s.GetAccount(accountID)
}
// GetAccountBySetupKey returns account by setup key id
func (s *FileStore) GetAccountBySetupKey(setupKey string) (*Account, error) {
accountId, accountIdFound := s.SetupKeyId2AccountId[strings.ToUpper(setupKey)]
if !accountIdFound {
return nil, status.Errorf(codes.NotFound, "provided setup key doesn't exists")
accountID, accountIDFound := s.SetupKeyID2AccountID[strings.ToUpper(setupKey)]
if !accountIDFound {
return nil, status.Errorf(codes.NotFound, "account not found: provided setup key doesn't exists")
}
account, err := s.GetAccount(accountId)
if err != nil {
return nil, err
}
return account, nil
}
// GetAccountPeers returns account peers
func (s *FileStore) GetAccountPeers(accountId string) ([]*Peer, error) {
s.mux.Lock()
defer s.mux.Unlock()
account, err := s.GetAccount(accountId)
if err != nil {
return nil, err
}
var peers []*Peer
for _, peer := range account.Peers {
peers = append(peers, peer)
}
return peers, nil
return s.GetAccount(accountID)
}
// GetAllAccounts returns all accounts
@@ -426,148 +178,39 @@ func (s *FileStore) GetAllAccounts() (all []*Account) {
}
// GetAccount returns an account for id
func (s *FileStore) GetAccount(accountId string) (*Account, error) {
account, accountFound := s.Accounts[accountId]
func (s *FileStore) GetAccount(accountID string) (*Account, error) {
account, accountFound := s.Accounts[accountID]
if !accountFound {
return nil, status.Errorf(codes.NotFound, "account not found")
}
return account, nil
return account.Copy(), nil
}
// GetUserAccount returns a user account
func (s *FileStore) GetUserAccount(userId string) (*Account, error) {
// GetAccountByUser returns a user account
func (s *FileStore) GetAccountByUser(userID string) (*Account, error) {
s.mux.Lock()
defer s.mux.Unlock()
accountId, accountIdFound := s.UserId2AccountId[userId]
if !accountIdFound {
accountID, accountIDFound := s.UserID2AccountID[userID]
if !accountIDFound {
return nil, status.Errorf(codes.NotFound, "account not found")
}
return s.GetAccount(accountId)
return s.GetAccount(accountID)
}
func (s *FileStore) getPeerAccount(peerKey string) (*Account, error) {
accountId, accountIdFound := s.PeerKeyId2AccountId[peerKey]
if !accountIdFound {
// GetAccountByPeerPubKey returns an account for a given peer WireGuard public key
func (s *FileStore) GetAccountByPeerPubKey(peerKey string) (*Account, error) {
s.mux.Lock()
defer s.mux.Unlock()
accountID, accountIDFound := s.PeerKeyID2AccountID[peerKey]
if !accountIDFound {
return nil, status.Errorf(codes.NotFound, "Provided peer key doesn't exists %s", peerKey)
}
return s.GetAccount(accountId)
}
// GetPeerAccount returns user account if exists
func (s *FileStore) GetPeerAccount(peerKey string) (*Account, error) {
s.mux.Lock()
defer s.mux.Unlock()
return s.getPeerAccount(peerKey)
}
// GetPeerSrcRules return list of source rules for peer
func (s *FileStore) GetPeerSrcRules(accountId, peerKey string) ([]*Rule, error) {
s.mux.Lock()
defer s.mux.Unlock()
account, err := s.GetAccount(accountId)
if err != nil {
return nil, err
}
ruleIDs, ok := s.PeerKeyId2SrcRulesId[peerKey]
if !ok {
return nil, fmt.Errorf("no rules for peer: %v", ruleIDs)
}
rules := []*Rule{}
for id := range ruleIDs {
rule, ok := account.Rules[id]
if ok {
rules = append(rules, rule)
}
}
return rules, nil
}
// GetPeerDstRules return list of destination rules for peer
func (s *FileStore) GetPeerDstRules(accountId, peerKey string) ([]*Rule, error) {
s.mux.Lock()
defer s.mux.Unlock()
account, err := s.GetAccount(accountId)
if err != nil {
return nil, err
}
ruleIDs, ok := s.PeerKeyId2DstRulesId[peerKey]
if !ok {
return nil, fmt.Errorf("no rules for peer: %v", ruleIDs)
}
rules := []*Rule{}
for id := range ruleIDs {
rule, ok := account.Rules[id]
if ok {
rules = append(rules, rule)
}
}
return rules, nil
}
// GetPeerRoutes return list of routes for peer
func (s *FileStore) GetPeerRoutes(peerKey string) ([]*route.Route, error) {
s.mux.Lock()
defer s.mux.Unlock()
account, err := s.getPeerAccount(peerKey)
if err != nil {
return nil, err
}
var routes []*route.Route
routeIDs, ok := s.PeerKeyID2RouteIDs[peerKey]
if !ok {
return routes, nil
}
for id := range routeIDs {
route, found := account.Routes[id]
if found {
routes = append(routes, route)
}
}
return routes, nil
}
// GetRoutesByPrefix return list of routes by account and route prefix
func (s *FileStore) GetRoutesByPrefix(accountID string, prefix netip.Prefix) ([]*route.Route, error) {
s.mux.Lock()
defer s.mux.Unlock()
account, err := s.GetAccount(accountID)
if err != nil {
return nil, err
}
routeIDs, ok := s.AccountPrefix2RouteIDs[accountID][prefix.String()]
if !ok {
return nil, status.Errorf(codes.NotFound, "no routes for prefix: %v", prefix.String())
}
var routes []*route.Route
for _, id := range routeIDs {
route, found := account.Routes[id]
if found {
routes = append(routes, route)
}
}
return routes, nil
return s.GetAccount(accountID)
}
// GetInstallationID returns the installation ID from the store

View File

@@ -2,6 +2,7 @@ package server
import (
"github.com/netbirdio/netbird/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net"
"path/filepath"
@@ -9,6 +10,10 @@ import (
"time"
)
type accounts struct {
Accounts map[string]*Account
}
func TestNewStore(t *testing.T) {
store := newStore(t)
@@ -16,16 +21,16 @@ func TestNewStore(t *testing.T) {
t.Errorf("expected to create a new empty Accounts map when creating a new FileStore")
}
if store.SetupKeyId2AccountId == nil || len(store.SetupKeyId2AccountId) != 0 {
t.Errorf("expected to create a new empty SetupKeyId2AccountId map when creating a new FileStore")
if store.SetupKeyID2AccountID == nil || len(store.SetupKeyID2AccountID) != 0 {
t.Errorf("expected to create a new empty SetupKeyID2AccountID map when creating a new FileStore")
}
if store.PeerKeyId2AccountId == nil || len(store.PeerKeyId2AccountId) != 0 {
t.Errorf("expected to create a new empty PeerKeyId2AccountId map when creating a new FileStore")
if store.PeerKeyID2AccountID == nil || len(store.PeerKeyID2AccountID) != 0 {
t.Errorf("expected to create a new empty PeerKeyID2AccountID map when creating a new FileStore")
}
if store.UserId2AccountId == nil || len(store.UserId2AccountId) != 0 {
t.Errorf("expected to create a new empty UserId2AccountId map when creating a new FileStore")
if store.UserID2AccountID == nil || len(store.UserID2AccountID) != 0 {
t.Errorf("expected to create a new empty UserID2AccountID map when creating a new FileStore")
}
}
@@ -55,16 +60,16 @@ func TestSaveAccount(t *testing.T) {
t.Errorf("expecting Account to be stored after SaveAccount()")
}
if store.PeerKeyId2AccountId["peerkey"] == "" {
t.Errorf("expecting PeerKeyId2AccountId index updated after SaveAccount()")
if store.PeerKeyID2AccountID["peerkey"] == "" {
t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount()")
}
if store.UserId2AccountId["testuser"] == "" {
t.Errorf("expecting UserId2AccountId index updated after SaveAccount()")
if store.UserID2AccountID["testuser"] == "" {
t.Errorf("expecting UserID2AccountID index updated after SaveAccount()")
}
if store.SetupKeyId2AccountId[setupKey.Key] == "" {
t.Errorf("expecting SetupKeyId2AccountId index updated after SaveAccount()")
if store.SetupKeyID2AccountID[setupKey.Key] == "" {
t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount()")
}
}
@@ -141,11 +146,11 @@ func TestRestore(t *testing.T) {
require.NotNil(t, account.SetupKeys["A2C8E62B-38F5-4553-B31E-DD66C696CEBB"], "failed to restore a FileStore file - missing Account SetupKey A2C8E62B-38F5-4553-B31E-DD66C696CEBB")
require.Len(t, store.UserId2AccountId, 2, "failed to restore a FileStore wrong UserId2AccountId mapping length")
require.Len(t, store.UserID2AccountID, 2, "failed to restore a FileStore wrong UserID2AccountID mapping length")
require.Len(t, store.SetupKeyId2AccountId, 1, "failed to restore a FileStore wrong SetupKeyId2AccountId mapping length")
require.Len(t, store.SetupKeyID2AccountID, 1, "failed to restore a FileStore wrong SetupKeyID2AccountID mapping length")
require.Len(t, store.PrivateDomain2AccountId, 1, "failed to restore a FileStore wrong PrivateDomain2AccountId mapping length")
require.Len(t, store.PrivateDomain2AccountID, 1, "failed to restore a FileStore wrong PrivateDomain2AccountID mapping length")
}
func TestGetAccountByPrivateDomain(t *testing.T) {
@@ -171,6 +176,48 @@ func TestGetAccountByPrivateDomain(t *testing.T) {
require.Error(t, err, "should return error on domain lookup")
}
func TestFileStore_GetAccount(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)
}
accounts := &accounts{}
_, err = util.ReadJson(storeFile, accounts)
if err != nil {
t.Fatal(err)
}
store, err := NewStore(storeDir)
if err != nil {
t.Fatal(err)
}
expected := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"]
if expected == nil {
t.Fatalf("expected account doesn't exist")
}
account, err := store.GetAccount(expected.Id)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expected.IsDomainPrimaryAccount, account.IsDomainPrimaryAccount)
assert.Equal(t, expected.DomainCategory, account.DomainCategory)
assert.Equal(t, expected.Domain, account.Domain)
assert.Equal(t, expected.CreatedBy, account.CreatedBy)
assert.Equal(t, expected.Network.Id, account.Network.Id)
assert.Len(t, account.Peers, len(expected.Peers))
assert.Len(t, account.Users, len(expected.Users))
assert.Len(t, account.SetupKeys, len(expected.SetupKeys))
assert.Len(t, account.Rules, len(expected.Rules))
assert.Len(t, account.Routes, len(expected.Routes))
assert.Len(t, account.NameServerGroups, len(expected.NameServerGroups))
}
func newStore(t *testing.T) *FileStore {
store, err := NewStore(t.TempDir())
if err != nil {

View File

@@ -3,6 +3,7 @@ package server
import (
"context"
"fmt"
"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/netbirdio/netbird/route"
gPeer "google.golang.org/grpc/peer"
"strings"
@@ -30,10 +31,12 @@ type GRPCServer struct {
config *Config
turnCredentialsManager TURNCredentialsManager
jwtMiddleware *middleware.JWTMiddleware
appMetrics telemetry.AppMetrics
}
// NewServer creates a new Management server
func NewServer(config *Config, accountManager AccountManager, peersUpdateManager *PeersUpdateManager, turnCredentialsManager TURNCredentialsManager) (*GRPCServer, error) {
func NewServer(config *Config, accountManager AccountManager, peersUpdateManager *PeersUpdateManager,
turnCredentialsManager TURNCredentialsManager, appMetrics telemetry.AppMetrics) (*GRPCServer, error) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
@@ -53,6 +56,16 @@ func NewServer(config *Config, accountManager AccountManager, peersUpdateManager
log.Debug("unable to use http config to create new jwt middleware")
}
if appMetrics != nil {
// update gauge based on number of connected peers which is equal to open gRPC streams
err = appMetrics.GRPCMetrics().RegisterConnectedStreams(func() int64 {
return int64(len(peersUpdateManager.peerChannels))
})
if err != nil {
return nil, err
}
}
return &GRPCServer{
wgKey: key,
// peerKey -> event channel
@@ -61,11 +74,15 @@ func NewServer(config *Config, accountManager AccountManager, peersUpdateManager
config: config,
turnCredentialsManager: turnCredentialsManager,
jwtMiddleware: jwtMiddleware,
appMetrics: appMetrics,
}, nil
}
func (s *GRPCServer) GetServerKey(ctx context.Context, req *proto.Empty) (*proto.ServerKeyResponse, error) {
// todo introduce something more meaningful with the key expiration/rotation
if s.appMetrics != nil {
s.appMetrics.GRPCMetrics().CountGetKeyRequest()
}
now := time.Now().Add(24 * time.Hour)
secs := int64(now.Second())
nanos := int32(now.Nanosecond())
@@ -80,6 +97,9 @@ func (s *GRPCServer) GetServerKey(ctx context.Context, req *proto.Empty) (*proto
// Sync validates the existence of a connecting peer, sends an initial state (all available for the connecting peers) and
// notifies the connected peer of any updates (e.g. new peers under the same account)
func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_SyncServer) error {
if s.appMetrics != nil {
s.appMetrics.GRPCMetrics().CountSyncRequest()
}
p, ok := gRPCPeer.FromContext(srv.Context())
if ok {
log.Debugf("Sync request from peer [%s] [%s]", req.WgPubKey, p.Addr.String())
@@ -259,6 +279,9 @@ func (s *GRPCServer) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest)
// In case it isn't, the endpoint checks whether setup key is provided within the request and tries to register a peer.
// In case of the successful registration login is also successful
func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
if s.appMetrics != nil {
s.appMetrics.GRPCMetrics().CountLoginRequest()
}
p, ok := gRPCPeer.FromContext(ctx)
if ok {
log.Debugf("Login request from peer [%s] [%s]", req.WgPubKey, p.Addr.String())

View File

@@ -444,12 +444,24 @@ components:
type: array
items:
type: string
primary:
description: Nameserver group primary status
type: boolean
domains:
description: Nameserver group domain list
type: array
items:
type: string
minLength: 1
maxLength: 255
required:
- name
- description
- nameservers
- enabled
- groups
- primary
- domains
NameserverGroup:
allOf:
- type: object

View File

@@ -159,6 +159,9 @@ type NameserverGroup struct {
// Description Nameserver group description
Description string `json:"description"`
// Domains Nameserver group domain list
Domains []string `json:"domains"`
// Enabled Nameserver group status
Enabled bool `json:"enabled"`
@@ -173,6 +176,9 @@ type NameserverGroup struct {
// Nameservers Nameserver group
Nameservers []Nameserver `json:"nameservers"`
// Primary Nameserver group primary status
Primary bool `json:"primary"`
}
// NameserverGroupPatchOperation defines model for NameserverGroupPatchOperation.
@@ -198,6 +204,9 @@ type NameserverGroupRequest struct {
// Description Nameserver group description
Description string `json:"description"`
// Domains Nameserver group domain list
Domains []string `json:"domains"`
// Enabled Nameserver group status
Enabled bool `json:"enabled"`
@@ -209,6 +218,9 @@ type NameserverGroupRequest struct {
// Nameservers Nameserver group
Nameservers []Nameserver `json:"nameservers"`
// Primary Nameserver group primary status
Primary bool `json:"primary"`
}
// PatchMinimum defines model for PatchMinimum.

View File

@@ -33,7 +33,7 @@ func NewGroups(accountManager server.AccountManager, authAudience string) *Group
// GetAllGroupsHandler list for the account
func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -50,7 +50,7 @@ func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
// UpdateGroupHandler handles update to a group identified by a given ID
func (h *Groups) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -111,7 +111,7 @@ func (h *Groups) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
// PatchGroupHandler handles patch updates to a group identified by a given ID
func (h *Groups) PatchGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -226,7 +226,7 @@ func (h *Groups) PatchGroupHandler(w http.ResponseWriter, r *http.Request) {
// CreateGroupHandler handles group creation request
func (h *Groups) CreateGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -260,7 +260,7 @@ func (h *Groups) CreateGroupHandler(w http.ResponseWriter, r *http.Request) {
// DeleteGroupHandler handles group deletion request
func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -295,7 +295,7 @@ func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) {
// GetGroupHandler returns a group
func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return

View File

@@ -21,11 +21,11 @@ import (
)
var TestPeers = map[string]*server.Peer{
"A": &server.Peer{Key: "A", IP: net.ParseIP("100.100.100.100")},
"B": &server.Peer{Key: "B", IP: net.ParseIP("200.200.200.200")},
"A": {Key: "A", IP: net.ParseIP("100.100.100.100")},
"B": {Key: "B", IP: net.ParseIP("200.200.200.200")},
}
func initGroupTestData(groups ...*server.Group) *Groups {
func initGroupTestData(user *server.User, groups ...*server.Group) *Groups {
return &Groups{
accountManager: &mock_server.MockAccountManager{
SaveGroupFunc: func(accountID string, group *server.Group) error {
@@ -72,6 +72,9 @@ func initGroupTestData(groups ...*server.Group) *Groups {
Id: claims.AccountId,
Domain: "hotmail.com",
Peers: TestPeers,
Users: map[string]*server.User{
user.Id: user,
},
Groups: map[string]*server.Group{
"id-existed": {ID: "id-existed", Peers: []string{"A", "B"}},
"id-all": {ID: "id-all", Name: "All"}},
@@ -120,7 +123,8 @@ func TestGetGroup(t *testing.T) {
Name: "Group",
}
p := initGroupTestData(group)
adminUser := server.NewAdminUser("test_user")
p := initGroupTestData(adminUser, group)
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
@@ -270,7 +274,8 @@ func TestWriteGroup(t *testing.T) {
},
}
p := initGroupTestData()
adminUser := server.NewAdminUser("test_user")
p := initGroupTestData(adminUser)
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {

View File

@@ -4,12 +4,14 @@ import (
"github.com/gorilla/mux"
s "github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/http/middleware"
"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/rs/cors"
"net/http"
)
// APIHandler creates the Management service HTTP API handler registering all the available endpoints.
func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience string, authKeysLocation string) (http.Handler, error) {
func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience string, authKeysLocation string,
appMetrics telemetry.AppMetrics) (http.Handler, error) {
jwtMiddleware, err := middleware.NewJwtMiddleware(
authIssuer,
authAudience,
@@ -21,12 +23,15 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
corsMiddleware := cors.AllowAll()
acMiddleware := middleware.NewAccessControll(
acMiddleware := middleware.NewAccessControl(
authAudience,
accountManager.IsUserAdmin)
apiHandler := mux.NewRouter()
apiHandler.Use(corsMiddleware.Handler, jwtMiddleware.Handler, acMiddleware.Handler)
rootRouter := mux.NewRouter()
metricsMiddleware := appMetrics.HTTPMiddleware()
apiHandler := rootRouter.PathPrefix("/api").Subrouter()
apiHandler.Use(metricsMiddleware.Handler, corsMiddleware.Handler, jwtMiddleware.Handler, acMiddleware.Handler)
groupsHandler := NewGroups(accountManager, authAudience)
rulesHandler := NewRules(accountManager, authAudience)
@@ -36,46 +41,67 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
routesHandler := NewRoutes(accountManager, authAudience)
nameserversHandler := NewNameservers(accountManager, authAudience)
apiHandler.HandleFunc("/api/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).
apiHandler.HandleFunc("/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/peers/{id}", peersHandler.HandlePeer).
Methods("GET", "PUT", "DELETE", "OPTIONS")
apiHandler.HandleFunc("/api/users", userHandler.GetUsers).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/users/{id}", userHandler.UpdateUser).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/api/users", userHandler.CreateUserHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/users", userHandler.GetUsers).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/users/{id}", userHandler.UpdateUser).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/users", userHandler.CreateUserHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetAllSetupKeysHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys", keysHandler.CreateSetupKeyHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.GetSetupKeyHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.UpdateSetupKeyHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/setup-keys", keysHandler.GetAllSetupKeysHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/setup-keys", keysHandler.CreateSetupKeyHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/setup-keys/{id}", keysHandler.GetSetupKeyHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/setup-keys/{id}", keysHandler.UpdateSetupKeyHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/api/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/rules", rulesHandler.CreateRuleHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/api/rules/{id}", rulesHandler.UpdateRuleHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/api/rules/{id}", rulesHandler.PatchRuleHandler).Methods("PATCH", "OPTIONS")
apiHandler.HandleFunc("/api/rules/{id}", rulesHandler.GetRuleHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/rules/{id}", rulesHandler.DeleteRuleHandler).Methods("DELETE", "OPTIONS")
apiHandler.HandleFunc("/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/rules", rulesHandler.CreateRuleHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/rules/{id}", rulesHandler.UpdateRuleHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/rules/{id}", rulesHandler.PatchRuleHandler).Methods("PATCH", "OPTIONS")
apiHandler.HandleFunc("/rules/{id}", rulesHandler.GetRuleHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/rules/{id}", rulesHandler.DeleteRuleHandler).Methods("DELETE", "OPTIONS")
apiHandler.HandleFunc("/api/groups", groupsHandler.GetAllGroupsHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/groups", groupsHandler.CreateGroupHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.UpdateGroupHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.PatchGroupHandler).Methods("PATCH", "OPTIONS")
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.GetGroupHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/groups/{id}", groupsHandler.DeleteGroupHandler).Methods("DELETE", "OPTIONS")
apiHandler.HandleFunc("/groups", groupsHandler.GetAllGroupsHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/groups", groupsHandler.CreateGroupHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/groups/{id}", groupsHandler.UpdateGroupHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/groups/{id}", groupsHandler.PatchGroupHandler).Methods("PATCH", "OPTIONS")
apiHandler.HandleFunc("/groups/{id}", groupsHandler.GetGroupHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/groups/{id}", groupsHandler.DeleteGroupHandler).Methods("DELETE", "OPTIONS")
apiHandler.HandleFunc("/api/routes", routesHandler.GetAllRoutesHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/routes", routesHandler.CreateRouteHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.UpdateRouteHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.PatchRouteHandler).Methods("PATCH", "OPTIONS")
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.GetRouteHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.DeleteRouteHandler).Methods("DELETE", "OPTIONS")
apiHandler.HandleFunc("/routes", routesHandler.GetAllRoutesHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/routes", routesHandler.CreateRouteHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/routes/{id}", routesHandler.UpdateRouteHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/routes/{id}", routesHandler.PatchRouteHandler).Methods("PATCH", "OPTIONS")
apiHandler.HandleFunc("/routes/{id}", routesHandler.GetRouteHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/routes/{id}", routesHandler.DeleteRouteHandler).Methods("DELETE", "OPTIONS")
apiHandler.HandleFunc("/api/dns/nameservers", nameserversHandler.GetAllNameserversHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/dns/nameservers", nameserversHandler.CreateNameserverGroupHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/api/dns/nameservers/{id}", nameserversHandler.UpdateNameserverGroupHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/api/dns/nameservers/{id}", nameserversHandler.PatchNameserverGroupHandler).Methods("PATCH", "OPTIONS")
apiHandler.HandleFunc("/api/dns/nameservers/{id}", nameserversHandler.GetNameserverGroupHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/api/dns/nameservers/{id}", nameserversHandler.DeleteNameserverGroupHandler).Methods("DELETE", "OPTIONS")
apiHandler.HandleFunc("/dns/nameservers", nameserversHandler.GetAllNameserversHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/dns/nameservers", nameserversHandler.CreateNameserverGroupHandler).Methods("POST", "OPTIONS")
apiHandler.HandleFunc("/dns/nameservers/{id}", nameserversHandler.UpdateNameserverGroupHandler).Methods("PUT", "OPTIONS")
apiHandler.HandleFunc("/dns/nameservers/{id}", nameserversHandler.PatchNameserverGroupHandler).Methods("PATCH", "OPTIONS")
apiHandler.HandleFunc("/dns/nameservers/{id}", nameserversHandler.GetNameserverGroupHandler).Methods("GET", "OPTIONS")
apiHandler.HandleFunc("/dns/nameservers/{id}", nameserversHandler.DeleteNameserverGroupHandler).Methods("DELETE", "OPTIONS")
return apiHandler, nil
err = apiHandler.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
methods, err := route.GetMethods()
if err != nil {
return err
}
for _, method := range methods {
template, err := route.GetPathTemplate()
if err != nil {
return err
}
err = metricsMiddleware.AddHTTPRequestResponseCounter(template, method)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
return rootRouter, nil
}

View File

@@ -9,24 +9,25 @@ import (
type IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error)
// AccessControll middleware to restrict to make POST/PUT/DELETE requests by admin only
type AccessControll struct {
// AccessControl middleware to restrict to make POST/PUT/DELETE requests by admin only
type AccessControl struct {
jwtExtractor jwtclaims.ClaimsExtractor
isUserAdmin IsUserAdminFunc
audience string
}
// NewAccessControll instance constructor
func NewAccessControll(audience string, isUserAdmin IsUserAdminFunc) *AccessControll {
return &AccessControll{
// NewAccessControl instance constructor
func NewAccessControl(audience string, isUserAdmin IsUserAdminFunc) *AccessControl {
return &AccessControl{
isUserAdmin: isUserAdmin,
audience: audience,
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
}
}
// Handler method of the middleware which forbinneds all modify requests for non admin users
func (a *AccessControll) Handler(h http.Handler) http.Handler {
// 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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jwtClaims := a.jwtExtractor.ExtractClaimsFromRequestContext(r, a.audience)
@@ -34,7 +35,6 @@ func (a *AccessControll) Handler(h http.Handler) http.Handler {
if err != nil {
http.Error(w, fmt.Sprintf("error get user from JWT: %v", err), http.StatusUnauthorized)
return
}
if !ok {

View File

@@ -186,7 +186,7 @@ func (m *JWTMiddleware) CheckJWTFromRequest(w http.ResponseWriter, r *http.Reque
validatedToken, err := m.ValidateAndParse(token)
if err != nil {
m.Options.ErrorHandler(w, r, "The token isn't valid")
m.Options.ErrorHandler(w, r, err.Error())
return err
}

View File

@@ -30,7 +30,7 @@ func NewNameservers(accountManager server.AccountManager, authAudience string) *
// GetAllNameserversHandler returns the list of nameserver groups for the account
func (h *Nameservers) GetAllNameserversHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -53,7 +53,7 @@ func (h *Nameservers) GetAllNameserversHandler(w http.ResponseWriter, r *http.Re
// CreateNameserverGroupHandler handles nameserver group creation request
func (h *Nameservers) CreateNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -71,7 +71,7 @@ func (h *Nameservers) CreateNameserverGroupHandler(w http.ResponseWriter, r *htt
return
}
nsGroup, err := h.accountManager.CreateNameServerGroup(account.Id, req.Name, req.Description, nsList, req.Groups, req.Enabled)
nsGroup, err := h.accountManager.CreateNameServerGroup(account.Id, req.Name, req.Description, nsList, req.Groups, req.Primary, req.Domains, req.Enabled)
if err != nil {
toHTTPError(err, w)
return
@@ -84,7 +84,7 @@ func (h *Nameservers) CreateNameserverGroupHandler(w http.ResponseWriter, r *htt
// UpdateNameserverGroupHandler handles update to a nameserver group identified by a given ID
func (h *Nameservers) UpdateNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -131,7 +131,7 @@ func (h *Nameservers) UpdateNameserverGroupHandler(w http.ResponseWriter, r *htt
// PatchNameserverGroupHandler handles patch updates to a nameserver group identified by a given ID
func (h *Nameservers) PatchNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -202,7 +202,7 @@ func (h *Nameservers) PatchNameserverGroupHandler(w http.ResponseWriter, r *http
// DeleteNameserverGroupHandler handles nameserver group deletion request
func (h *Nameservers) DeleteNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -226,7 +226,7 @@ func (h *Nameservers) DeleteNameserverGroupHandler(w http.ResponseWriter, r *htt
// GetNameserverGroupHandler handles a nameserver group Get request identified by ID
func (h *Nameservers) GetNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)

View File

@@ -29,12 +29,16 @@ const (
var testingNSAccount = &server.Account{
Id: testNSGroupAccountID,
Domain: "hotmail.com",
Users: map[string]*server.User{
"test_user": server.NewAdminUser("test_user"),
},
}
var baseExistingNSGroup = &nbdns.NameServerGroup{
ID: existingNSGroupID,
Name: "super",
Description: "super",
Primary: true,
NameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
@@ -60,7 +64,7 @@ func initNameserversTestData() *Nameservers {
}
return nil, status.Errorf(codes.NotFound, "nameserver group with ID %s not found", nsGroupID)
},
CreateNameServerGroupFunc: func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error) {
CreateNameServerGroupFunc: func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool) (*nbdns.NameServerGroup, error) {
return &nbdns.NameServerGroup{
ID: existingNSGroupID,
Name: name,
@@ -68,6 +72,8 @@ func initNameserversTestData() *Nameservers {
NameServers: nameServerList,
Groups: groups,
Enabled: enabled,
Primary: primary,
Domains: domains,
}, nil
},
DeleteNameServerGroupFunc: func(accountID, nsGroupID string) error {
@@ -150,7 +156,7 @@ func TestNameserversHandlers(t *testing.T) {
requestType: http.MethodPost,
requestPath: "/api/dns/nameservers",
requestBody: bytes.NewBuffer(
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1.1.1.1\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1.1.1.1\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true,\"primary\":true}")),
expectedStatus: http.StatusOK,
expectedBody: true,
expectedNSGroup: &api.NameserverGroup{
@@ -173,7 +179,7 @@ func TestNameserversHandlers(t *testing.T) {
requestType: http.MethodPost,
requestPath: "/api/dns/nameservers",
requestBody: bytes.NewBuffer(
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1000\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1000\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true,\"primary\":true}")),
expectedStatus: http.StatusBadRequest,
expectedBody: false,
},
@@ -182,7 +188,7 @@ func TestNameserversHandlers(t *testing.T) {
requestType: http.MethodPut,
requestPath: "/api/dns/nameservers/" + existingNSGroupID,
requestBody: bytes.NewBuffer(
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1.1.1.1\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1.1.1.1\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true,\"primary\":true}")),
expectedStatus: http.StatusOK,
expectedBody: true,
expectedNSGroup: &api.NameserverGroup{
@@ -205,7 +211,7 @@ func TestNameserversHandlers(t *testing.T) {
requestType: http.MethodPut,
requestPath: "/api/dns/nameservers/" + notFoundNSGroupID,
requestBody: bytes.NewBuffer(
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1.1.1.1\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1.1.1.1\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true,\"primary\":true}")),
expectedStatus: http.StatusNotFound,
expectedBody: false,
},
@@ -214,7 +220,7 @@ func TestNameserversHandlers(t *testing.T) {
requestType: http.MethodPut,
requestPath: "/api/dns/nameservers/" + notFoundNSGroupID,
requestBody: bytes.NewBuffer(
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"100\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"100\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true,\"primary\":true}")),
expectedStatus: http.StatusBadRequest,
expectedBody: false,
},

View File

@@ -56,7 +56,7 @@ func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseW
}
func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -95,15 +95,20 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) {
func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
peers, err := h.accountManager.GetPeers(account.Id, user.Id)
if err != nil {
return
}
respBody := []*api.Peer{}
for _, peer := range account.Peers {
for _, peer := range peers {
respBody = append(respBody, toPeerResponse(peer, account))
}
writeJSONObject(w, respBody)

View File

@@ -16,15 +16,21 @@ import (
"github.com/netbirdio/netbird/management/server/mock_server"
)
func initTestMetaData(peer ...*server.Peer) *Peers {
func initTestMetaData(peers ...*server.Peer) *Peers {
return &Peers{
accountManager: &mock_server.MockAccountManager{
GetPeersFunc: func(accountID, userID string) ([]*server.Peer, error) {
return peers, nil
},
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
return &server.Account{
Id: claims.AccountId,
Domain: "hotmail.com",
Peers: map[string]*server.Peer{
"test_peer": peer[0],
"test_peer": peers[0],
},
Users: map[string]*server.User{
"test_user": server.NewAdminUser("test_user"),
},
}, nil
},

View File

@@ -33,16 +33,24 @@ func NewRoutes(accountManager server.AccountManager, authAudience string) *Route
// GetAllRoutesHandler returns the list of routes for the account
func (h *Routes) GetAllRoutesHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
routes, err := h.accountManager.ListRoutes(account.Id)
routes, err := h.accountManager.ListRoutes(account.Id, user.Id)
if err != nil {
log.Error(err)
if e, ok := server.FromError(err); ok {
switch e.Type() {
case server.PermissionDenied:
http.Error(w, e.Error(), http.StatusForbidden)
return
default:
}
}
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
@@ -56,7 +64,7 @@ func (h *Routes) GetAllRoutesHandler(w http.ResponseWriter, r *http.Request) {
// CreateRouteHandler handles route creation request
func (h *Routes) CreateRouteHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -104,7 +112,7 @@ func (h *Routes) CreateRouteHandler(w http.ResponseWriter, r *http.Request) {
// UpdateRouteHandler handles update to a route identified by a given ID
func (h *Routes) UpdateRouteHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -117,7 +125,7 @@ func (h *Routes) UpdateRouteHandler(w http.ResponseWriter, r *http.Request) {
return
}
_, err = h.accountManager.GetRoute(account.Id, routeID)
_, err = h.accountManager.GetRoute(account.Id, routeID, user.Id)
if err != nil {
http.Error(w, fmt.Sprintf("couldn't find route for ID %s", routeID), http.StatusNotFound)
return
@@ -177,7 +185,7 @@ func (h *Routes) UpdateRouteHandler(w http.ResponseWriter, r *http.Request) {
// PatchRouteHandler handles patch updates to a route identified by a given ID
func (h *Routes) PatchRouteHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -190,7 +198,7 @@ func (h *Routes) PatchRouteHandler(w http.ResponseWriter, r *http.Request) {
return
}
_, err = h.accountManager.GetRoute(account.Id, routeID)
_, err = h.accountManager.GetRoute(account.Id, routeID, user.Id)
if err != nil {
log.Error(err)
http.Error(w, fmt.Sprintf("couldn't find route ID %s", routeID), http.StatusNotFound)
@@ -334,7 +342,7 @@ func (h *Routes) PatchRouteHandler(w http.ResponseWriter, r *http.Request) {
// DeleteRouteHandler handles route deletion request
func (h *Routes) DeleteRouteHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -363,7 +371,7 @@ func (h *Routes) DeleteRouteHandler(w http.ResponseWriter, r *http.Request) {
// GetRouteHandler handles a route Get request identified by ID
func (h *Routes) GetRouteHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -375,7 +383,7 @@ func (h *Routes) GetRouteHandler(w http.ResponseWriter, r *http.Request) {
return
}
foundRoute, err := h.accountManager.GetRoute(account.Id, routeID)
foundRoute, err := h.accountManager.GetRoute(account.Id, routeID, user.Id)
if err != nil {
http.Error(w, "route not found", http.StatusNotFound)
return

View File

@@ -51,12 +51,15 @@ var testingAccount = &server.Account{
IP: netip.MustParseAddr(existingPeerID).AsSlice(),
},
},
Users: map[string]*server.User{
"test_user": server.NewAdminUser("test_user"),
},
}
func initRoutesTestData() *Routes {
return &Routes{
accountManager: &mock_server.MockAccountManager{
GetRouteFunc: func(_, routeID string) (*route.Route, error) {
GetRouteFunc: func(_, routeID, _ string) (*route.Route, error) {
if routeID == existingRouteID {
return baseExistingRoute, nil
}

View File

@@ -31,15 +31,29 @@ func NewRules(accountManager server.AccountManager, authAudience string) *Rules
// GetAllRulesHandler list for the account
func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
accountRules, err := h.accountManager.ListRules(account.Id, user.Id)
if err != nil {
log.Error(err)
if e, ok := server.FromError(err); ok {
switch e.Type() {
case server.PermissionDenied:
http.Error(w, e.Error(), http.StatusForbidden)
return
default:
}
}
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
rules := []*api.Rule{}
for _, r := range account.Rules {
for _, r := range accountRules {
rules = append(rules, toRuleResponse(account, r))
}
@@ -48,7 +62,7 @@ func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) {
// UpdateRuleHandler handles update to a rule identified by a given ID
func (h *Rules) UpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -118,7 +132,7 @@ func (h *Rules) UpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
// PatchRuleHandler handles patch updates to a rule identified by a given ID
func (h *Rules) PatchRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -275,7 +289,7 @@ func (h *Rules) PatchRuleHandler(w http.ResponseWriter, r *http.Request) {
// CreateRuleHandler handles rule creation request
func (h *Rules) CreateRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -332,7 +346,7 @@ func (h *Rules) CreateRuleHandler(w http.ResponseWriter, r *http.Request) {
// DeleteRuleHandler handles rule deletion request
func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -356,7 +370,7 @@ func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
// GetRuleHandler handles a group Get request identified by ID
func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
@@ -370,7 +384,7 @@ func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) {
return
}
rule, err := h.accountManager.GetRule(account.Id, ruleID)
rule, err := h.accountManager.GetRule(account.Id, ruleID, user.Id)
if err != nil {
http.Error(w, "rule not found", http.StatusNotFound)
return

View File

@@ -28,7 +28,7 @@ func initRulesTestData(rules ...*server.Rule) *Rules {
}
return nil
},
GetRuleFunc: func(_, ruleID string) (*server.Rule, error) {
GetRuleFunc: func(_, ruleID, _ string) (*server.Rule, error) {
if ruleID != "idoftherule" {
return nil, fmt.Errorf("not found")
}
@@ -75,6 +75,9 @@ func initRulesTestData(rules ...*server.Rule) *Rules {
"F": {ID: "F"},
"G": {ID: "G"},
},
Users: map[string]*server.User{
"test_user": server.NewAdminUser("test_user"),
},
}, nil
},
},

View File

@@ -31,7 +31,7 @@ func NewSetupKeysHandler(accountManager server.AccountManager, authAudience stri
// CreateSetupKeyHandler is a POST requests that creates a new SetupKey
func (h *SetupKeys) CreateSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -81,7 +81,7 @@ func (h *SetupKeys) CreateSetupKeyHandler(w http.ResponseWriter, r *http.Request
// GetSetupKeyHandler is a GET request to get a SetupKey by ID
func (h *SetupKeys) GetSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -95,7 +95,7 @@ func (h *SetupKeys) GetSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
return
}
key, err := h.accountManager.GetSetupKey(account.Id, keyID)
key, err := h.accountManager.GetSetupKey(account.Id, user.Id, keyID)
if err != nil {
errStatus, ok := status.FromError(err)
if ok && errStatus.Code() == codes.NotFound {
@@ -112,7 +112,7 @@ func (h *SetupKeys) GetSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
// UpdateSetupKeyHandler is a PUT request to update server.SetupKey
func (h *SetupKeys) UpdateSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -168,14 +168,14 @@ func (h *SetupKeys) UpdateSetupKeyHandler(w http.ResponseWriter, r *http.Request
// GetAllSetupKeysHandler is a GET request that returns a list of SetupKey
func (h *SetupKeys) GetAllSetupKeysHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
setupKeys, err := h.accountManager.ListSetupKeys(account.Id)
setupKeys, err := h.accountManager.ListSetupKeys(account.Id, user.Id)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)

View File

@@ -28,13 +28,17 @@ const (
notFoundSetupKeyID = "notFoundSetupKeyID"
)
func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.SetupKey, updatedSetupKey *server.SetupKey) *SetupKeys {
func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.SetupKey, updatedSetupKey *server.SetupKey,
user *server.User) *SetupKeys {
return &SetupKeys{
accountManager: &mock_server.MockAccountManager{
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
return &server.Account{
Id: testAccountID,
Domain: "hotmail.com",
Users: map[string]*server.User{
user.Id: user,
},
SetupKeys: map[string]*server.SetupKey{
defaultKey.Key: defaultKey,
},
@@ -49,7 +53,7 @@ func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.Setup
}
return nil, fmt.Errorf("failed creating setup key")
},
GetSetupKeyFunc: func(accountID string, keyID string) (*server.SetupKey, error) {
GetSetupKeyFunc: func(accountID, userID, keyID string) (*server.SetupKey, error) {
switch keyID {
case defaultKey.Id:
return defaultKey, nil
@@ -67,7 +71,7 @@ func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.Setup
return nil, status.Errorf(codes.NotFound, "key %s not found", key.Id)
},
ListSetupKeysFunc: func(accountID string) ([]*server.SetupKey, error) {
ListSetupKeysFunc: func(accountID, userID string) ([]*server.SetupKey, error) {
return []*server.SetupKey{defaultKey}, nil
},
},
@@ -75,7 +79,7 @@ func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.Setup
jwtExtractor: jwtclaims.ClaimsExtractor{
ExtractClaimsFromRequestContext: func(r *http.Request, authAudience string) jwtclaims.AuthorizationClaims {
return jwtclaims.AuthorizationClaims{
UserId: "test_user",
UserId: user.Id,
Domain: "hotmail.com",
AccountId: testAccountID,
}
@@ -88,6 +92,8 @@ func TestSetupKeysHandlers(t *testing.T) {
defaultSetupKey := server.GenerateDefaultSetupKey()
defaultSetupKey.Id = existingSetupKeyID
adminUser := server.NewAdminUser("test_user")
newSetupKey := server.GenerateSetupKey(newSetupKeyName, server.SetupKeyReusable, 0, []string{"group-1"})
updatedDefaultSetupKey := defaultSetupKey.Copy()
updatedDefaultSetupKey.AutoGroups = []string{"group-1"}
@@ -153,7 +159,7 @@ func TestSetupKeysHandlers(t *testing.T) {
},
}
handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey)
handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey, adminUser)
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {

View File

@@ -34,7 +34,7 @@ func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", http.StatusBadRequest)
}
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
@@ -87,7 +87,7 @@ func (h *UserHandler) CreateUserHandler(w http.ResponseWriter, r *http.Request)
http.Error(w, "", http.StatusNotFound)
}
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, _, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
}
@@ -132,12 +132,13 @@ func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", http.StatusBadRequest)
}
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
account, user, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
data, err := h.accountManager.GetUsersFromAccount(account.Id)
data, err := h.accountManager.GetUsersFromAccount(account.Id, user.Id)
if err != nil {
log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError)

View File

@@ -27,7 +27,7 @@ func initUsers(user ...*server.User) *UserHandler {
Users: users,
}, nil
},
GetUsersFromAccountFunc: func(accountID string) ([]*server.UserInfo, error) {
GetUsersFromAccountFunc: func(accountID, userID string) ([]*server.UserInfo, error) {
users := make([]*server.UserInfo, 0)
for _, v := range user {
users = append(users, &server.UserInfo{
@@ -44,7 +44,7 @@ func initUsers(user ...*server.User) *UserHandler {
jwtExtractor: jwtclaims.ClaimsExtractor{
ExtractClaimsFromRequestContext: func(r *http.Request, authAudiance string) jwtclaims.AuthorizationClaims {
return jwtclaims.AuthorizationClaims{
UserId: "test_user",
UserId: "1",
Domain: "hotmail.com",
AccountId: "test_id",
}

View File

@@ -56,16 +56,22 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
func getJWTAccount(accountManager server.AccountManager,
jwtExtractor jwtclaims.ClaimsExtractor,
authAudience string, r *http.Request) (*server.Account, error) {
authAudience string, r *http.Request) (*server.Account, *server.User, error) {
jwtClaims := jwtExtractor.ExtractClaimsFromRequestContext(r, authAudience)
claims := jwtExtractor.ExtractClaimsFromRequestContext(r, authAudience)
account, err := accountManager.GetAccountFromToken(jwtClaims)
account, err := accountManager.GetAccountFromToken(claims)
if err != nil {
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
return nil, nil, fmt.Errorf("failed getting account of a user %s: %v", claims.UserId, err)
}
return account, nil
user := account.Users[claims.UserId]
if user == nil {
// this is not really possible because we got an account by user ID
return nil, nil, fmt.Errorf("user %s not found", claims.UserId)
}
return account, user, nil
}
func toHTTPError(err error, w http.ResponseWriter) {

View File

@@ -6,6 +6,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/netbirdio/netbird/management/server/telemetry"
"io"
"net/http"
"net/url"
@@ -24,6 +25,7 @@ type Auth0Manager struct {
httpClient ManagerHTTPClient
credentials ManagerCredentials
helper ManagerHelper
appMetrics telemetry.AppMetrics
}
// Auth0ClientConfig auth0 manager client configurations
@@ -51,6 +53,7 @@ type Auth0Credentials struct {
httpClient ManagerHTTPClient
jwtToken JWTToken
mux sync.Mutex
appMetrics telemetry.AppMetrics
}
// createUserRequest is a user create request
@@ -106,7 +109,7 @@ type auth0Profile struct {
}
// NewAuth0Manager creates a new instance of the Auth0Manager
func NewAuth0Manager(config Auth0ClientConfig) (*Auth0Manager, error) {
func NewAuth0Manager(config Auth0ClientConfig, appMetrics telemetry.AppMetrics) (*Auth0Manager, error) {
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
httpTransport.MaxIdleConns = 5
@@ -134,12 +137,15 @@ func NewAuth0Manager(config Auth0ClientConfig) (*Auth0Manager, error) {
clientConfig: config,
httpClient: httpClient,
helper: helper,
appMetrics: appMetrics,
}
return &Auth0Manager{
authIssuer: config.AuthIssuer,
credentials: credentials,
httpClient: httpClient,
helper: helper,
appMetrics: appMetrics,
}, nil
}
@@ -170,6 +176,9 @@ func (c *Auth0Credentials) requestJWTToken() (*http.Response, error) {
res, err = c.httpClient.Do(req)
if err != nil {
if c.appMetrics != nil {
c.appMetrics.IDPMetrics().CountRequestError()
}
return res, err
}
@@ -214,6 +223,10 @@ func (c *Auth0Credentials) Authenticate() (JWTToken, error) {
c.mux.Lock()
defer c.mux.Unlock()
if c.appMetrics != nil {
c.appMetrics.IDPMetrics().CountAuthenticate()
}
// If jwtToken has an expires time and we have enough time to do a request return immediately
if c.jwtStillValid() {
return c.jwtToken, nil
@@ -287,9 +300,16 @@ func (am *Auth0Manager) GetAccount(accountID string) ([]*UserData, error) {
res, err := am.httpClient.Do(req)
if err != nil {
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestError()
}
return nil, err
}
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountGetAccount()
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
@@ -342,9 +362,16 @@ func (am *Auth0Manager) GetUserDataByID(userID string, appMetadata AppMetadata)
res, err := am.httpClient.Do(req)
if err != nil {
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestError()
}
return nil, err
}
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountGetUserDataByID()
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
@@ -398,9 +425,16 @@ func (am *Auth0Manager) UpdateUserAppMetadata(userID string, appMetadata AppMeta
res, err := am.httpClient.Do(req)
if err != nil {
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestError()
}
return err
}
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
}
defer func() {
err = res.Body.Close()
if err != nil {
@@ -416,12 +450,13 @@ func (am *Auth0Manager) UpdateUserAppMetadata(userID string, appMetadata AppMeta
}
func buildCreateUserRequestPayload(email string, name string, accountID string) (string, error) {
invite := true
req := &createUserRequest{
Email: email,
Name: name,
AppMeta: AppMetadata{
WTAccountID: accountID,
WTPendingInvite: true,
WTPendingInvite: &invite,
},
Connection: "Username-Password-Authentication",
Password: GeneratePassword(8, 1, 1, 1),
@@ -502,6 +537,9 @@ func (am *Auth0Manager) GetAllAccounts() (map[string][]*UserData, error) {
jobResp, err := am.httpClient.Do(exportJobReq)
if err != nil {
log.Debugf("Couldn't get job response %v", err)
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestError()
}
return nil, err
}
@@ -512,6 +550,9 @@ func (am *Auth0Manager) GetAllAccounts() (map[string][]*UserData, error) {
}
}()
if jobResp.StatusCode != 200 {
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to update the appMetadata, statusCode %d", jobResp.StatusCode)
}
@@ -530,6 +571,9 @@ func (am *Auth0Manager) GetAllAccounts() (map[string][]*UserData, error) {
}
if exportJobResp.ID == "" {
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("couldn't get an batch id status %d, %s, response body: %v", jobResp.StatusCode, jobResp.Status, exportJobResp)
}
@@ -556,12 +600,16 @@ func (am *Auth0Manager) GetUserByEmail(email string) ([]*UserData, error) {
if err != nil {
return nil, err
}
reqURL := am.authIssuer + "/api/v2/users-by-email?email=" + email
reqURL := am.authIssuer + "/api/v2/users-by-email?email=" + url.QueryEscape(email)
body, err := doGetReq(am.httpClient, reqURL, jwtToken.AccessToken)
if err != nil {
return nil, err
}
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountGetUserByEmail()
}
userResp := []*UserData{}
err = am.helper.Unmarshal(body, &userResp)
@@ -585,9 +633,16 @@ func (am *Auth0Manager) CreateUser(email string, name string, accountID string)
return nil, err
}
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountCreateUser()
}
resp, err := am.httpClient.Do(req)
if err != nil {
log.Debugf("Couldn't get job response %v", err)
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestError()
}
return nil, err
}
@@ -598,6 +653,9 @@ func (am *Auth0Manager) CreateUser(email string, name string, accountID string)
}
}()
if !(resp.StatusCode == 200 || resp.StatusCode == 201) {
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to create user, statusCode %d", resp.StatusCode)
}
@@ -698,7 +756,7 @@ func (am *Auth0Manager) downloadProfileExport(location string) (map[string][]*Us
Email: profile.Email,
AppMetadata: AppMetadata{
WTAccountID: profile.AccountID,
WTPendingInvite: profile.PendingInvite,
WTPendingInvite: &profile.PendingInvite,
},
})
}
@@ -729,13 +787,12 @@ func doGetReq(client ManagerHTTPClient, url, accessToken string) ([]byte, error)
log.Errorf("error while closing body for url %s: %v", url, err)
}
}()
if res.StatusCode != 200 {
return nil, fmt.Errorf("unable to get %s, statusCode %d", url, res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("unable to get %s, statusCode %d", url, res.StatusCode)
}
return body, nil
}

View File

@@ -3,6 +3,7 @@ package idp
import (
"encoding/json"
"fmt"
"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/stretchr/testify/require"
"io"
"net/http"
@@ -340,7 +341,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
name: "Bad Status Code",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":false}}", appMetadata.WTAccountID),
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null}}", appMetadata.WTAccountID),
appMetadata: appMetadata,
statusCode: 400,
helper: JsonParser{},
@@ -363,7 +364,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
name: "Good request",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":false}}", appMetadata.WTAccountID),
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null}}", appMetadata.WTAccountID),
appMetadata: appMetadata,
statusCode: 200,
helper: JsonParser{},
@@ -371,7 +372,23 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
assertErrFuncMessage: "shouldn't return error",
}
for _, testCase := range []updateUserAppMetadataTest{updateUserAppMetadataTestCase1, updateUserAppMetadataTestCase2, updateUserAppMetadataTestCase3, updateUserAppMetadataTestCase4} {
invite := true
updateUserAppMetadataTestCase5 := updateUserAppMetadataTest{
name: "Update Pending Invite",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":true}}", appMetadata.WTAccountID),
appMetadata: AppMetadata{
WTAccountID: "ok",
WTPendingInvite: &invite,
},
statusCode: 200,
helper: JsonParser{},
assertErrFunc: assert.NoError,
assertErrFuncMessage: "shouldn't return error",
}
for _, testCase := range []updateUserAppMetadataTest{updateUserAppMetadataTestCase1, updateUserAppMetadataTestCase2,
updateUserAppMetadataTestCase3, updateUserAppMetadataTestCase4, updateUserAppMetadataTestCase5} {
t.Run(testCase.name, func(t *testing.T) {
jwtReqClient := mockHTTPClient{
resBody: testCase.inputReqBody,
@@ -459,7 +476,7 @@ func TestNewAuth0Manager(t *testing.T) {
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4} {
t.Run(testCase.name, func(t *testing.T) {
_, err := NewAuth0Manager(testCase.inputConfig)
_, err := NewAuth0Manager(testCase.inputConfig, &telemetry.MockAppMetrics{})
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
})
}

View File

@@ -2,6 +2,7 @@ package idp
import (
"fmt"
"github.com/netbirdio/netbird/management/server/telemetry"
"net/http"
"strings"
"time"
@@ -51,7 +52,7 @@ type AppMetadata struct {
// WTAccountID is a NetBird (previously Wiretrustee) account id to update in the IDP
// maps to wt_account_id when json.marshal
WTAccountID string `json:"wt_account_id,omitempty"`
WTPendingInvite bool `json:"wt_pending_invite"`
WTPendingInvite *bool `json:"wt_pending_invite"`
}
// JWTToken a JWT object that holds information of a token
@@ -64,12 +65,12 @@ type JWTToken struct {
}
// NewManager returns a new idp manager based on the configuration that it receives
func NewManager(config Config) (Manager, error) {
func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error) {
switch strings.ToLower(config.ManagerType) {
case "none", "":
return nil, nil
case "auth0":
return NewAuth0Manager(config.Auth0ClientCredentials)
return NewAuth0Manager(config.Auth0ClientCredentials, appMetrics)
default:
return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
}

View File

@@ -403,12 +403,12 @@ func startManagement(t *testing.T, port int, config *Config) (*grpc.Server, erro
return nil, err
}
peersUpdateManager := NewPeersUpdateManager()
accountManager, err := BuildManager(store, peersUpdateManager, nil)
accountManager, err := BuildManager(store, peersUpdateManager, nil, "")
if err != nil {
return nil, err
}
turnManager := NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := NewServer(config, accountManager, peersUpdateManager, turnManager)
mgmtServer, err := NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
if err != nil {
return nil, err
}

View File

@@ -493,12 +493,12 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
peersUpdateManager := server.NewPeersUpdateManager()
accountManager, err := server.BuildManager(store, peersUpdateManager, nil)
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "")
if err != nil {
log.Fatalf("failed creating a manager: %v", err)
}
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
Expect(err).NotTo(HaveOccurred())
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
go func() {

View File

@@ -17,7 +17,7 @@ import (
const (
// PayloadEvent identifies an event type
PayloadEvent = "self-hosted stats"
// payloadEndpoint metrics endpoint to send anonymous data
// payloadEndpoint metrics defaultEndpoint to send anonymous data
payloadEndpoint = "https://metrics.netbird.io"
// defaultPushInterval default interval to push metrics
defaultPushInterval = 24 * time.Hour
@@ -188,13 +188,17 @@ func (w *Worker) generateProperties() properties {
userPeers++
}
_, connected := connections[peer.Key]
if connected || peer.Status.LastSeen.After(w.lastRun) {
activePeersLastDay++
}
osKey := strings.ToLower(fmt.Sprintf("peer_os_%s", peer.Meta.GoOS))
osCount := osPeers[osKey]
osPeers[osKey] = osCount + 1
_, connected := connections[peer.Key]
if connected || peer.Status.LastSeen.After(w.lastRun) {
activePeersLastDay++
osActiveKey := osKey + "_active"
osActiveCount := osPeers[osActiveKey]
osPeers[osActiveKey] = osActiveCount + 1
}
}
}
@@ -279,5 +283,4 @@ func createPostRequest(ctx context.Context, endpoint string, payloadStr string)
req.Header.Add("content-type", "application/json")
return req, nil
}

View File

@@ -1,13 +0,0 @@
## Migration from Store v2 to Store v2
Previously Account.Id was an Auth0 user id.
Conversion moves user id to Account.CreatedBy and generates a new Account.Id using xid.
It also adds a User with id = old Account.Id with a role Admin.
To start a conversion simply run the command below providing your current Wiretrustee Management datadir (where store.json file is located)
and a new data directory location (where a converted store.js will be stored):
```shell
./migration --oldDir /var/wiretrustee/datadir --newDir /var/wiretrustee/newdatadir/
```
Afterwards you can run the Management service providing ```/var/wiretrustee/newdatadir/ ``` as a datadir.

View File

@@ -1,56 +0,0 @@
package main
import (
"flag"
"fmt"
"github.com/netbirdio/netbird/management/server"
"github.com/rs/xid"
)
func main() {
oldDir := flag.String("oldDir", "old store directory", "/var/wiretrustee/datadir")
newDir := flag.String("newDir", "new store directory", "/var/wiretrustee/newdatadir")
flag.Parse()
oldStore, err := server.NewStore(*oldDir)
if err != nil {
panic(err)
}
newStore, err := server.NewStore(*newDir)
if err != nil {
panic(err)
}
err = Convert(oldStore, newStore)
if err != nil {
panic(err)
}
fmt.Println("successfully converted")
}
// Convert converts old store ato a new store
// Previously Account.Id was an Auth0 user id
// Conversion moved user id to Account.CreatedBy and generated a new Account.Id using xid
// It also adds a User with id = old Account.Id with a role Admin
func Convert(oldStore *server.FileStore, newStore *server.FileStore) error {
for _, account := range oldStore.Accounts {
accountCopy := account.Copy()
accountCopy.Id = xid.New().String()
accountCopy.CreatedBy = account.Id
accountCopy.Users[account.Id] = &server.User{
Id: account.Id,
Role: server.UserRoleAdmin,
}
err := newStore.SaveAccount(accountCopy)
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,76 +0,0 @@
package main
import (
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/util"
"path/filepath"
"testing"
)
func TestConvertAccounts(t *testing.T) {
storeDir := t.TempDir()
err := util.CopyFileContents("../testdata/storev1.json", filepath.Join(storeDir, "store.json"))
if err != nil {
t.Fatal(err)
}
store, err := server.NewStore(storeDir)
if err != nil {
t.Fatal(err)
}
convertedStore, err := server.NewStore(filepath.Join(storeDir, "converted"))
if err != nil {
t.Fatal(err)
}
err = Convert(store, convertedStore)
if err != nil {
t.Fatal(err)
}
if len(store.Accounts) != len(convertedStore.Accounts) {
t.Errorf("expecting the same number of accounts after conversion")
}
for _, account := range store.Accounts {
convertedAccount, err := convertedStore.GetUserAccount(account.Id)
if err != nil || convertedAccount == nil {
t.Errorf("expecting Account %s to be converted", account.Id)
return
}
if convertedAccount.CreatedBy != account.Id {
t.Errorf("expecting converted Account.CreatedBy field to be equal to the old Account.Id")
return
}
if convertedAccount.Id == account.Id {
t.Errorf("expecting converted Account.Id to be different from Account.Id")
return
}
if len(convertedAccount.Users) != 1 {
t.Errorf("expecting converted Account.Users to be of size 1")
return
}
user := convertedAccount.Users[account.Id]
if user == nil {
t.Errorf("expecting to find a user in converted Account.Users")
return
}
if user.Role != server.UserRoleAdmin {
t.Errorf("expecting to find a user in converted Account.Users with a role Admin")
return
}
for peerId := range account.Peers {
convertedPeer := convertedAccount.Peers[peerId]
if convertedPeer == nil {
t.Errorf("expecting Account Peer of StoreV1 to be found in StoreV2")
return
}
}
}
}

View File

@@ -14,14 +14,14 @@ type MockAccountManager struct {
GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error)
GetAccountByUserFunc func(userId string) (*server.Account, error)
CreateSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration, autoGroups []string) (*server.SetupKey, error)
GetSetupKeyFunc func(accountID string, keyID string) (*server.SetupKey, error)
GetSetupKeyFunc func(accountID, userID, keyID string) (*server.SetupKey, error)
GetAccountByIdFunc func(accountId string) (*server.Account, error)
GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error)
IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error)
AccountExistsFunc func(accountId string) (*bool, error)
GetPeerFunc func(peerKey string) (*server.Peer, error)
GetPeersFunc func(accountID, userID string) ([]*server.Peer, error)
MarkPeerConnectedFunc func(peerKey string, connected bool) error
RenamePeerFunc func(accountId string, peerKey string, newName string) (*server.Peer, error)
DeletePeerFunc func(accountId string, peerKey string) (*server.Peer, error)
GetPeerByIPFunc func(accountId string, peerIP string) (*server.Peer, error)
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
@@ -35,26 +35,26 @@ type MockAccountManager struct {
GroupAddPeerFunc func(accountID, groupID, peerKey string) error
GroupDeletePeerFunc func(accountID, groupID, peerKey string) error
GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error)
GetRuleFunc func(accountID, ruleID string) (*server.Rule, error)
GetRuleFunc func(accountID, ruleID, userID string) (*server.Rule, error)
SaveRuleFunc func(accountID string, rule *server.Rule) error
UpdateRuleFunc func(accountID string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error)
DeleteRuleFunc func(accountID, ruleID string) error
ListRulesFunc func(accountID string) ([]*server.Rule, error)
GetUsersFromAccountFunc func(accountID string) ([]*server.UserInfo, error)
ListRulesFunc func(accountID, userID string) ([]*server.Rule, error)
GetUsersFromAccountFunc func(accountID, userID string) ([]*server.UserInfo, error)
UpdatePeerMetaFunc func(peerKey string, meta server.PeerSystemMeta) error
UpdatePeerSSHKeyFunc func(peerKey string, sshKey string) error
UpdatePeerFunc func(accountID string, peer *server.Peer) (*server.Peer, error)
CreateRouteFunc func(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error)
GetRouteFunc func(accountID, routeID string) (*route.Route, error)
GetRouteFunc func(accountID, routeID, userID string) (*route.Route, error)
SaveRouteFunc func(accountID string, route *route.Route) error
UpdateRouteFunc func(accountID string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error)
DeleteRouteFunc func(accountID, routeID string) error
ListRoutesFunc func(accountID string) ([]*route.Route, error)
ListRoutesFunc func(accountID, userID string) ([]*route.Route, error)
SaveSetupKeyFunc func(accountID string, key *server.SetupKey) (*server.SetupKey, error)
ListSetupKeysFunc func(accountID string) ([]*server.SetupKey, error)
ListSetupKeysFunc func(accountID, userID string) ([]*server.SetupKey, error)
SaveUserFunc func(accountID string, user *server.User) (*server.UserInfo, error)
GetNameServerGroupFunc func(accountID, nsGroupID string) (*nbdns.NameServerGroup, error)
CreateNameServerGroupFunc func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error)
CreateNameServerGroupFunc func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool) (*nbdns.NameServerGroup, error)
SaveNameServerGroupFunc func(accountID string, nsGroupToSave *nbdns.NameServerGroup) error
UpdateNameServerGroupFunc func(accountID, nsGroupID string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error)
DeleteNameServerGroupFunc func(accountID, nsGroupID string) error
@@ -64,13 +64,21 @@ type MockAccountManager struct {
}
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
func (am *MockAccountManager) GetUsersFromAccount(accountID string) ([]*server.UserInfo, error) {
func (am *MockAccountManager) GetUsersFromAccount(accountID string, userID string) ([]*server.UserInfo, error) {
if am.GetUsersFromAccountFunc != nil {
return am.GetUsersFromAccountFunc(accountID)
return am.GetUsersFromAccountFunc(accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetUsersFromAccount is not implemented")
}
// DeletePeer mock implementation of DeletePeer from server.AccountManager interface
func (am *MockAccountManager) DeletePeer(accountId string, peerKey string) (*server.Peer, error) {
if am.DeletePeerFunc != nil {
return am.DeletePeerFunc(accountId, peerKey)
}
return nil, status.Errorf(codes.Unimplemented, "method DeletePeer is not implemented")
}
// GetOrCreateAccountByUser mock implementation of GetOrCreateAccountByUser from server.AccountManager interface
func (am *MockAccountManager) GetOrCreateAccountByUser(
userId, domain string,
@@ -151,26 +159,6 @@ func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool)
return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented")
}
// RenamePeer mock implementation of RenamePeer from server.AccountManager interface
func (am *MockAccountManager) RenamePeer(
accountId string,
peerKey string,
newName string,
) (*server.Peer, error) {
if am.RenamePeerFunc != nil {
return am.RenamePeerFunc(accountId, peerKey, newName)
}
return nil, status.Errorf(codes.Unimplemented, "method RenamePeer is not implemented")
}
// DeletePeer mock implementation of DeletePeer from server.AccountManager interface
func (am *MockAccountManager) DeletePeer(accountId string, peerKey string) (*server.Peer, error) {
if am.DeletePeerFunc != nil {
return am.DeletePeerFunc(accountId, peerKey)
}
return nil, status.Errorf(codes.Unimplemented, "method DeletePeer is not implemented")
}
// GetPeerByIP mock implementation of GetPeerByIP from server.AccountManager interface
func (am *MockAccountManager) GetPeerByIP(accountId string, peerIP string) (*server.Peer, error) {
if am.GetPeerByIPFunc != nil {
@@ -272,9 +260,9 @@ func (am *MockAccountManager) GroupListPeers(accountID, groupID string) ([]*serv
}
// GetRule mock implementation of GetRule from server.AccountManager interface
func (am *MockAccountManager) GetRule(accountID, ruleID string) (*server.Rule, error) {
func (am *MockAccountManager) GetRule(accountID, ruleID, userID string) (*server.Rule, error) {
if am.GetRuleFunc != nil {
return am.GetRuleFunc(accountID, ruleID)
return am.GetRuleFunc(accountID, ruleID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetRule is not implemented")
}
@@ -304,9 +292,9 @@ func (am *MockAccountManager) DeleteRule(accountID, ruleID string) error {
}
// ListRules mock implementation of ListRules from server.AccountManager interface
func (am *MockAccountManager) ListRules(accountID string) ([]*server.Rule, error) {
func (am *MockAccountManager) ListRules(accountID, userID string) ([]*server.Rule, error) {
if am.ListRulesFunc != nil {
return am.ListRulesFunc(accountID)
return am.ListRulesFunc(accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method ListRules is not implemented")
}
@@ -345,16 +333,16 @@ func (am *MockAccountManager) UpdatePeer(accountID string, peer *server.Peer) (*
// CreateRoute mock implementation of CreateRoute from server.AccountManager interface
func (am *MockAccountManager) CreateRoute(accountID string, network, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) {
if am.GetRouteFunc != nil {
if am.CreateRouteFunc != nil {
return am.CreateRouteFunc(accountID, network, peer, description, netID, masquerade, metric, enabled)
}
return nil, status.Errorf(codes.Unimplemented, "method CreateRoute is not implemented")
}
// GetRoute mock implementation of GetRoute from server.AccountManager interface
func (am *MockAccountManager) GetRoute(accountID, routeID string) (*route.Route, error) {
func (am *MockAccountManager) GetRoute(accountID, routeID, userID string) (*route.Route, error) {
if am.GetRouteFunc != nil {
return am.GetRouteFunc(accountID, routeID)
return am.GetRouteFunc(accountID, routeID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetRoute is not implemented")
}
@@ -384,9 +372,9 @@ func (am *MockAccountManager) DeleteRoute(accountID, routeID string) error {
}
// ListRoutes mock implementation of ListRoutes from server.AccountManager interface
func (am *MockAccountManager) ListRoutes(accountID string) ([]*route.Route, error) {
func (am *MockAccountManager) ListRoutes(accountID, userID string) ([]*route.Route, error) {
if am.ListRoutesFunc != nil {
return am.ListRoutesFunc(accountID)
return am.ListRoutesFunc(accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method ListRoutes is not implemented")
}
@@ -401,18 +389,18 @@ func (am *MockAccountManager) SaveSetupKey(accountID string, key *server.SetupKe
}
// GetSetupKey mocks GetSetupKey of the AccountManager interface
func (am *MockAccountManager) GetSetupKey(accountID, keyID string) (*server.SetupKey, error) {
func (am *MockAccountManager) GetSetupKey(accountID, userID, keyID string) (*server.SetupKey, error) {
if am.GetSetupKeyFunc != nil {
return am.GetSetupKeyFunc(accountID, keyID)
return am.GetSetupKeyFunc(accountID, userID, keyID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetSetupKey is not implemented")
}
// ListSetupKeys mocks ListSetupKeys of the AccountManager interface
func (am *MockAccountManager) ListSetupKeys(accountID string) ([]*server.SetupKey, error) {
func (am *MockAccountManager) ListSetupKeys(accountID, userID string) ([]*server.SetupKey, error) {
if am.ListSetupKeysFunc != nil {
return am.ListSetupKeysFunc(accountID)
return am.ListSetupKeysFunc(accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method ListSetupKeys is not implemented")
@@ -435,9 +423,9 @@ func (am *MockAccountManager) GetNameServerGroup(accountID, nsGroupID string) (*
}
// CreateNameServerGroup mocks CreateNameServerGroup of the AccountManager interface
func (am *MockAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error) {
func (am *MockAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool) (*nbdns.NameServerGroup, error) {
if am.CreateNameServerGroupFunc != nil {
return am.CreateNameServerGroupFunc(accountID, name, description, nameServerList, groups, enabled)
return am.CreateNameServerGroupFunc(accountID, name, description, nameServerList, groups, primary, domains, enabled)
}
return nil, nil
}
@@ -489,3 +477,11 @@ func (am *MockAccountManager) GetAccountFromToken(claims jwtclaims.Authorization
}
return nil, status.Errorf(codes.Unimplemented, "method GetAccountFromToken is not implemented")
}
// GetPeers mocks GetPeers of the AccountManager interface
func (am *MockAccountManager) GetPeers(accountID, userID string) ([]*server.Peer, error) {
if am.GetAccountFromTokenFunc != nil {
return am.GetPeersFunc(accountID, userID)
}
return nil, status.Errorf(codes.Unimplemented, "method GetPeersFunc is not implemented")
}

View File

@@ -1,6 +1,7 @@
package server
import (
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/rs/xid"
"google.golang.org/grpc/codes"
@@ -20,6 +21,10 @@ const (
UpdateNameServerGroupGroups
// UpdateNameServerGroupEnabled indicates a nameserver group status update operation
UpdateNameServerGroupEnabled
// UpdateNameServerGroupPrimary indicates a nameserver group primary status update operation
UpdateNameServerGroupPrimary
// UpdateNameServerGroupDomains indicates a nameserver group' domains update operation
UpdateNameServerGroupDomains
)
// NameServerGroupUpdateOperationType operation type
@@ -37,6 +42,10 @@ func (t NameServerGroupUpdateOperationType) String() string {
return "UpdateNameServerGroupGroups"
case UpdateNameServerGroupEnabled:
return "UpdateNameServerGroupEnabled"
case UpdateNameServerGroupPrimary:
return "UpdateNameServerGroupPrimary"
case UpdateNameServerGroupDomains:
return "UpdateNameServerGroupDomains"
default:
return "InvalidOperation"
}
@@ -67,7 +76,7 @@ func (am *DefaultAccountManager) GetNameServerGroup(accountID, nsGroupID string)
}
// CreateNameServerGroup creates and saves a new nameserver group
func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error) {
func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool) (*nbdns.NameServerGroup, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -83,6 +92,8 @@ func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, d
NameServers: nameServerList,
Groups: groups,
Enabled: enabled,
Primary: primary,
Domains: domains,
}
err = validateNameServerGroup(false, newNSGroup, account)
@@ -205,6 +216,18 @@ func (am *DefaultAccountManager) UpdateNameServerGroup(accountID, nsGroupID stri
return nil, status.Errorf(codes.InvalidArgument, "failed to parse enabled %s, not boolean", operation.Values[0])
}
newNSGroup.Enabled = enabled
case UpdateNameServerGroupPrimary:
primary, err := strconv.ParseBool(operation.Values[0])
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to parse primary status %s, not boolean", operation.Values[0])
}
newNSGroup.Primary = primary
case UpdateNameServerGroupDomains:
err = validateDomainInput(false, operation.Values)
if err != nil {
return nil, err
}
newNSGroup.Domains = operation.Values
}
}
@@ -268,7 +291,12 @@ func validateNameServerGroup(existingGroup bool, nameserverGroup *nbdns.NameServ
}
}
err := validateNSGroupName(nameserverGroup.Name, nsGroupID, account.NameServerGroups)
err := validateDomainInput(nameserverGroup.Primary, nameserverGroup.Domains)
if err != nil {
return err
}
err = validateNSGroupName(nameserverGroup.Name, nsGroupID, account.NameServerGroups)
if err != nil {
return err
}
@@ -286,6 +314,24 @@ func validateNameServerGroup(existingGroup bool, nameserverGroup *nbdns.NameServ
return nil
}
func validateDomainInput(primary bool, domains []string) error {
if !primary && len(domains) == 0 {
return status.Errorf(codes.InvalidArgument, "nameserver group primary status is false and domains are empty,"+
" it should be primary or have at least one domain")
}
if primary && len(domains) != 0 {
return status.Errorf(codes.InvalidArgument, "nameserver group primary status is true and domains are not empty,"+
" you should set either primary or domain")
}
for _, domain := range domains {
_, valid := dns.IsDomainName(domain)
if !valid {
return status.Errorf(codes.InvalidArgument, "nameserver group got an invalid domain: %s", domain)
}
}
return nil
}
func validateNSGroupName(name, nsGroupID string, nsGroupMap map[string]*nbdns.NameServerGroup) error {
if utf8.RuneCountInString(name) > nbdns.MaxGroupNameChar || name == "" {
return status.Errorf(codes.InvalidArgument, "nameserver group name should be between 1 and %d", nbdns.MaxGroupNameChar)

View File

@@ -14,6 +14,8 @@ const (
existingNSGroupID = "existingNSGroup"
nsGroupPeer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8="
nsGroupPeer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI="
validDomain = "example.com"
invalidDomain = "dnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdns.com"
)
func TestCreateNameServerGroup(t *testing.T) {
@@ -23,6 +25,8 @@ func TestCreateNameServerGroup(t *testing.T) {
enabled bool
groups []string
nameServers []nbdns.NameServer
primary bool
domains []string
}
testCases := []struct {
@@ -33,11 +37,12 @@ func TestCreateNameServerGroup(t *testing.T) {
expectedNSGroup *nbdns.NameServerGroup
}{
{
name: "Create A NS Group",
name: "Create A NS Group With Primary Status",
inputArgs: input{
name: "super",
description: "super",
groups: []string{group1ID},
primary: true,
nameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
@@ -57,6 +62,52 @@ func TestCreateNameServerGroup(t *testing.T) {
expectedNSGroup: &nbdns.NameServerGroup{
Name: "super",
Description: "super",
Primary: true,
Groups: []string{group1ID},
NameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
{
IP: netip.MustParseAddr("1.1.2.2"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
},
Enabled: true,
},
},
{
name: "Create A NS Group With Domains",
inputArgs: input{
name: "super",
description: "super",
groups: []string{group1ID},
primary: false,
domains: []string{validDomain},
nameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
{
IP: netip.MustParseAddr("1.1.2.2"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
},
enabled: true,
},
errFunc: require.NoError,
shouldCreate: true,
expectedNSGroup: &nbdns.NameServerGroup{
Name: "super",
Description: "super",
Primary: false,
Domains: []string{"example.com"},
Groups: []string{group1ID},
NameServers: []nbdns.NameServer{
{
@@ -78,6 +129,7 @@ func TestCreateNameServerGroup(t *testing.T) {
inputArgs: input{
name: existingNSGroupName,
description: "super",
primary: true,
groups: []string{group1ID},
nameServers: []nbdns.NameServer{
{
@@ -101,6 +153,7 @@ func TestCreateNameServerGroup(t *testing.T) {
inputArgs: input{
name: "",
description: "super",
primary: true,
groups: []string{group1ID},
nameServers: []nbdns.NameServer{
{
@@ -124,6 +177,7 @@ func TestCreateNameServerGroup(t *testing.T) {
inputArgs: input{
name: "1234567890123456789012345678901234567890extra",
description: "super",
primary: true,
groups: []string{group1ID},
nameServers: []nbdns.NameServer{
{
@@ -147,6 +201,7 @@ func TestCreateNameServerGroup(t *testing.T) {
inputArgs: input{
name: "super",
description: "super",
primary: true,
groups: []string{group1ID},
nameServers: []nbdns.NameServer{},
enabled: true,
@@ -159,6 +214,7 @@ func TestCreateNameServerGroup(t *testing.T) {
inputArgs: input{
name: "super",
description: "super",
primary: true,
groups: []string{group1ID},
nameServers: []nbdns.NameServer{
{
@@ -187,6 +243,7 @@ func TestCreateNameServerGroup(t *testing.T) {
inputArgs: input{
name: "super",
description: "super",
primary: true,
groups: []string{},
nameServers: []nbdns.NameServer{
{
@@ -210,6 +267,7 @@ func TestCreateNameServerGroup(t *testing.T) {
inputArgs: input{
name: "super",
description: "super",
primary: true,
groups: []string{"missingGroup"},
nameServers: []nbdns.NameServer{
{
@@ -233,6 +291,7 @@ func TestCreateNameServerGroup(t *testing.T) {
inputArgs: input{
name: "super",
description: "super",
primary: true,
groups: []string{""},
nameServers: []nbdns.NameServer{
{
@@ -251,6 +310,53 @@ func TestCreateNameServerGroup(t *testing.T) {
errFunc: require.Error,
shouldCreate: false,
},
{
name: "Should Not Create If No Domain Or Primary",
inputArgs: input{
name: "super",
description: "super",
groups: []string{group1ID},
nameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
{
IP: netip.MustParseAddr("1.1.2.2"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
},
enabled: true,
},
errFunc: require.Error,
shouldCreate: false,
},
{
name: "Should Not Create If Domain List Is Invalid",
inputArgs: input{
name: "super",
description: "super",
groups: []string{group1ID},
domains: []string{invalidDomain},
nameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
{
IP: netip.MustParseAddr("1.1.2.2"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
},
enabled: true,
},
errFunc: require.Error,
shouldCreate: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
@@ -270,6 +376,8 @@ func TestCreateNameServerGroup(t *testing.T) {
testCase.inputArgs.description,
testCase.inputArgs.nameServers,
testCase.inputArgs.groups,
testCase.inputArgs.primary,
testCase.inputArgs.domains,
testCase.inputArgs.enabled,
)
@@ -295,6 +403,7 @@ func TestSaveNameServerGroup(t *testing.T) {
ID: "testingNSGroup",
Name: "super",
Description: "super",
Primary: true,
NameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
@@ -313,6 +422,10 @@ func TestSaveNameServerGroup(t *testing.T) {
validGroups := []string{group2ID}
invalidGroups := []string{"nonExisting"}
disabledPrimary := false
validDomains := []string{validDomain}
invalidDomains := []string{invalidDomain}
validNameServerList := []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
@@ -348,6 +461,8 @@ func TestSaveNameServerGroup(t *testing.T) {
existingNSGroup *nbdns.NameServerGroup
newID *string
newName *string
newPrimary *bool
newDomains []string
newNSList []nbdns.NameServer
newGroups []string
skipCopying bool
@@ -360,12 +475,16 @@ func TestSaveNameServerGroup(t *testing.T) {
existingNSGroup: existingNSGroup,
newName: &validName,
newGroups: validGroups,
newPrimary: &disabledPrimary,
newDomains: validDomains,
newNSList: validNameServerList,
errFunc: require.NoError,
shouldCreate: true,
expectedNSGroup: &nbdns.NameServerGroup{
ID: "testingNSGroup",
Name: validName,
Primary: false,
Domains: validDomains,
Description: "super",
NameServers: validNameServerList,
Groups: validGroups,
@@ -435,6 +554,29 @@ func TestSaveNameServerGroup(t *testing.T) {
errFunc: require.Error,
shouldCreate: false,
},
{
name: "Should Not Update If Domains List Is Empty",
existingNSGroup: existingNSGroup,
newPrimary: &disabledPrimary,
errFunc: require.Error,
shouldCreate: false,
},
{
name: "Should Not Update If Primary And Domains",
existingNSGroup: existingNSGroup,
newPrimary: &existingNSGroup.Primary,
newDomains: validDomains,
errFunc: require.Error,
shouldCreate: false,
},
{
name: "Should Not Update If Domains List Is Invalid",
existingNSGroup: existingNSGroup,
newPrimary: &disabledPrimary,
newDomains: invalidDomains,
errFunc: require.Error,
shouldCreate: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
@@ -475,6 +617,14 @@ func TestSaveNameServerGroup(t *testing.T) {
if testCase.newNSList != nil {
nsGroupToSave.NameServers = testCase.newNSList
}
if testCase.newPrimary != nil {
nsGroupToSave.Primary = *testCase.newPrimary
}
if testCase.newDomains != nil {
nsGroupToSave.Domains = testCase.newDomains
}
}
err = am.SaveNameServerGroup(account.Id, nsGroupToSave)
@@ -485,6 +635,11 @@ func TestSaveNameServerGroup(t *testing.T) {
return
}
account, err = am.GetAccountById(account.Id)
if err != nil {
t.Fatal(err)
}
savedNSGroup, saved := account.NameServerGroups[testCase.expectedNSGroup.ID]
require.True(t, saved)
@@ -503,6 +658,7 @@ func TestUpdateNameServerGroup(t *testing.T) {
ID: nsGroupID,
Name: "super",
Description: "super",
Primary: true,
NameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
@@ -544,6 +700,7 @@ func TestUpdateNameServerGroup(t *testing.T) {
ID: nsGroupID,
Name: "superNew",
Description: "super",
Primary: true,
NameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("1.1.1.1"),
@@ -585,6 +742,14 @@ func TestUpdateNameServerGroup(t *testing.T) {
Type: UpdateNameServerGroupEnabled,
Values: []string{"false"},
},
NameServerGroupUpdateOperation{
Type: UpdateNameServerGroupPrimary,
Values: []string{"false"},
},
NameServerGroupUpdateOperation{
Type: UpdateNameServerGroupDomains,
Values: []string{validDomain},
},
},
errFunc: require.NoError,
shouldCreate: true,
@@ -592,6 +757,8 @@ func TestUpdateNameServerGroup(t *testing.T) {
ID: nsGroupID,
Name: "superNew",
Description: "superDescription",
Primary: false,
Domains: []string{validDomain},
NameServers: []nbdns.NameServer{
{
IP: netip.MustParseAddr("127.0.0.1"),
@@ -740,6 +907,30 @@ func TestUpdateNameServerGroup(t *testing.T) {
},
errFunc: require.Error,
},
{
name: "Should Not Update On Invalid Domains",
existingNSGroup: existingNSGroup,
nsGroupID: existingNSGroup.ID,
operations: []NameServerGroupUpdateOperation{
NameServerGroupUpdateOperation{
Type: UpdateNameServerGroupDomains,
Values: []string{invalidDomain},
},
},
errFunc: require.Error,
},
{
name: "Should Not Update On Invalid Primary Status",
existingNSGroup: existingNSGroup,
nsGroupID: existingNSGroup.ID,
operations: []NameServerGroupUpdateOperation{
NameServerGroupUpdateOperation{
Type: UpdateNameServerGroupPrimary,
Values: []string{"yes"},
},
},
errFunc: require.Error,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
@@ -865,7 +1056,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) {
if err != nil {
return nil, err
}
return BuildManager(store, NewPeersUpdateManager(), nil)
return BuildManager(store, NewPeersUpdateManager(), nil, "")
}
func createNSStore(t *testing.T) (Store, error) {

View File

@@ -68,38 +68,67 @@ func (p *Peer) Copy() *Peer {
}
}
// GetPeer returns a peer from a Store
func (am *DefaultAccountManager) GetPeer(peerKey string) (*Peer, error) {
// GetPeer looks up peer by its public WireGuard key
func (am *DefaultAccountManager) GetPeer(peerPubKey string) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()
peer, err := am.Store.GetPeer(peerKey)
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
if err != nil {
return nil, err
}
return peer, nil
return account.FindPeerByPubKey(peerPubKey)
}
// 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) {
am.mux.Lock()
defer am.mux.Unlock()
account, err := am.Store.GetAccount(accountID)
if err != nil {
return nil, err
}
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
peers := make([]*Peer, 0, len(account.Peers))
for _, peer := range account.Peers {
if !user.IsAdmin() && user.Id != peer.UserID {
// only display peers that belong to the current user if the current user is not an admin
continue
}
peers = append(peers, peer.Copy())
}
return peers, nil
}
// MarkPeerConnected marks peer as connected (true) or disconnected (false)
func (am *DefaultAccountManager) MarkPeerConnected(peerKey string, connected bool) error {
func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected bool) error {
am.mux.Lock()
defer am.mux.Unlock()
peer, err := am.Store.GetPeer(peerKey)
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
if err != nil {
return err
}
account, err := am.Store.GetPeerAccount(peerKey)
peer, err := account.FindPeerByPubKey(peerPubKey)
if err != nil {
return err
}
peerCopy := peer.Copy()
peerCopy.Status.LastSeen = time.Now()
peerCopy.Status.Connected = connected
err = am.Store.SavePeer(account.Id, peerCopy)
peer.Status.LastSeen = time.Now()
peer.Status.Connected = connected
account.UpdatePeer(peer)
err = am.Store.SaveAccount(account)
if err != nil {
return err
}
@@ -116,18 +145,20 @@ func (am *DefaultAccountManager) UpdatePeer(accountID string, update *Peer) (*Pe
return nil, status.Errorf(codes.NotFound, "account not found")
}
peer, err := am.Store.GetPeer(update.Key)
//TODO Peer.ID migration: we will need to replace search by ID here
peer, err := account.FindPeerByPubKey(update.Key)
if err != nil {
return nil, err
}
peerCopy := peer.Copy()
if peer.Name != "" {
peerCopy.Name = update.Name
peer.Name = update.Name
}
peerCopy.SSHEnabled = update.SSHEnabled
peer.SSHEnabled = update.SSHEnabled
err = am.Store.SavePeer(accountID, peerCopy)
account.UpdatePeer(peer)
err = am.Store.SaveAccount(account)
if err != nil {
return nil, err
}
@@ -137,66 +168,32 @@ func (am *DefaultAccountManager) UpdatePeer(accountID string, update *Peer) (*Pe
return nil, err
}
return peerCopy, nil
return peer, nil
}
// RenamePeer changes peer's name
func (am *DefaultAccountManager) RenamePeer(
accountId string,
peerKey string,
newName string,
) (*Peer, error) {
// DeletePeer removes peer from the account by its IP
func (am *DefaultAccountManager) DeletePeer(accountID string, peerPubKey string) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()
peer, err := am.Store.GetPeer(peerKey)
if err != nil {
return nil, err
}
peerCopy := peer.Copy()
peerCopy.Name = newName
err = am.Store.SavePeer(accountId, peerCopy)
if err != nil {
return nil, err
}
return peerCopy, nil
}
// DeletePeer removes peer from the account by it's IP
func (am *DefaultAccountManager) DeletePeer(accountId string, peerKey string) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()
account, err := am.Store.GetAccount(accountId)
account, err := am.Store.GetAccount(accountID)
if err != nil {
return nil, status.Errorf(codes.NotFound, "account not found")
}
// delete peer from groups
for _, g := range account.Groups {
for i, pk := range g.Peers {
if pk == peerKey {
g.Peers = append(g.Peers[:i], g.Peers[i+1:]...)
break
}
}
}
peer, err := am.Store.DeletePeer(accountId, peerKey)
peer, err := account.FindPeerByPubKey(peerPubKey)
if err != nil {
return nil, err
}
account.Network.IncSerial()
account.DeletePeer(peerPubKey)
err = am.Store.SaveAccount(account)
if err != nil {
return nil, err
}
err = am.peersUpdateManager.SendUpdate(peerKey,
err = am.peersUpdateManager.SendUpdate(peerPubKey,
&UpdateMessage{
Update: &proto.SyncResponse{
// fill those field for backward compatibility
@@ -214,20 +211,21 @@ func (am *DefaultAccountManager) DeletePeer(accountId string, peerKey string) (*
return nil, err
}
// TODO Peer.ID migration: we will need to replace search by Peer.ID here
if err := am.updateAccountPeers(account); err != nil {
return nil, err
}
am.peersUpdateManager.CloseChannel(peerKey)
am.peersUpdateManager.CloseChannel(peerPubKey)
return peer, nil
}
// GetPeerByIP returns peer by it's IP
func (am *DefaultAccountManager) GetPeerByIP(accountId string, peerIP string) (*Peer, error) {
// GetPeerByIP returns peer by its IP
func (am *DefaultAccountManager) GetPeerByIP(accountID string, peerIP string) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()
account, err := am.Store.GetAccount(accountId)
account, err := am.Store.GetAccount(accountID)
if err != nil {
return nil, status.Errorf(codes.NotFound, "account not found")
}
@@ -242,17 +240,17 @@ func (am *DefaultAccountManager) GetPeerByIP(accountId string, peerIP string) (*
}
// GetNetworkMap returns Network map for a given peer (omits original peer from the Peers result)
func (am *DefaultAccountManager) GetNetworkMap(peerKey string) (*NetworkMap, error) {
func (am *DefaultAccountManager) GetNetworkMap(peerPubKey string) (*NetworkMap, error) {
am.mux.Lock()
defer am.mux.Unlock()
account, err := am.Store.GetPeerAccount(peerKey)
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
if err != nil {
return nil, status.Errorf(codes.Internal, "Invalid peer key %s", peerKey)
return nil, status.Errorf(codes.Internal, "Invalid peer key %s", peerPubKey)
}
aclPeers := am.getPeersByACL(account, peerKey)
routesUpdate := am.getPeersRoutes(append(aclPeers, account.Peers[peerKey]))
aclPeers := am.getPeersByACL(account, peerPubKey)
routesUpdate := account.GetPeersRoutes(append(aclPeers, account.Peers[peerPubKey]))
return &NetworkMap{
Peers: aclPeers,
@@ -262,13 +260,13 @@ func (am *DefaultAccountManager) GetNetworkMap(peerKey string) (*NetworkMap, err
}
// GetPeerNetwork returns the Network for a given peer
func (am *DefaultAccountManager) GetPeerNetwork(peerKey string) (*Network, error) {
func (am *DefaultAccountManager) GetPeerNetwork(peerPubKey string) (*Network, error) {
am.mux.Lock()
defer am.mux.Unlock()
account, err := am.Store.GetPeerAccount(peerKey)
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
if err != nil {
return nil, status.Errorf(codes.Internal, "Invalid peer key %s", peerKey)
return nil, status.Errorf(codes.Internal, "invalid peer key %s", peerPubKey)
}
return account.Network.Copy(), err
@@ -281,11 +279,7 @@ func (am *DefaultAccountManager) GetPeerNetwork(peerKey string) (*Network, error
// to it. We also add the User ID to the peer metadata to identify registrant.
// 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 string,
userID string,
peer *Peer,
) (*Peer, error) {
func (am *DefaultAccountManager) AddPeer(setupKey string, userID string, peer *Peer) (*Peer, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -326,7 +320,7 @@ func (am *DefaultAccountManager) AddPeer(
groupsToAdd = sk.AutoGroups
} else if len(userID) != 0 {
account, err = am.Store.GetUserAccount(userID)
account, err = am.Store.GetAccountByUser(userID)
if err != nil {
return nil, status.Errorf(codes.NotFound, "unable to register peer, unknown user with ID: %s", userID)
}
@@ -395,34 +389,34 @@ func (am *DefaultAccountManager) AddPeer(
}
// UpdatePeerSSHKey updates peer's public SSH key
func (am *DefaultAccountManager) UpdatePeerSSHKey(peerKey string, sshKey string) error {
func (am *DefaultAccountManager) UpdatePeerSSHKey(peerPubKey string, sshKey string) error {
am.mux.Lock()
defer am.mux.Unlock()
if sshKey == "" {
log.Debugf("empty SSH key provided for peer %s, skipping update", peerKey)
log.Debugf("empty SSH key provided for peer %s, skipping update", peerPubKey)
return nil
}
peer, err := am.Store.GetPeer(peerKey)
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
if err != nil {
return err
}
peer, err := account.FindPeerByPubKey(peerPubKey)
if err != nil {
return err
}
if peer.SSHKey == sshKey {
log.Debugf("same SSH key provided for peer %s, skipping update", peerKey)
log.Debugf("same SSH key provided for peer %s, skipping update", peerPubKey)
return nil
}
account, err := am.Store.GetPeerAccount(peerKey)
if err != nil {
return err
}
peer.SSHKey = sshKey
account.UpdatePeer(peer)
peerCopy := peer.Copy()
peerCopy.SSHKey = sshKey
err = am.Store.SavePeer(account.Id, peerCopy)
err = am.Store.SaveAccount(account)
if err != nil {
return err
}
@@ -432,29 +426,29 @@ func (am *DefaultAccountManager) UpdatePeerSSHKey(peerKey string, sshKey string)
}
// UpdatePeerMeta updates peer's system metadata
func (am *DefaultAccountManager) UpdatePeerMeta(peerKey string, meta PeerSystemMeta) error {
func (am *DefaultAccountManager) UpdatePeerMeta(peerPubKey string, meta PeerSystemMeta) error {
am.mux.Lock()
defer am.mux.Unlock()
peer, err := am.Store.GetPeer(peerKey)
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
if err != nil {
return err
}
account, err := am.Store.GetPeerAccount(peerKey)
peer, err := account.FindPeerByPubKey(peerPubKey)
if err != nil {
return err
}
peerCopy := peer.Copy()
// Avoid overwriting UIVersion if the update was triggered sole by the CLI client
if meta.UIVersion == "" {
meta.UIVersion = peerCopy.Meta.UIVersion
meta.UIVersion = peer.Meta.UIVersion
}
peerCopy.Meta = meta
peer.Meta = meta
account.UpdatePeer(peer)
err = am.Store.SavePeer(account.Id, peerCopy)
err = am.Store.SaveAccount(account)
if err != nil {
return err
}
@@ -462,17 +456,9 @@ func (am *DefaultAccountManager) UpdatePeerMeta(peerKey string, meta PeerSystemM
}
// getPeersByACL returns all peers that given peer has access to.
func (am *DefaultAccountManager) getPeersByACL(account *Account, peerKey string) []*Peer {
func (am *DefaultAccountManager) getPeersByACL(account *Account, peerPubKey string) []*Peer {
var peers []*Peer
srcRules, err := am.Store.GetPeerSrcRules(account.Id, peerKey)
if err != nil {
srcRules = []*Rule{}
}
dstRules, err := am.Store.GetPeerDstRules(account.Id, peerKey)
if err != nil {
dstRules = []*Rule{}
}
srcRules, dstRules := account.GetPeerRules(peerPubKey)
groups := map[string]*Group{}
for _, r := range srcRules {
@@ -515,7 +501,7 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerKey string)
continue
}
// exclude original peer
if _, ok := peersSet[peer.Key]; peer.Key != peerKey && !ok {
if _, ok := peersSet[peer.Key]; peer.Key != peerPubKey && !ok {
peersSet[peer.Key] = struct{}{}
peers = append(peers, peer.Copy())
}
@@ -529,18 +515,14 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerKey string)
// Should be called when changes have to be synced to peers.
func (am *DefaultAccountManager) updateAccountPeers(account *Account) error {
// notify other peers of the change
peers, err := am.Store.GetAccountPeers(account.Id)
if err != nil {
return err
}
peers := account.GetPeers()
network := account.Network.Copy()
for _, peer := range peers {
aclPeers := am.getPeersByACL(account, peer.Key)
peersUpdate := toRemotePeerConfig(aclPeers)
routesUpdate := toProtocolRoutes(am.getPeersRoutes(append(aclPeers, peer)))
err = am.peersUpdateManager.SendUpdate(peer.Key,
routesUpdate := toProtocolRoutes(account.GetPeersRoutes(append(aclPeers, peer)))
err := am.peersUpdateManager.SendUpdate(peer.Key,
&UpdateMessage{
Update: &proto.SyncResponse{
// fill deprecated fields for backward compatibility

View File

@@ -134,7 +134,7 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
return
}
rules, err := manager.ListRules(account.Id)
rules, err := manager.ListRules(account.Id, userId)
if err != nil {
t.Errorf("expecting to get a list of rules, got failure %v", err)
return

View File

@@ -60,7 +60,7 @@ type RouteUpdateOperation struct {
}
// GetRoute gets a route object from account and route IDs
func (am *DefaultAccountManager) GetRoute(accountID, routeID string) (*route.Route, error) {
func (am *DefaultAccountManager) GetRoute(accountID, routeID, userID string) (*route.Route, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -69,6 +69,15 @@ func (am *DefaultAccountManager) GetRoute(accountID, routeID string) (*route.Rou
return nil, status.Errorf(codes.NotFound, "account not found")
}
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
if !user.IsAdmin() {
return nil, Errorf(PermissionDenied, "Only administrators can view Network Routes")
}
wantedRoute, found := account.Routes[routeID]
if found {
return wantedRoute, nil
@@ -84,7 +93,12 @@ func (am *DefaultAccountManager) checkPrefixPeerExists(accountID, peer string, p
return nil
}
routesWithPrefix, err := am.Store.GetRoutesByPrefix(accountID, prefix)
account, err := am.Store.GetAccount(accountID)
if err != nil {
return err
}
routesWithPrefix := account.GetRoutesByPrefix(prefix)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
@@ -325,7 +339,7 @@ func (am *DefaultAccountManager) DeleteRoute(accountID, routeID string) error {
}
// ListRoutes returns a list of routes from account
func (am *DefaultAccountManager) ListRoutes(accountID string) ([]*route.Route, error) {
func (am *DefaultAccountManager) ListRoutes(accountID, userID string) ([]*route.Route, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -334,6 +348,15 @@ func (am *DefaultAccountManager) ListRoutes(accountID string) ([]*route.Route, e
return nil, status.Errorf(codes.NotFound, "account not found")
}
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
if !user.IsAdmin() {
return nil, Errorf(PermissionDenied, "Only administrators can view Network Routes")
}
routes := make([]*route.Route, 0, len(account.Routes))
for _, item := range account.Routes {
routes = append(routes, item)
@@ -354,30 +377,6 @@ func toProtocolRoute(route *route.Route) *proto.Route {
}
}
func (am *DefaultAccountManager) getPeersRoutes(peers []*Peer) []*route.Route {
routes := make([]*route.Route, 0)
for _, peer := range peers {
peerRoutes, err := am.Store.GetPeerRoutes(peer.Key)
if err != nil {
errorStatus, ok := status.FromError(err)
if !ok && errorStatus.Code() != codes.NotFound {
log.Error(err)
}
continue
}
activeRoutes := make([]*route.Route, 0)
for _, pr := range peerRoutes {
if pr.Enabled {
activeRoutes = append(activeRoutes, pr)
}
}
if len(activeRoutes) > 0 {
routes = append(routes, activeRoutes...)
}
}
return routes
}
func toProtocolRoutes(routes []*route.Route) []*proto.Route {
protoRoutes := make([]*proto.Route, 0)
for _, r := range routes {

View File

@@ -380,6 +380,11 @@ func TestSaveRoute(t *testing.T) {
return
}
account, err = am.GetAccountById(account.Id)
if err != nil {
t.Fatal(err)
}
savedRoute, saved := account.Routes[testCase.expectedRoute.ID]
require.True(t, saved)
@@ -740,7 +745,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) {
err = am.SaveGroup(account.Id, newGroup)
require.NoError(t, err)
rules, err := am.ListRules(account.Id)
rules, err := am.ListRules(account.Id, "testingUser")
require.NoError(t, err)
defaultRule := rules[0]
@@ -778,7 +783,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) {
if err != nil {
return nil, err
}
return BuildManager(store, NewPeersUpdateManager(), nil)
return BuildManager(store, NewPeersUpdateManager(), nil, "")
}
func createRouterStore(t *testing.T) (Store, error) {
@@ -840,5 +845,5 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
return nil, err
}
return account, nil
return am.GetAccountById(accountID)
}

View File

@@ -89,7 +89,7 @@ func (r *Rule) Copy() *Rule {
}
// GetRule of ACL from the store
func (am *DefaultAccountManager) GetRule(accountID, ruleID string) (*Rule, error) {
func (am *DefaultAccountManager) GetRule(accountID, ruleID, userID string) (*Rule, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -98,6 +98,15 @@ func (am *DefaultAccountManager) GetRule(accountID, ruleID string) (*Rule, error
return nil, status.Errorf(codes.NotFound, "account not found")
}
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
if !user.IsAdmin() {
return nil, Errorf(PermissionDenied, "only admins are allowed to view rules")
}
rule, ok := account.Rules[ruleID]
if ok {
return rule, nil
@@ -222,7 +231,7 @@ func (am *DefaultAccountManager) DeleteRule(accountID, ruleID string) error {
}
// ListRules of ACL from the store
func (am *DefaultAccountManager) ListRules(accountID string) ([]*Rule, error) {
func (am *DefaultAccountManager) ListRules(accountID, userID string) ([]*Rule, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -231,6 +240,15 @@ func (am *DefaultAccountManager) ListRules(accountID string) ([]*Rule, error) {
return nil, status.Errorf(codes.NotFound, "account not found")
}
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
if !user.IsAdmin() {
return nil, Errorf(PermissionDenied, "Only Administrators can view Access Rules")
}
rules := make([]*Rule, 0, len(account.Rules))
for _, item := range account.Rules {
rules = append(rules, item)

View File

@@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"
)
const (
@@ -100,6 +101,15 @@ func (key *SetupKey) Copy() *SetupKey {
}
}
// HiddenCopy returns a copy of the key with a Key value hidden with "*" and a 5 character prefix.
// E.g., "831F6*******************************"
func (key *SetupKey) HiddenCopy() *SetupKey {
k := key.Copy()
prefix := k.Key[0:5]
k.Key = prefix + strings.Repeat("*", utf8.RuneCountInString(key.Key)-len(prefix))
return k
}
// 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()
@@ -238,7 +248,7 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup
}
// ListSetupKeys returns a list of all setup keys of the account
func (am *DefaultAccountManager) ListSetupKeys(accountID string) ([]*SetupKey, error) {
func (am *DefaultAccountManager) ListSetupKeys(accountID, userID string) ([]*SetupKey, error) {
am.mux.Lock()
defer am.mux.Unlock()
account, err := am.Store.GetAccount(accountID)
@@ -246,16 +256,27 @@ func (am *DefaultAccountManager) ListSetupKeys(accountID string) ([]*SetupKey, e
return nil, status.Errorf(codes.NotFound, "account not found")
}
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
keys := make([]*SetupKey, 0, len(account.SetupKeys))
for _, key := range account.SetupKeys {
keys = append(keys, key.Copy())
var k *SetupKey
if !user.IsAdmin() {
k = key.HiddenCopy()
} else {
k = key.Copy()
}
keys = append(keys, k)
}
return keys, nil
}
// GetSetupKey looks up a SetupKey by KeyID, returns NotFound error if not found.
func (am *DefaultAccountManager) GetSetupKey(accountID, keyID string) (*SetupKey, error) {
func (am *DefaultAccountManager) GetSetupKey(accountID, userID, keyID string) (*SetupKey, error) {
am.mux.Lock()
defer am.mux.Unlock()
@@ -264,6 +285,11 @@ func (am *DefaultAccountManager) GetSetupKey(accountID, keyID string) (*SetupKey
return nil, status.Errorf(codes.NotFound, "account not found")
}
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
var foundKey *SetupKey
for _, key := range account.SetupKeys {
if key.Id == keyID {
@@ -280,5 +306,9 @@ func (am *DefaultAccountManager) GetSetupKey(accountID, keyID string) (*SetupKey
foundKey.UpdatedAt = foundKey.CreatedAt
}
if !user.IsAdmin() {
foundKey = foundKey.HiddenCopy()
}
return foundKey, nil
}

View File

@@ -1,26 +1,13 @@
package server
import (
"github.com/netbirdio/netbird/route"
"net/netip"
)
type Store interface {
GetPeer(peerKey string) (*Peer, error)
DeletePeer(accountId string, peerKey string) (*Peer, error)
SavePeer(accountId string, peer *Peer) error
GetAllAccounts() []*Account
GetAccount(accountId string) (*Account, error)
GetUserAccount(userId string) (*Account, error)
GetAccountPeers(accountId string) ([]*Peer, error)
GetPeerAccount(peerKey string) (*Account, error)
GetPeerSrcRules(accountId, peerKey string) ([]*Rule, error)
GetPeerDstRules(accountId, peerKey string) ([]*Rule, error)
GetAccountBySetupKey(setupKey string) (*Account, error)
GetAccount(accountID string) (*Account, error)
GetAccountByUser(userID string) (*Account, error)
GetAccountByPeerPubKey(peerKey string) (*Account, error)
GetAccountBySetupKey(setupKey string) (*Account, error) //todo use key hash later
GetAccountByPrivateDomain(domain string) (*Account, error)
SaveAccount(account *Account) error
GetPeerRoutes(peerKey string) ([]*route.Route, error)
GetRoutesByPrefix(accountID string, prefix netip.Prefix) ([]*route.Route, error)
GetInstallationID() string
SaveInstallationID(id string) error
}

View File

@@ -0,0 +1,181 @@
package telemetry
import (
"context"
"fmt"
"github.com/gorilla/mux"
prometheus2 "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/exporters/prometheus"
metric2 "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk/metric"
"net"
"net/http"
"reflect"
)
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
}
// GetMeter mocks the GetMeter function of the AppMetrics interface
func (mock *MockAppMetrics) GetMeter() metric2.Meter {
if mock.GetMeterFunc != nil {
return mock.GetMeterFunc()
}
return nil
}
// Close mocks the Close function of the AppMetrics interface
func (mock *MockAppMetrics) Close() error {
if mock.CloseFunc != nil {
return mock.CloseFunc()
}
return fmt.Errorf("unimplemented")
}
// Expose mocks the Expose function of the AppMetrics interface
func (mock *MockAppMetrics) Expose(port int, endpoint string) error {
if mock.ExposeFunc != nil {
return mock.ExposeFunc(port, endpoint)
}
return fmt.Errorf("unimplemented")
}
// IDPMetrics mocks the IDPMetrics function of the IDPMetrics interface
func (mock *MockAppMetrics) IDPMetrics() *IDPMetrics {
if mock.IDPMetricsFunc != nil {
return mock.IDPMetricsFunc()
}
return nil
}
// HTTPMiddleware mocks the HTTPMiddleware function of the IDPMetrics interface
func (mock *MockAppMetrics) HTTPMiddleware() *HTTPMiddleware {
if mock.HTTPMiddlewareFunc != nil {
return mock.HTTPMiddlewareFunc()
}
return nil
}
// GRPCMetrics mocks the GRPCMetrics function of the IDPMetrics interface
func (mock *MockAppMetrics) GRPCMetrics() *GRPCMetrics {
if mock.GRPCMetricsFunc != nil {
return mock.GRPCMetricsFunc()
}
return nil
}
// AppMetrics is metrics interface
type AppMetrics interface {
GetMeter() metric2.Meter
Close() error
Expose(port int, endpoint string) error
IDPMetrics() *IDPMetrics
HTTPMiddleware() *HTTPMiddleware
GRPCMetrics() *GRPCMetrics
}
// 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
}
// IDPMetrics returns metrics for the idp package
func (appMetrics *defaultAppMetrics) IDPMetrics() *IDPMetrics {
return appMetrics.idpMetrics
}
// HTTPMiddleware returns metrics for the http api package
func (appMetrics *defaultAppMetrics) HTTPMiddleware() *HTTPMiddleware {
return appMetrics.httpMiddleware
}
// GRPCMetrics returns metrics for the gRPC api
func (appMetrics *defaultAppMetrics) GRPCMetrics() *GRPCMetrics {
return appMetrics.grpcMetrics
}
// Close stop application metrics HTTP handler and closes listener.
func (appMetrics *defaultAppMetrics) Close() error {
if appMetrics.listener == nil {
return nil
}
return appMetrics.listener.Close()
}
// Expose metrics on a given port and endpoint. If endpoint is empty a defaultEndpoint one will be used.
// Exposes metrics in the Prometheus format https://prometheus.io/
func (appMetrics *defaultAppMetrics) Expose(port int, endpoint string) error {
if endpoint == "" {
endpoint = defaultEndpoint
}
rootRouter := mux.NewRouter()
rootRouter.Handle(endpoint, promhttp.HandlerFor(
prometheus2.DefaultGatherer,
promhttp.HandlerOpts{EnableOpenMetrics: true}))
listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", port))
if err != nil {
return err
}
appMetrics.listener = listener
go func() {
err := http.Serve(listener, rootRouter)
if err != nil {
return
}
}()
log.Infof("enabled application metrics and exposing on http://%s", listener.Addr().String())
return nil
}
// GetMeter returns metrics meter that can be used to add various counters
func (appMetrics *defaultAppMetrics) GetMeter() metric2.Meter {
return appMetrics.Meter
}
// NewDefaultAppMetrics and expose them via defaultEndpoint on a given HTTP port
func NewDefaultAppMetrics(ctx context.Context) (AppMetrics, error) {
exporter, err := prometheus.New()
if err != nil {
return nil, err
}
provider := metric.NewMeterProvider(metric.WithReader(exporter))
pkg := reflect.TypeOf(defaultEndpoint).PkgPath()
meter := provider.Meter(pkg)
idpMetrics, err := NewIDPMetrics(ctx, meter)
if err != nil {
return nil, err
}
middleware, err := NewMetricsMiddleware(ctx, meter)
if err != nil {
return nil, err
}
grpcMetrics, err := NewGRPCMetrics(ctx, meter)
if err != nil {
return nil, err
}
return &defaultAppMetrics{Meter: meter, ctx: ctx, idpMetrics: idpMetrics, httpMiddleware: middleware,
grpcMetrics: grpcMetrics}, nil
}

View File

@@ -0,0 +1,76 @@
package telemetry
import (
"context"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/instrument"
"go.opentelemetry.io/otel/metric/instrument/asyncint64"
"go.opentelemetry.io/otel/metric/instrument/syncint64"
)
// GRPCMetrics are gRPC server metrics
type GRPCMetrics struct {
meter metric.Meter
syncRequestsCounter syncint64.Counter
loginRequestsCounter syncint64.Counter
getKeyRequestsCounter syncint64.Counter
activeStreamsGauge asyncint64.Gauge
ctx context.Context
}
// NewGRPCMetrics creates new GRPCMetrics struct and registers common metrics of the gRPC server
func NewGRPCMetrics(ctx context.Context, meter metric.Meter) (*GRPCMetrics, error) {
syncRequestsCounter, err := meter.SyncInt64().Counter("management.grpc.sync.request.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
loginRequestsCounter, err := meter.SyncInt64().Counter("management.grpc.login.request.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
getKeyRequestsCounter, err := meter.SyncInt64().Counter("management.grpc.key.request.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
activeStreamsGauge, err := meter.AsyncInt64().Gauge("management.grpc.connected.streams", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
return &GRPCMetrics{
meter: meter,
syncRequestsCounter: syncRequestsCounter,
loginRequestsCounter: loginRequestsCounter,
getKeyRequestsCounter: getKeyRequestsCounter,
activeStreamsGauge: activeStreamsGauge,
ctx: ctx,
}, err
}
// CountSyncRequest counts the number of gRPC sync requests coming to the gRPC API
func (grpcMetrics *GRPCMetrics) CountSyncRequest() {
grpcMetrics.syncRequestsCounter.Add(grpcMetrics.ctx, 1)
}
// CountGetKeyRequest counts the number of gRPC get server key requests coming to the gRPC API
func (grpcMetrics *GRPCMetrics) CountGetKeyRequest() {
grpcMetrics.getKeyRequestsCounter.Add(grpcMetrics.ctx, 1)
}
// CountLoginRequest counts the number of gRPC login requests coming to the gRPC API
func (grpcMetrics *GRPCMetrics) CountLoginRequest() {
grpcMetrics.loginRequestsCounter.Add(grpcMetrics.ctx, 1)
}
// RegisterConnectedStreams registers a function that collects number of active streams and feeds it to the metrics gauge.
func (grpcMetrics *GRPCMetrics) RegisterConnectedStreams(producer func() int64) error {
return grpcMetrics.meter.RegisterCallback(
[]instrument.Asynchronous{
grpcMetrics.activeStreamsGauge,
},
func(ctx context.Context) {
grpcMetrics.activeStreamsGauge.Observe(ctx, producer())
},
)
}

View File

@@ -0,0 +1,176 @@
package telemetry
import (
"context"
"fmt"
log "github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/instrument"
"go.opentelemetry.io/otel/metric/instrument/syncint64"
"hash/fnv"
"net/http"
"strings"
)
const (
httpRequestCounterPrefix = "management.http.request.counter"
httpResponseCounterPrefix = "management.http.response.counter"
)
// WrappedResponseWriter is a wrapper for http.ResponseWriter that allows the
// written HTTP status code to be captured for metrics reporting or logging purposes.
type WrappedResponseWriter struct {
http.ResponseWriter
status int
wroteHeader bool
}
// WrapResponseWriter wraps original http.ResponseWriter
func WrapResponseWriter(w http.ResponseWriter) *WrappedResponseWriter {
return &WrappedResponseWriter{ResponseWriter: w}
}
// Status returns response status
func (rw *WrappedResponseWriter) Status() int {
return rw.status
}
// WriteHeader wraps http.ResponseWriter.WriteHeader method
func (rw *WrappedResponseWriter) WriteHeader(code int) {
if rw.wroteHeader {
return
}
rw.status = code
rw.ResponseWriter.WriteHeader(code)
rw.wroteHeader = true
}
// HTTPMiddleware handler used to collect metrics of every request/response coming to the API.
// Also adds request tracing (logging).
type HTTPMiddleware struct {
meter metric.Meter
ctx context.Context
// defaultEndpoint & method
httpRequestCounters map[string]syncint64.Counter
// defaultEndpoint & method & status code
httpResponseCounters map[string]syncint64.Counter
// all HTTP requests
totalHTTPRequestsCounter syncint64.Counter
// all HTTP responses
totalHTTPResponseCounter syncint64.Counter
// all HTTP responses by status code
totalHTTPResponseCodeCounters map[int]syncint64.Counter
}
// AddHTTPRequestResponseCounter adds a new meter for an HTTP defaultEndpoint and Method (GET, POST, etc)
// Creates one request counter and multiple response counters (one per http response status code).
func (m *HTTPMiddleware) AddHTTPRequestResponseCounter(endpoint string, method string) error {
meterKey := getRequestCounterKey(endpoint, method)
httpReqCounter, err := m.meter.SyncInt64().Counter(meterKey, instrument.WithUnit("1"))
if err != nil {
return err
}
m.httpRequestCounters[meterKey] = httpReqCounter
respCodes := []int{200, 204, 400, 401, 403, 404, 500, 502, 503}
for _, code := range respCodes {
meterKey = getResponseCounterKey(endpoint, method, code)
httpRespCounter, err := m.meter.SyncInt64().Counter(meterKey, instrument.WithUnit("1"))
if err != nil {
return err
}
m.httpResponseCounters[meterKey] = httpRespCounter
meterKey = fmt.Sprintf("%s_%d_total", httpResponseCounterPrefix, code)
totalHTTPResponseCodeCounter, err := m.meter.SyncInt64().Counter(meterKey, instrument.WithUnit("1"))
if err != nil {
return err
}
m.totalHTTPResponseCodeCounters[code] = totalHTTPResponseCodeCounter
}
return nil
}
// NewMetricsMiddleware creates a new HTTPMiddleware
func NewMetricsMiddleware(ctx context.Context, meter metric.Meter) (*HTTPMiddleware, error) {
totalHTTPRequestsCounter, err := meter.SyncInt64().Counter(
fmt.Sprintf("%s_total", httpRequestCounterPrefix),
instrument.WithUnit("1"))
if err != nil {
return nil, err
}
totalHTTPResponseCounter, err := meter.SyncInt64().Counter(
fmt.Sprintf("%s_total", httpResponseCounterPrefix),
instrument.WithUnit("1"))
if err != nil {
return nil, err
}
return &HTTPMiddleware{
ctx: ctx,
httpRequestCounters: map[string]syncint64.Counter{},
httpResponseCounters: map[string]syncint64.Counter{},
totalHTTPResponseCodeCounters: map[int]syncint64.Counter{},
meter: meter,
totalHTTPRequestsCounter: totalHTTPRequestsCounter,
totalHTTPResponseCounter: totalHTTPResponseCounter,
},
nil
}
func getRequestCounterKey(endpoint, method string) string {
return fmt.Sprintf("%s%s_%s", httpRequestCounterPrefix,
strings.ReplaceAll(endpoint, "/", "_"), method)
}
func getResponseCounterKey(endpoint, method string, status int) string {
return fmt.Sprintf("%s%s_%s_%d", httpResponseCounterPrefix,
strings.ReplaceAll(endpoint, "/", "_"), method, status)
}
// Handler logs every request and response and adds the, to metrics.
func (m *HTTPMiddleware) Handler(h http.Handler) http.Handler {
fn := func(rw http.ResponseWriter, r *http.Request) {
traceID := hash(fmt.Sprintf("%v", r))
log.Tracef("HTTP request %v: %v %v", traceID, r.Method, r.URL)
metricKey := getRequestCounterKey(r.URL.Path, r.Method)
if c, ok := m.httpRequestCounters[metricKey]; ok {
c.Add(m.ctx, 1)
}
m.totalHTTPRequestsCounter.Add(m.ctx, 1)
w := WrapResponseWriter(rw)
h.ServeHTTP(w, r)
if w.Status() > 399 {
log.Errorf("HTTP response %v: %v %v status %v", traceID, r.Method, r.URL, w.Status())
} else {
log.Tracef("HTTP response %v: %v %v status %v", traceID, r.Method, r.URL, w.Status())
}
metricKey = getResponseCounterKey(r.URL.Path, r.Method, w.Status())
if c, ok := m.httpResponseCounters[metricKey]; ok {
c.Add(m.ctx, 1)
}
m.totalHTTPResponseCounter.Add(m.ctx, 1)
if c, ok := m.totalHTTPResponseCodeCounters[w.Status()]; ok {
c.Add(m.ctx, 1)
}
}
return http.HandlerFunc(fn)
}
func hash(s string) uint32 {
h := fnv.New32a()
_, err := h.Write([]byte(s))
if err != nil {
panic(err)
}
return h.Sum32()
}

View File

@@ -0,0 +1,119 @@
package telemetry
import (
"context"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/instrument"
"go.opentelemetry.io/otel/metric/instrument/syncint64"
)
// IDPMetrics is common IdP metrics
type IDPMetrics struct {
metaUpdateCounter syncint64.Counter
getUserByEmailCounter syncint64.Counter
getAllAccountsCounter syncint64.Counter
createUserCounter syncint64.Counter
getAccountCounter syncint64.Counter
getUserByIDCounter syncint64.Counter
authenticateRequestCounter syncint64.Counter
requestErrorCounter syncint64.Counter
requestStatusErrorCounter syncint64.Counter
ctx context.Context
}
// NewIDPMetrics creates new IDPMetrics struct and registers common
func NewIDPMetrics(ctx context.Context, meter metric.Meter) (*IDPMetrics, error) {
metaUpdateCounter, err := meter.SyncInt64().Counter("management.idp.update.user.meta.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
getUserByEmailCounter, err := meter.SyncInt64().Counter("management.idp.get.user.by.email.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
getAllAccountsCounter, err := meter.SyncInt64().Counter("management.idp.get.accounts.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
createUserCounter, err := meter.SyncInt64().Counter("management.idp.create.user.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
getAccountCounter, err := meter.SyncInt64().Counter("management.idp.get.account.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
getUserByIDCounter, err := meter.SyncInt64().Counter("management.idp.get.user.by.id.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
authenticateRequestCounter, err := meter.SyncInt64().Counter("management.idp.authenticate.request.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
requestErrorCounter, err := meter.SyncInt64().Counter("management.idp.request.error.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
requestStatusErrorCounter, err := meter.SyncInt64().Counter("management.idp.request.status.error.counter", instrument.WithUnit("1"))
if err != nil {
return nil, err
}
return &IDPMetrics{
metaUpdateCounter: metaUpdateCounter,
getUserByEmailCounter: getUserByEmailCounter,
getAllAccountsCounter: getAllAccountsCounter,
createUserCounter: createUserCounter,
getAccountCounter: getAccountCounter,
getUserByIDCounter: getUserByIDCounter,
authenticateRequestCounter: authenticateRequestCounter,
requestErrorCounter: requestErrorCounter,
requestStatusErrorCounter: requestStatusErrorCounter,
ctx: ctx}, nil
}
// CountUpdateUserAppMetadata ...
func (idpMetrics *IDPMetrics) CountUpdateUserAppMetadata() {
idpMetrics.metaUpdateCounter.Add(idpMetrics.ctx, 1)
}
// CountGetUserByEmail ...
func (idpMetrics *IDPMetrics) CountGetUserByEmail() {
idpMetrics.getUserByEmailCounter.Add(idpMetrics.ctx, 1)
}
// CountCreateUser ...
func (idpMetrics *IDPMetrics) CountCreateUser() {
idpMetrics.createUserCounter.Add(idpMetrics.ctx, 1)
}
// CountGetAllAccounts ...
func (idpMetrics *IDPMetrics) CountGetAllAccounts() {
idpMetrics.getAllAccountsCounter.Add(idpMetrics.ctx, 1)
}
// CountGetAccount ...
func (idpMetrics *IDPMetrics) CountGetAccount() {
idpMetrics.getAccountCounter.Add(idpMetrics.ctx, 1)
}
// CountGetUserDataByID ...
func (idpMetrics *IDPMetrics) CountGetUserDataByID() {
idpMetrics.getUserByIDCounter.Add(idpMetrics.ctx, 1)
}
// CountAuthenticate ...
func (idpMetrics *IDPMetrics) CountAuthenticate() {
idpMetrics.authenticateRequestCounter.Add(idpMetrics.ctx, 1)
}
// CountRequestError counts number of error that happened when doing http request (httpClient.Do)
func (idpMetrics *IDPMetrics) CountRequestError() {
idpMetrics.requestErrorCounter.Add(idpMetrics.ctx, 1)
}
// CountRequestStatusError counts number of responses that came from IdP with non success status code
func (idpMetrics *IDPMetrics) CountRequestStatusError() {
idpMetrics.requestStatusErrorCounter.Add(idpMetrics.ctx, 1)
}

View File

@@ -46,6 +46,11 @@ type User struct {
AutoGroups []string
}
// IsAdmin returns true if user is an admin, false otherwise
func (u *User) IsAdmin() bool {
return u.Role == UserRoleAdmin
}
// toUserInfo converts a User object to a UserInfo object.
func (u *User) toUserInfo(userData *idp.UserData) (*UserInfo, error) {
autoGroups := u.AutoGroups
@@ -68,7 +73,7 @@ func (u *User) toUserInfo(userData *idp.UserData) (*UserInfo, error) {
}
userStatus := UserStatusActive
if userData.AppMetadata.WTPendingInvite {
if userData.AppMetadata.WTPendingInvite != nil && *userData.AppMetadata.WTPendingInvite {
userStatus = UserStatusInvited
}
@@ -235,7 +240,7 @@ func (am *DefaultAccountManager) GetOrCreateAccountByUser(userId, domain string)
lowerDomain := strings.ToLower(domain)
account, err := am.Store.GetUserAccount(userId)
account, err := am.Store.GetAccountByUser(userId)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
account, err = am.newAccount(userId, lowerDomain)
@@ -270,7 +275,7 @@ func (am *DefaultAccountManager) GetAccountByUser(userId string) (*Account, erro
am.mux.Lock()
defer am.mux.Unlock()
return am.Store.GetUserAccount(userId)
return am.Store.GetAccountByUser(userId)
}
// IsUserAdmin flag for current user authenticated by JWT token
@@ -288,13 +293,19 @@ func (am *DefaultAccountManager) IsUserAdmin(claims jwtclaims.AuthorizationClaim
return user.Role == UserRoleAdmin, nil
}
// GetUsersFromAccount performs a batched request for users from IDP by account ID
func (am *DefaultAccountManager) GetUsersFromAccount(accountID string) ([]*UserInfo, error) {
// GetUsersFromAccount performs a batched request for users from IDP by account ID apply filter on what data to return
// based on provided user role.
func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error) {
account, err := am.GetAccountById(accountID)
if err != nil {
return nil, err
}
user, err := account.FindUser(userID)
if err != nil {
return nil, err
}
queriedUsers := make([]*idp.UserData, 0)
if !isNil(am.idpManager) {
users := make(map[string]struct{}, len(account.Users))
@@ -311,8 +322,12 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID string) ([]*UserI
// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
if len(queriedUsers) == 0 {
for _, user := range account.Users {
info, err := user.toUserInfo(nil)
for _, accountUser := range account.Users {
if !user.IsAdmin() && user.Id != accountUser.Id {
// if user is not an admin then show only current user and do not show other users
continue
}
info, err := accountUser.toUserInfo(nil)
if err != nil {
return nil, err
}
@@ -322,6 +337,10 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID string) ([]*UserI
}
for _, queriedUser := range queriedUsers {
if !user.IsAdmin() && user.Id != queriedUser.ID {
// if user is not an admin then show only current user and do not show other users
continue
}
if localUser, contains := account.Users[queriedUser.ID]; contains {
info, err := localUser.toUserInfo(queriedUser)

View File

@@ -3,7 +3,6 @@ package util
import (
"encoding/json"
"io"
"io/ioutil"
"os"
"path/filepath"
)
@@ -24,7 +23,7 @@ func WriteJson(file string, obj interface{}) error {
return err
}
tempFile, err := ioutil.TempFile(configDir, ".*"+configFileName)
tempFile, err := os.CreateTemp(configDir, ".*"+configFileName)
if err != nil {
return err
}
@@ -43,7 +42,7 @@ func WriteJson(file string, obj interface{}) error {
}
}()
err = ioutil.WriteFile(tempFileName, bs, 0600)
err = os.WriteFile(tempFileName, bs, 0600)
if err != nil {
return err
}
@@ -65,7 +64,7 @@ func ReadJson(file string, res interface{}) (interface{}, error) {
}
defer f.Close()
bs, err := ioutil.ReadAll(f)
bs, err := io.ReadAll(f)
if err != nil {
return nil, err
}