package server import ( "context" "crypto/sha256" "net" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/management/server/group" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/util" ) type accounts struct { Accounts map[string]*Account } func TestStalePeerIndices(t *testing.T) { storeDir := t.TempDir() err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json")) if err != nil { t.Fatal(err) } store, err := NewFileStore(context.Background(), storeDir, nil) if err != nil { return } account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") require.NoError(t, err) peerID := "some_peer" peerKey := "some_peer_key" account.Peers[peerID] = &nbpeer.Peer{ ID: peerID, Key: peerKey, } err = store.SaveAccount(context.Background(), account) require.NoError(t, err) account.DeletePeer(peerID) err = store.SaveAccount(context.Background(), account) require.NoError(t, err) _, err = store.GetAccountByPeerID(context.Background(), peerID) require.Error(t, err, "expecting to get an error when found stale index") _, err = store.GetAccountByPeerPubKey(context.Background(), peerKey) require.Error(t, err, "expecting to get an error when found stale index") } func TestNewStore(t *testing.T) { store := newStore(t) defer store.Close(context.Background()) if store.Accounts == nil || len(store.Accounts) != 0 { 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.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.HashedPAT2TokenID == nil || len(store.HashedPAT2TokenID) != 0 { t.Errorf("expected to create a new empty HashedPAT2TokenID map when creating a new FileStore") } if store.TokenID2UserID == nil || len(store.TokenID2UserID) != 0 { t.Errorf("expected to create a new empty TokenID2UserID map when creating a new FileStore") } } func TestSaveAccount(t *testing.T) { store := newStore(t) defer store.Close(context.Background()) account := newAccountWithId(context.Background(), "account_id", "testuser", "") setupKey := GenerateDefaultSetupKey() account.SetupKeys[setupKey.Key] = setupKey account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } // SaveAccount should trigger persist err := store.SaveAccount(context.Background(), account) if err != nil { return } if store.Accounts[account.Id] == nil { t.Errorf("expecting Account to be stored 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.SetupKeyID2AccountID[setupKey.Key] == "" { t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount()") } } func TestDeleteAccount(t *testing.T) { storeDir := t.TempDir() storeFile := filepath.Join(storeDir, "store.json") err := util.CopyFileContents("testdata/store.json", storeFile) if err != nil { t.Fatal(err) } store, err := NewFileStore(context.Background(), storeDir, nil) if err != nil { t.Fatal(err) } defer store.Close(context.Background()) var account *Account for _, a := range store.Accounts { account = a break } require.NotNil(t, account, "failed to restore a FileStore file and get at least one account") err = store.DeleteAccount(context.Background(), account) require.NoError(t, err, "failed to delete account, error: %v", err) _, ok := store.Accounts[account.Id] require.False(t, ok, "failed to delete account") for id := range account.Users { _, ok := store.UserID2AccountID[id] assert.False(t, ok, "failed to delete UserID2AccountID index") for _, pat := range account.Users[id].PATs { _, ok := store.HashedPAT2TokenID[pat.HashedToken] assert.False(t, ok, "failed to delete HashedPAT2TokenID index") _, ok = store.TokenID2UserID[pat.ID] assert.False(t, ok, "failed to delete TokenID2UserID index") } } for _, p := range account.Peers { _, ok := store.PeerKeyID2AccountID[p.Key] assert.False(t, ok, "failed to delete PeerKeyID2AccountID index") _, ok = store.PeerID2AccountID[p.ID] assert.False(t, ok, "failed to delete PeerID2AccountID index") } for id := range account.SetupKeys { _, ok := store.SetupKeyID2AccountID[id] assert.False(t, ok, "failed to delete SetupKeyID2AccountID index") } _, ok = store.PrivateDomain2AccountID[account.Domain] assert.False(t, ok, "failed to delete PrivateDomain2AccountID index") } func TestStore(t *testing.T) { store := newStore(t) defer store.Close(context.Background()) account := newAccountWithId(context.Background(), "account_id", "testuser", "") account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } account.Groups["all"] = &group.Group{ ID: "all", Name: "all", Peers: []string{"testpeer"}, } account.Policies = append(account.Policies, &Policy{ ID: "all", Name: "all", Enabled: true, Rules: []*PolicyRule{ { ID: "all", Name: "all", Sources: []string{"all"}, Destinations: []string{"all"}, }, }, }) account.Policies = append(account.Policies, &Policy{ ID: "dmz", Name: "dmz", Enabled: true, Rules: []*PolicyRule{ { ID: "dmz", Name: "dmz", Enabled: true, Sources: []string{"all"}, Destinations: []string{"all"}, }, }, }) // SaveAccount should trigger persist err := store.SaveAccount(context.Background(), account) if err != nil { return } restored, err := NewFileStore(context.Background(), store.storeFile, nil) if err != nil { return } restoredAccount := restored.Accounts[account.Id] if restoredAccount == nil { t.Errorf("failed to restore a FileStore file - missing Account %s", account.Id) return } if restoredAccount.Peers["testpeer"] == nil { t.Errorf("failed to restore a FileStore file - missing Peer testpeer") } if restoredAccount.CreatedBy != "testuser" { t.Errorf("failed to restore a FileStore file - missing Account CreatedBy") } if restoredAccount.Users["testuser"] == nil { t.Errorf("failed to restore a FileStore file - missing User testuser") } if restoredAccount.Network == nil { t.Errorf("failed to restore a FileStore file - missing Network") } if restoredAccount.Groups["all"] == nil { t.Errorf("failed to restore a FileStore file - missing Group all") } if len(restoredAccount.Policies) != 2 { t.Errorf("failed to restore a FileStore file - missing Policies") return } assert.Equal(t, account.Policies[0], restoredAccount.Policies[0], "failed to restore a FileStore file - missing Policy all") assert.Equal(t, account.Policies[1], restoredAccount.Policies[1], "failed to restore a FileStore file - missing Policy dmz") } func TestRestore(t *testing.T) { storeDir := t.TempDir() err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json")) if err != nil { t.Fatal(err) } store, err := NewFileStore(context.Background(), storeDir, nil) if err != nil { return } account := store.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"] require.NotNil(t, account, "failed to restore a FileStore file - missing account bf1c8084-ba50-4ce7-9439-34653001fc3b") require.NotNil(t, account.Users["edafee4e-63fb-11ec-90d6-0242ac120003"], "failed to restore a FileStore file - missing Account User edafee4e-63fb-11ec-90d6-0242ac120003") require.NotNil(t, account.Users["f4f6d672-63fb-11ec-90d6-0242ac120003"], "failed to restore a FileStore file - missing Account User f4f6d672-63fb-11ec-90d6-0242ac120003") require.NotNil(t, account.Network, "failed to restore a FileStore file - missing Account Network") 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.NotNil(t, account.Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs["9dj38s35-63fb-11ec-90d6-0242ac120003"], "failed to restore a FileStore wrong PATs 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.PrivateDomain2AccountID, 1, "failed to restore a FileStore wrong PrivateDomain2AccountID mapping length") require.Len(t, store.HashedPAT2TokenID, 1, "failed to restore a FileStore wrong HashedPAT2TokenID mapping length") require.Len(t, store.TokenID2UserID, 1, "failed to restore a FileStore wrong TokenID2UserID mapping length") } func TestRestoreGroups_Migration(t *testing.T) { storeDir := t.TempDir() err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json")) if err != nil { t.Fatal(err) } store, err := NewFileStore(context.Background(), storeDir, nil) if err != nil { return } // create default group account := store.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"] account.Groups = map[string]*group.Group{ "cfefqs706sqkneg59g3g": { ID: "cfefqs706sqkneg59g3g", Name: "All", }, } err = store.SaveAccount(context.Background(), account) require.NoError(t, err, "failed to save account") // restore account with default group with empty Issue field if store, err = NewFileStore(context.Background(), storeDir, nil); err != nil { return } account = store.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"] require.Contains(t, account.Groups, "cfefqs706sqkneg59g3g", "failed to restore a FileStore file - missing Account Groups") require.Equal(t, group.GroupIssuedAPI, account.Groups["cfefqs706sqkneg59g3g"].Issued, "default group should has API issued mark") } func TestGetAccountByPrivateDomain(t *testing.T) { storeDir := t.TempDir() err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json")) if err != nil { t.Fatal(err) } store, err := NewFileStore(context.Background(), storeDir, nil) if err != nil { return } existingDomain := "test.com" account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) require.NoError(t, err, "should found account") require.Equal(t, existingDomain, account.Domain, "domains should match") _, err = store.GetAccountByPrivateDomain(context.Background(), "missing-domain.com") 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 := NewFileStore(context.Background(), storeDir, nil) if err != nil { t.Fatal(err) } expected := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"] if expected == nil { t.Fatalf("expected account doesn't exist") return } account, err := store.GetAccount(context.Background(), 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.Identifier, account.Network.Identifier) 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.Routes, len(expected.Routes)) assert.Len(t, account.NameServerGroups, len(expected.NameServerGroups)) } func TestFileStore_GetTokenIDByHashedToken(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 := NewFileStore(context.Background(), storeDir, nil) if err != nil { t.Fatal(err) } hashedToken := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"].Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs["9dj38s35-63fb-11ec-90d6-0242ac120003"].HashedToken tokenID, err := store.GetTokenIDByHashedToken(context.Background(), hashedToken) if err != nil { t.Fatal(err) } expectedTokenID := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"].Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs["9dj38s35-63fb-11ec-90d6-0242ac120003"].ID assert.Equal(t, expectedTokenID, tokenID) } func TestFileStore_DeleteHashedPAT2TokenIDIndex(t *testing.T) { store := newStore(t) defer store.Close(context.Background()) store.HashedPAT2TokenID["someHashedToken"] = "someTokenId" err := store.DeleteHashedPAT2TokenIDIndex("someHashedToken") if err != nil { t.Fatal(err) } assert.Empty(t, store.HashedPAT2TokenID["someHashedToken"]) } func TestFileStore_DeleteTokenID2UserIDIndex(t *testing.T) { store := newStore(t) store.TokenID2UserID["someTokenId"] = "someUserId" err := store.DeleteTokenID2UserIDIndex("someTokenId") if err != nil { t.Fatal(err) } assert.Empty(t, store.TokenID2UserID["someTokenId"]) } func TestFileStore_GetTokenIDByHashedToken_Failure(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 := NewFileStore(context.Background(), storeDir, nil) if err != nil { t.Fatal(err) } wrongToken := sha256.Sum256([]byte("someNotValidTokenThatFails1234")) _, err = store.GetTokenIDByHashedToken(context.Background(), string(wrongToken[:])) assert.Error(t, err, "GetTokenIDByHashedToken should throw error if token invalid") } func TestFileStore_GetUserByTokenID(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 := NewFileStore(context.Background(), storeDir, nil) if err != nil { t.Fatal(err) } tokenID := accounts.Accounts["bf1c8084-ba50-4ce7-9439-34653001fc3b"].Users["f4f6d672-63fb-11ec-90d6-0242ac120003"].PATs["9dj38s35-63fb-11ec-90d6-0242ac120003"].ID user, err := store.GetUserByTokenID(context.Background(), tokenID) if err != nil { t.Fatal(err) } assert.Equal(t, "f4f6d672-63fb-11ec-90d6-0242ac120003", user.Id) } func TestFileStore_GetUserByTokenID_Failure(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 := NewFileStore(context.Background(), storeDir, nil) if err != nil { t.Fatal(err) } wrongTokenID := "someNonExistingTokenID" _, err = store.GetUserByTokenID(context.Background(), wrongTokenID) assert.Error(t, err, "GetUserByTokenID should throw error if tokenID invalid") } func TestFileStore_SavePeerStatus(t *testing.T) { storeDir := t.TempDir() err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json")) if err != nil { t.Fatal(err) } store, err := NewFileStore(context.Background(), storeDir, nil) if err != nil { return } account, err := store.getAccount("bf1c8084-ba50-4ce7-9439-34653001fc3b") if err != nil { t.Fatal(err) } // save status of non-existing peer newStatus := nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()} err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus) assert.Error(t, err) // save new status of existing peer account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", ID: "testpeer", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, } err = store.SaveAccount(context.Background(), account) if err != nil { t.Fatal(err) } err = store.SavePeerStatus(account.Id, "testpeer", newStatus) if err != nil { t.Fatal(err) } account, err = store.getAccount(account.Id) if err != nil { t.Fatal(err) } actual := account.Peers["testpeer"].Status assert.Equal(t, newStatus, *actual) } func TestFileStore_SavePeerLocation(t *testing.T) { storeDir := t.TempDir() err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json")) if err != nil { t.Fatal(err) } store, err := NewFileStore(context.Background(), storeDir, nil) if err != nil { return } account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") require.NoError(t, err) peer := &nbpeer.Peer{ AccountID: account.Id, ID: "testpeer", Location: nbpeer.Location{ ConnectionIP: net.ParseIP("10.0.0.0"), CountryCode: "YY", CityName: "City", GeoNameID: 1, }, Meta: nbpeer.PeerSystemMeta{}, } // error is expected as peer is not in store yet err = store.SavePeerLocation(account.Id, peer) assert.Error(t, err) account.Peers[peer.ID] = peer err = store.SaveAccount(context.Background(), account) require.NoError(t, err) peer.Location.ConnectionIP = net.ParseIP("35.1.1.1") peer.Location.CountryCode = "DE" peer.Location.CityName = "Berlin" peer.Location.GeoNameID = 2950159 err = store.SavePeerLocation(account.Id, account.Peers[peer.ID]) assert.NoError(t, err) account, err = store.GetAccount(context.Background(), account.Id) require.NoError(t, err) actual := account.Peers[peer.ID].Location assert.Equal(t, peer.Location, actual) } func newStore(t *testing.T) *FileStore { t.Helper() store, err := NewFileStore(context.Background(), t.TempDir(), nil) if err != nil { t.Errorf("failed creating a new store") } return store }