mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-20 12:29:09 +02:00
[management] Add support for pagination in GetAllPeers call (#8190)
* use libopenapi OpenAPI generator Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * added a handler for v1alpha1/peers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * wire up request validator Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * wired up spec-based validation Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use sync validation; set base url to v1alpha1 Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * moved stuff around, extracted runtime libopenapu deps into runtime_tooling Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * testing /peers path params Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * fixed tests Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * added user schemas and paths Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use api validation in tests Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * post-merge fixes Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * deleted tmp command used to test validator integration Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * require request body in post/put requests in order to force validation Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * making linter happy Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * switch to api/v1alpha1 Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * go mod tidy Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * keep the original order of middleware: metrics, cors, then auth Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * return 422 on validation errors Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * more cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * v1alpha1 spec cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * no need for a double-pointer Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * more linter fixes Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * removed more double pointers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use di to inject api_v0 and api_v1 http routers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * do not re-add middleware on repeated call to ApiHandler Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * support for pagination and filtering in /peers call Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * calling ApiRouter() now also calls ApiV1Router() Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * adding peer filters Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * introduced paginated GetAllPeers call Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * wire up pagination Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * convert pagination state to query scope Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * peer filter wip Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * implemented peer filter Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * test for filtering by os Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * support for sorting of peers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * support for ordering + tests Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * more tests Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * wire up total number of records in paginated GetAllPeers call Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * cleaned up GetAllPeers response Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * pacify linter Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * cleanups and fixes Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * moar cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * pacify linter Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * remove duplicated test Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * make linter happy Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * moving GetAllPeersPaginated to peer manager, p. 1 Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * moving GetAllPeersPaginated to peer manager, p. 2 Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * added peer manager tests for GetPeersPaginated Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * order peers in GetAccountPeersPaginated by their ids by default Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * always order by peer_id, filter out proxies Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * fixed broken tests Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * support for filtering of peers by peer_id in GetAllPeers() Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * filtering by peer_ids Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use lowercase in 'like' queries, drop ordering by ip Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * drop sort_by ip key Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * drop forced_approve filter Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * replace mac with darwin in peer os filter Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * fix peers query parameter: use peer_ids Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * fixed broken peer_test Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * responded to feedback Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> --------- Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
This commit is contained in:
@@ -28,8 +28,8 @@ import (
|
||||
type Manager interface {
|
||||
GetPeer(ctx context.Context, accountID, userID, peerID string) (*peer.Peer, error)
|
||||
GetPeerAccountID(ctx context.Context, peerID string) (string, error)
|
||||
GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error)
|
||||
GetPeersByGroupIDs(ctx context.Context, accountID string, groupsIDs []string) ([]*peer.Peer, error)
|
||||
GetPeersPaginated(ctx context.Context, accountID, userID string, pagination store.PaginationState, filters store.PeerFilters, sorting store.PeerSorting) ([]*peer.Peer, int, error)
|
||||
DeletePeers(ctx context.Context, accountID string, peerIDs []string, userID string, checkConnected bool) error
|
||||
SetNetworkMapController(networkMapController network_map.Controller)
|
||||
SetIntegratedPeerValidator(integratedPeerValidator integrated_validator.IntegratedValidator)
|
||||
@@ -51,8 +51,7 @@ type managerImpl struct {
|
||||
permissionsManager permissions.Manager
|
||||
integratedPeerValidator integrated_validator.IntegratedValidator
|
||||
accountManager account.Manager
|
||||
|
||||
networkMapController network_map.Controller
|
||||
networkMapController network_map.Controller
|
||||
}
|
||||
|
||||
func NewManager(store store.Store, permissionsManager permissions.Manager) Manager {
|
||||
@@ -87,19 +86,6 @@ func (m *managerImpl) GetPeer(ctx context.Context, accountID, userID, peerID str
|
||||
return m.store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
||||
}
|
||||
|
||||
func (m *managerImpl) GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error) {
|
||||
allowed, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Read)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to validate user permissions: %w", err)
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
return m.store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID)
|
||||
}
|
||||
|
||||
return m.store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, "", "", "")
|
||||
}
|
||||
|
||||
func (m *managerImpl) GetPeerAccountID(ctx context.Context, peerID string) (string, error) {
|
||||
return m.store.GetAccountIDByPeerID(ctx, store.LockingStrengthNone, peerID)
|
||||
}
|
||||
@@ -108,6 +94,35 @@ func (m *managerImpl) GetPeersByGroupIDs(ctx context.Context, accountID string,
|
||||
return m.store.GetPeersByGroupIDs(ctx, accountID, groupsIDs)
|
||||
}
|
||||
|
||||
func (m *managerImpl) GetPeersPaginated(ctx context.Context, accountID, userID string, pagination store.PaginationState, filters store.PeerFilters, sorting store.PeerSorting) ([]*peer.Peer, int, error) {
|
||||
user, err := m.store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
allowed, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Read)
|
||||
if err != nil {
|
||||
return nil, 0, status.NewPermissionValidationError(err)
|
||||
}
|
||||
|
||||
if allowed {
|
||||
return m.store.GetAccountPeersPaginated(ctx, store.LockingStrengthNone, accountID, pagination, filters, sorting)
|
||||
}
|
||||
|
||||
settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get account settings: %w", err)
|
||||
}
|
||||
|
||||
if user.IsRestrictable() && settings.RegularUsersViewBlocked {
|
||||
return []*peer.Peer{}, 0, nil
|
||||
}
|
||||
|
||||
peers, err := m.store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID)
|
||||
|
||||
return peers, len(peers), err
|
||||
}
|
||||
|
||||
// GetPeerByTunnelIP delegates to the store's indexed lookup.
|
||||
func (m *managerImpl) GetPeerByTunnelIP(ctx context.Context, accountID string, ip net.IP) (*peer.Peer, error) {
|
||||
return m.store.GetPeerByIP(ctx, store.LockingStrengthNone, accountID, ip)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
account "github.com/netbirdio/netbird/management/server/account"
|
||||
integrated_validator "github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
|
||||
peer "github.com/netbirdio/netbird/management/server/peer"
|
||||
store "github.com/netbirdio/netbird/management/server/store"
|
||||
types "github.com/netbirdio/netbird/management/server/types"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
@@ -74,21 +75,6 @@ func (mr *MockManagerMockRecorder) DeletePeers(ctx, accountID, peerIDs, userID,
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePeers", reflect.TypeOf((*MockManager)(nil).DeletePeers), ctx, accountID, peerIDs, userID, checkConnected)
|
||||
}
|
||||
|
||||
// GetAllPeers mocks base method.
|
||||
func (m *MockManager) GetAllPeers(ctx context.Context, accountID, userID string) ([]*peer.Peer, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAllPeers", ctx, accountID, userID)
|
||||
ret0, _ := ret[0].([]*peer.Peer)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAllPeers indicates an expected call of GetAllPeers.
|
||||
func (mr *MockManagerMockRecorder) GetAllPeers(ctx, accountID, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllPeers", reflect.TypeOf((*MockManager)(nil).GetAllPeers), ctx, accountID, userID)
|
||||
}
|
||||
|
||||
// GetPeer mocks base method.
|
||||
func (m *MockManager) GetPeer(ctx context.Context, accountID, userID, peerID string) (*peer.Peer, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -180,6 +166,22 @@ func (mr *MockManagerMockRecorder) GetPeersByGroupIDs(ctx, accountID, groupsIDs
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeersByGroupIDs", reflect.TypeOf((*MockManager)(nil).GetPeersByGroupIDs), ctx, accountID, groupsIDs)
|
||||
}
|
||||
|
||||
// GetPeersPaginated mocks base method.
|
||||
func (m *MockManager) GetPeersPaginated(ctx context.Context, accountID, userID string, pagination store.PaginationState, filters store.PeerFilters, sorting store.PeerSorting) ([]*peer.Peer, int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPeersPaginated", ctx, accountID, userID, pagination, filters, sorting)
|
||||
ret0, _ := ret[0].([]*peer.Peer)
|
||||
ret1, _ := ret[1].(int)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// GetPeersPaginated indicates an expected call of GetPeersPaginated.
|
||||
func (mr *MockManagerMockRecorder) GetPeersPaginated(ctx, accountID, userID, pagination, filters, sorting any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeersPaginated", reflect.TypeOf((*MockManager)(nil).GetPeersPaginated), ctx, accountID, userID, pagination, filters, sorting)
|
||||
}
|
||||
|
||||
// SetAccountManager mocks base method.
|
||||
func (m *MockManager) SetAccountManager(accountManager account.Manager) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package peers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestManager_GetPeersPaginated_HappyPath(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
// GetUserByUserID
|
||||
// GetAccountPeersPaginated
|
||||
// GetAccountSettings
|
||||
// GetUserPeers
|
||||
mockStore := store.NewMockStore(ctrl)
|
||||
// ValidateUserPermissions
|
||||
permissionsManager := permissions.NewMockManager(ctrl)
|
||||
|
||||
m := NewManager(mockStore, permissionsManager)
|
||||
|
||||
mockStore.EXPECT().GetUserByUserID(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-user")).Times(1).
|
||||
Return(&types.User{
|
||||
Id: "test-user",
|
||||
}, nil)
|
||||
permissionsManager.EXPECT().ValidateUserPermissions(gomock.Any(), gomock.Eq("test-account"), gomock.Eq("test-user"), gomock.Eq(modules.Peers), gomock.Eq(operations.Read)).Times(1).
|
||||
Return(true, context.Background(), nil)
|
||||
mockStore.EXPECT().GetAccountPeersPaginated(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-account"),
|
||||
gomock.Eq(store.PaginationState{Page: 11, PageSize: 111}), gomock.Eq(store.PeerFilters{UserId: "test-user"}),
|
||||
gomock.Eq(store.PeerSorting{Fields: []store.PeerSortingKey{{Field: store.PeerSortByName, Order: store.DescOrderBy}}})).Times(1).
|
||||
Return([]*peer.Peer{{ID: "test-peer"}}, 10, nil)
|
||||
|
||||
peers, total, err := m.GetPeersPaginated(context.Background(), "test-account", "test-user",
|
||||
store.PaginationState{Page: 11, PageSize: 111}, store.PeerFilters{UserId: "test-user"},
|
||||
store.PeerSorting{Fields: []store.PeerSortingKey{{Field: store.PeerSortByName, Order: store.DescOrderBy}}})
|
||||
|
||||
assert.Equal(t, []*peer.Peer{{ID: "test-peer"}}, peers)
|
||||
assert.Equal(t, 10, total)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestManager_GetPeersPaginated_GetUserWithError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := store.NewMockStore(ctrl)
|
||||
permissionsManager := permissions.NewMockManager(ctrl)
|
||||
|
||||
m := NewManager(mockStore, permissionsManager)
|
||||
|
||||
mockStore.EXPECT().GetUserByUserID(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-user")).Times(1).
|
||||
Return(nil, fmt.Errorf("boom!"))
|
||||
|
||||
_, total, err := m.GetPeersPaginated(context.Background(), "test-account", "test-user",
|
||||
store.PaginationState{Page: 11, PageSize: 111}, store.PeerFilters{UserId: "test-user"},
|
||||
store.PeerSorting{Fields: []store.PeerSortingKey{{Field: store.PeerSortByName, Order: store.DescOrderBy}}})
|
||||
|
||||
assert.Equal(t, 0, total)
|
||||
assert.ErrorContains(t, err, "boom!")
|
||||
}
|
||||
|
||||
func TestManager_GetPeersPaginated_NoPermission(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := store.NewMockStore(ctrl)
|
||||
permissionsManager := permissions.NewMockManager(ctrl)
|
||||
|
||||
m := NewManager(mockStore, permissionsManager)
|
||||
|
||||
mockStore.EXPECT().GetUserByUserID(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-user")).Times(1).
|
||||
Return(&types.User{
|
||||
Id: "test-user",
|
||||
}, nil)
|
||||
permissionsManager.EXPECT().ValidateUserPermissions(gomock.Any(), gomock.Eq("test-account"), gomock.Eq("test-user"), gomock.Eq(modules.Peers), gomock.Eq(operations.Read)).Times(1).
|
||||
Return(false, context.Background(), nil)
|
||||
mockStore.EXPECT().GetAccountSettings(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-account")).Times(1).
|
||||
Return(&types.Settings{RegularUsersViewBlocked: false}, nil)
|
||||
mockStore.EXPECT().GetUserPeers(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-account"), gomock.Eq("test-user")).Times(1).
|
||||
Return([]*peer.Peer{{ID: "test-peer"}}, nil)
|
||||
|
||||
peers, total, err := m.GetPeersPaginated(context.Background(), "test-account", "test-user",
|
||||
store.PaginationState{Page: 11, PageSize: 111}, store.PeerFilters{UserId: "test-user"},
|
||||
store.PeerSorting{Fields: []store.PeerSortingKey{{Field: store.PeerSortByName, Order: store.DescOrderBy}}})
|
||||
|
||||
assert.Equal(t, []*peer.Peer{{ID: "test-peer"}}, peers)
|
||||
assert.Equal(t, 1, total)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestManager_GetPeersPaginated_NoPermission_GetUserPeers_WithError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := store.NewMockStore(ctrl)
|
||||
permissionsManager := permissions.NewMockManager(ctrl)
|
||||
|
||||
m := NewManager(mockStore, permissionsManager)
|
||||
|
||||
mockStore.EXPECT().GetUserByUserID(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-user")).Times(1).
|
||||
Return(&types.User{
|
||||
Id: "test-user",
|
||||
}, nil)
|
||||
permissionsManager.EXPECT().ValidateUserPermissions(gomock.Any(), gomock.Eq("test-account"), gomock.Eq("test-user"), gomock.Eq(modules.Peers), gomock.Eq(operations.Read)).Times(1).
|
||||
Return(false, context.Background(), nil)
|
||||
mockStore.EXPECT().GetAccountSettings(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-account")).Times(1).
|
||||
Return(&types.Settings{RegularUsersViewBlocked: false}, nil)
|
||||
mockStore.EXPECT().GetUserPeers(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-account"), gomock.Eq("test-user")).Times(1).
|
||||
Return(nil, fmt.Errorf("boom!"))
|
||||
|
||||
_, total, err := m.GetPeersPaginated(context.Background(), "test-account", "test-user",
|
||||
store.PaginationState{Page: 11, PageSize: 111}, store.PeerFilters{UserId: "test-user"},
|
||||
store.PeerSorting{Fields: []store.PeerSortingKey{{Field: store.PeerSortByName, Order: store.DescOrderBy}}})
|
||||
|
||||
assert.Equal(t, 0, total)
|
||||
assert.ErrorContains(t, err, "boom!")
|
||||
}
|
||||
|
||||
func TestManager_GetPeersPaginated_GetAccountSettings_WithError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := store.NewMockStore(ctrl)
|
||||
permissionsManager := permissions.NewMockManager(ctrl)
|
||||
|
||||
m := NewManager(mockStore, permissionsManager)
|
||||
|
||||
mockStore.EXPECT().GetUserByUserID(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-user")).Times(1).
|
||||
Return(&types.User{
|
||||
Id: "test-user",
|
||||
}, nil)
|
||||
permissionsManager.EXPECT().ValidateUserPermissions(gomock.Any(), gomock.Eq("test-account"), gomock.Eq("test-user"), gomock.Eq(modules.Peers), gomock.Eq(operations.Read)).Times(1).
|
||||
Return(false, context.Background(), nil)
|
||||
mockStore.EXPECT().GetAccountSettings(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-account")).Times(1).
|
||||
Return(nil, fmt.Errorf("boom!"))
|
||||
|
||||
_, total, err := m.GetPeersPaginated(context.Background(), "test-account", "test-user",
|
||||
store.PaginationState{Page: 11, PageSize: 111}, store.PeerFilters{UserId: "test-user"},
|
||||
store.PeerSorting{Fields: []store.PeerSortingKey{{Field: store.PeerSortByName, Order: store.DescOrderBy}}})
|
||||
|
||||
assert.Equal(t, 0, total)
|
||||
assert.ErrorContains(t, err, "boom!")
|
||||
}
|
||||
func TestManager_GetPeersPaginated_NoPermission_User_IsRestrictable_ViewUnblocked(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := store.NewMockStore(ctrl)
|
||||
permissionsManager := permissions.NewMockManager(ctrl)
|
||||
|
||||
m := NewManager(mockStore, permissionsManager)
|
||||
|
||||
mockStore.EXPECT().GetUserByUserID(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-user")).Times(1).
|
||||
Return(&types.User{
|
||||
Id: "test-user",
|
||||
Role: types.UserRoleUser,
|
||||
}, nil)
|
||||
permissionsManager.EXPECT().ValidateUserPermissions(gomock.Any(), gomock.Eq("test-account"), gomock.Eq("test-user"), gomock.Eq(modules.Peers), gomock.Eq(operations.Read)).Times(1).
|
||||
Return(false, context.Background(), nil)
|
||||
mockStore.EXPECT().GetAccountSettings(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-account")).Times(1).
|
||||
Return(&types.Settings{RegularUsersViewBlocked: false}, nil)
|
||||
mockStore.EXPECT().GetUserPeers(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-account"), gomock.Eq("test-user")).Times(1).
|
||||
Return([]*peer.Peer{{ID: "test-peer"}}, nil)
|
||||
|
||||
peers, total, err := m.GetPeersPaginated(context.Background(), "test-account", "test-user",
|
||||
store.PaginationState{Page: 11, PageSize: 111}, store.PeerFilters{UserId: "test-user"},
|
||||
store.PeerSorting{Fields: []store.PeerSortingKey{{Field: store.PeerSortByName, Order: store.DescOrderBy}}})
|
||||
|
||||
assert.Equal(t, []*peer.Peer{{ID: "test-peer"}}, peers)
|
||||
assert.Equal(t, 1, total)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestManager_GetPeersPaginated_NoPermission_User_IsRestrictable_ViewBlocked(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockStore := store.NewMockStore(ctrl)
|
||||
permissionsManager := permissions.NewMockManager(ctrl)
|
||||
|
||||
m := NewManager(mockStore, permissionsManager)
|
||||
|
||||
mockStore.EXPECT().GetUserByUserID(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-user")).Times(1).
|
||||
Return(&types.User{
|
||||
Id: "test-user",
|
||||
Role: types.UserRoleUser,
|
||||
}, nil)
|
||||
permissionsManager.EXPECT().ValidateUserPermissions(gomock.Any(), gomock.Eq("test-account"), gomock.Eq("test-user"), gomock.Eq(modules.Peers), gomock.Eq(operations.Read)).Times(1).
|
||||
Return(false, context.Background(), nil)
|
||||
mockStore.EXPECT().GetAccountSettings(gomock.Any(), gomock.Eq(store.LockingStrengthNone), gomock.Eq("test-account")).Times(1).
|
||||
Return(&types.Settings{RegularUsersViewBlocked: true}, nil)
|
||||
|
||||
peers, total, err := m.GetPeersPaginated(context.Background(), "test-account", "test-user",
|
||||
store.PaginationState{Page: 11, PageSize: 111}, store.PeerFilters{UserId: "test-user"},
|
||||
store.PeerSorting{Fields: []store.PeerSortingKey{{Field: store.PeerSortByName, Order: store.DescOrderBy}}})
|
||||
|
||||
assert.Equal(t, make([]*peer.Peer, 0), peers)
|
||||
assert.Equal(t, 0, total)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func (s *BaseServer) EventStore() activity.Store {
|
||||
func (s *BaseServer) APIHandler() http.Handler {
|
||||
_ = CreateNamed(s, "http_v1api", func() http.Handler {
|
||||
apiv1Router := s.ApiV1Router()
|
||||
_, err := v1alpha1.NewAPIV1Handler(context.Background(), apiv1Router, s.AccountManager(), s.NetworkMapController(), s.PermissionsManager())
|
||||
_, err := v1alpha1.NewAPIV1Handler(context.Background(), apiv1Router, s.AccountManager(), s.PeersManager(), s.NetworkMapController(), s.PermissionsManager())
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create API handler: %v", err)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
|
||||
peersmodule "github.com/netbirdio/netbird/management/internals/modules/peers"
|
||||
"github.com/netbirdio/netbird/management/server/api/v1alpha1/peers"
|
||||
"github.com/netbirdio/netbird/management/server/api/v1alpha1/users"
|
||||
)
|
||||
@@ -20,6 +21,7 @@ func NewAPIV1Handler(
|
||||
ctx context.Context,
|
||||
router *mux.Router,
|
||||
accountManager account.Manager,
|
||||
peerManager peersmodule.Manager,
|
||||
networkMapController network_map.Controller,
|
||||
permissionsManager permissions.Manager) (http.Handler, error) {
|
||||
|
||||
@@ -30,7 +32,7 @@ func NewAPIV1Handler(
|
||||
|
||||
router.Use(v1validatorMiddleware.Handler)
|
||||
|
||||
peersHandler := peers.NewHandler(accountManager, networkMapController, permissionsManager)
|
||||
peersHandler := peers.NewHandler(accountManager, peerManager, networkMapController, permissionsManager)
|
||||
_ = peersHandler.WithEndpointsForRouter(router)
|
||||
|
||||
usersHandler := users.NewHandler(accountManager)
|
||||
|
||||
@@ -6,17 +6,22 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/peers"
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/api/v1alpha1/groups"
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
@@ -26,12 +31,14 @@ type Handler struct {
|
||||
accountManager account.Manager
|
||||
permissionsManager permissions.Manager
|
||||
networkMapController network_map.Controller
|
||||
peersManager peers.Manager
|
||||
}
|
||||
|
||||
// NewHandler creates a new peers Handler
|
||||
func NewHandler(accountManager account.Manager, networkMapController network_map.Controller, permissionsManager permissions.Manager) *Handler {
|
||||
func NewHandler(accountManager account.Manager, peerManager peers.Manager, networkMapController network_map.Controller, permissionsManager permissions.Manager) *Handler {
|
||||
return &Handler{
|
||||
accountManager: accountManager,
|
||||
peersManager: peerManager,
|
||||
networkMapController: networkMapController,
|
||||
permissionsManager: permissionsManager,
|
||||
}
|
||||
@@ -210,14 +217,32 @@ func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
nameFilter := r.URL.Query().Get("name")
|
||||
ipFilter := r.URL.Query().Get("ip")
|
||||
macFilter := r.URL.Query().Get("mac")
|
||||
pagination, err := paginationFromQuery(r.URL.Query())
|
||||
if err != nil {
|
||||
log.WithContext(r.Context()).Errorf("error parsing query: %v", err)
|
||||
util.WriteError(r.Context(), &status.Error{ErrorType: status.InvalidArgument, Message: err.Error()}, w)
|
||||
return
|
||||
}
|
||||
|
||||
filters, err := filtersFromQuery(r.URL.Query())
|
||||
if err != nil {
|
||||
log.WithContext(r.Context()).Errorf("error parsing query: %v", err)
|
||||
util.WriteError(r.Context(), &status.Error{ErrorType: status.InvalidArgument, Message: err.Error()}, w)
|
||||
return
|
||||
}
|
||||
|
||||
sorting, err := sortingOrderFromQuery(r.URL.Query())
|
||||
if err != nil {
|
||||
log.WithContext(r.Context()).Errorf("error parsing query: %v", err)
|
||||
util.WriteError(r.Context(), &status.Error{ErrorType: status.InvalidArgument, Message: err.Error()}, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
|
||||
peers, err := h.accountManager.GetPeers(r.Context(), accountID, userID, nameFilter, ipFilter, macFilter)
|
||||
peers, peerCount, err := h.peersManager.GetPeersPaginated(r.Context(), accountID, userID, pagination, filters, sorting)
|
||||
if err != nil {
|
||||
log.WithContext(r.Context()).Errorf("error getting peers: %v", err)
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
@@ -232,35 +257,135 @@ func (h *Handler) GetAllPeers(w http.ResponseWriter, r *http.Request) {
|
||||
grps, _ := h.accountManager.GetAllGroups(r.Context(), accountID, userID)
|
||||
|
||||
grpsInfoMap := groups.ToGroupsInfoMap(grps, len(peers))
|
||||
respBody := make([]*apiv1alpha1.PeerBatch, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
if peer.ProxyMeta.Embedded {
|
||||
continue
|
||||
}
|
||||
respBody = append(respBody, toPeerListItemResponse(peer, grpsInfoMap[peer.ID], dnsDomain, 0))
|
||||
}
|
||||
|
||||
validPeersMap, invalidPeersMap, err := h.accountManager.GetValidatedPeers(r.Context(), accountID)
|
||||
validPeers, invalidPeers, err := h.accountManager.GetValidatedPeers(r.Context(), accountID)
|
||||
if err != nil {
|
||||
log.WithContext(r.Context()).Errorf("failed to get validated peers: %v", err)
|
||||
util.WriteError(r.Context(), fmt.Errorf("internal error"), w)
|
||||
return
|
||||
}
|
||||
h.setApprovalRequiredFlag(respBody, validPeersMap, invalidPeersMap)
|
||||
|
||||
respBody := apiv1alpha1.PeerBatch{Peers: make([]apiv1alpha1.Peer, 0, len(peers))}
|
||||
for _, peer := range peers {
|
||||
_, valid := validPeers[peer.ID]
|
||||
reason := invalidPeers[peer.ID]
|
||||
|
||||
respBody.Peers = append(respBody.Peers, *toSinglePeerResponse(peer, grpsInfoMap[peer.ID], dnsDomain, valid, reason))
|
||||
}
|
||||
respBody.Page = pagination.Page
|
||||
respBody.PageSize = pagination.PageSize
|
||||
respBody.PeerCount = peerCount
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, respBody)
|
||||
}
|
||||
|
||||
func (h *Handler) setApprovalRequiredFlag(respBody []*apiv1alpha1.PeerBatch, validPeersMap map[string]struct{}, invalidPeersMap map[string]string) {
|
||||
for _, peer := range respBody {
|
||||
_, ok := validPeersMap[peer.Id]
|
||||
if !ok {
|
||||
peer.ApprovalRequired = true
|
||||
func filtersFromQuery(v url.Values) (store.PeerFilters, error) {
|
||||
connected, err := parseBool(v.Get("connected"))
|
||||
if err != nil {
|
||||
return store.PeerFilters{}, err
|
||||
}
|
||||
|
||||
reason := invalidPeersMap[peer.Id]
|
||||
peer.DisapprovalReason = &reason
|
||||
approvalRequired, err := parseBool(v.Get("approval_required"))
|
||||
if err != nil {
|
||||
return store.PeerFilters{}, err
|
||||
}
|
||||
|
||||
var isServer *bool
|
||||
if v.Get("kind") != "" {
|
||||
isServer = new(v.Get("kind") == "server")
|
||||
if *isServer && v.Get("user_id") != "" {
|
||||
return store.PeerFilters{}, fmt.Errorf("incompatible filters: kind == server and a non-empty user_id")
|
||||
}
|
||||
}
|
||||
return store.PeerFilters{
|
||||
UserId: v.Get("user_id"),
|
||||
GroupIds: v["group_ids"],
|
||||
Connected: connected,
|
||||
ApprovalRequired: approvalRequired,
|
||||
Os: v["os"],
|
||||
IP: v.Get("ip"),
|
||||
MAC: v.Get("mac"),
|
||||
Name: v.Get("name"),
|
||||
Hostname: v.Get("hostname"),
|
||||
IsServer: isServer,
|
||||
PeersIds: v["peer_ids"],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseBool(maybeBool string) (*bool, error) {
|
||||
if maybeBool == "" {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
parsed, err := strconv.ParseBool(maybeBool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return new(parsed), nil
|
||||
}
|
||||
|
||||
func paginationFromQuery(v url.Values) (store.PaginationState, error) {
|
||||
page, err := strconv.Atoi(v.Get("page"))
|
||||
if err != nil {
|
||||
return store.PaginationState{}, err
|
||||
}
|
||||
|
||||
pageSize, err := strconv.Atoi(v.Get("page_size"))
|
||||
if err != nil {
|
||||
return store.PaginationState{}, err
|
||||
}
|
||||
|
||||
return store.PaginationState{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sortingOrderFromQuery(v url.Values) (store.PeerSorting, error) {
|
||||
toret := store.PeerSorting{}
|
||||
sortby, ok := v["sort_by"]
|
||||
if !ok {
|
||||
return toret, nil
|
||||
}
|
||||
|
||||
for i := 0; i < len(sortby); {
|
||||
// ascending sorting order by default
|
||||
sortkey := store.PeerSortingKey{Order: store.AscOrderBy}
|
||||
field, order := parseFieldAndOrder(sortby[i])
|
||||
|
||||
switch field {
|
||||
case "name":
|
||||
sortkey.Field = store.PeerSortByName
|
||||
case "last_seen":
|
||||
sortkey.Field = store.PeerSortByLastSeen
|
||||
case "version":
|
||||
sortkey.Field = store.PeerSortByVersion
|
||||
default:
|
||||
return toret, fmt.Errorf("unable to parse sorting order '%s'", sortby)
|
||||
}
|
||||
|
||||
switch order {
|
||||
case "asc", "":
|
||||
// nop, already set
|
||||
case "desc":
|
||||
sortkey.Order = store.DescOrderBy
|
||||
default:
|
||||
return store.PeerSorting{}, fmt.Errorf("unable to parse sorting order '%s'", sortby)
|
||||
}
|
||||
|
||||
toret.Fields = append(toret.Fields, sortkey)
|
||||
i++
|
||||
}
|
||||
|
||||
return toret, nil
|
||||
}
|
||||
|
||||
func parseFieldAndOrder(fieldOrder string) (string, string) {
|
||||
maybeFieldAndOrder := strings.SplitN(fieldOrder, ",", 2)
|
||||
if len(maybeFieldAndOrder) == 2 {
|
||||
return strings.TrimSpace(maybeFieldAndOrder[0]), strings.TrimSpace(maybeFieldAndOrder[1])
|
||||
}
|
||||
return strings.TrimSpace(maybeFieldAndOrder[0]), ""
|
||||
}
|
||||
|
||||
func parseIPv6(s *string) (netip.Addr, error) {
|
||||
@@ -337,61 +462,6 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []apiv1alpha1.GroupMinim
|
||||
return apiPeer
|
||||
}
|
||||
|
||||
func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []apiv1alpha1.GroupMinimum, dnsDomain string, accessiblePeersCount int) *apiv1alpha1.PeerBatch {
|
||||
osVersion := peer.Meta.OSVersion
|
||||
if osVersion == "" {
|
||||
osVersion = peer.Meta.Core
|
||||
}
|
||||
|
||||
return &apiv1alpha1.PeerBatch{
|
||||
CreatedAt: peer.CreatedAt,
|
||||
AccessiblePeersCount: accessiblePeersCount,
|
||||
Peer: apiv1alpha1.Peer{
|
||||
PeerMinimum: apiv1alpha1.PeerMinimum{
|
||||
Id: peer.ID,
|
||||
Name: peer.Name,
|
||||
},
|
||||
Ip: peer.IP.String(),
|
||||
Ipv6: peerIPv6String(peer),
|
||||
ConnectionIp: peer.Location.ConnectionIP.String(),
|
||||
Connected: peer.Status.Connected,
|
||||
LastSeen: peer.Status.LastSeen,
|
||||
Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion),
|
||||
KernelVersion: peer.Meta.KernelVersion,
|
||||
GeonameId: int(peer.Location.GeoNameID),
|
||||
Version: peer.Meta.WtVersion,
|
||||
Groups: groupsInfo,
|
||||
SshEnabled: peer.SSHEnabled,
|
||||
Hostname: peer.Meta.Hostname,
|
||||
UserId: peer.UserID,
|
||||
UiVersion: peer.Meta.UIVersion,
|
||||
DnsLabel: fqdn(peer, dnsDomain),
|
||||
ExtraDnsLabels: fqdnList(peer.ExtraDNSLabels, dnsDomain),
|
||||
LoginExpirationEnabled: peer.LoginExpirationEnabled,
|
||||
LastLogin: peer.GetLastLogin(),
|
||||
LoginExpired: peer.Status.LoginExpired,
|
||||
CountryCode: apiv1alpha1.CountryCode(peer.Location.CountryCode),
|
||||
CityName: apiv1alpha1.CityName(peer.Location.CityName),
|
||||
SerialNumber: peer.Meta.SystemSerialNumber,
|
||||
InactivityExpirationEnabled: peer.InactivityExpirationEnabled,
|
||||
Ephemeral: peer.Ephemeral,
|
||||
LocalFlags: &apiv1alpha1.PeerLocalFlags{
|
||||
BlockInbound: &peer.Meta.Flags.BlockInbound,
|
||||
BlockLanAccess: &peer.Meta.Flags.BlockLANAccess,
|
||||
DisableClientRoutes: &peer.Meta.Flags.DisableClientRoutes,
|
||||
DisableDns: &peer.Meta.Flags.DisableDNS,
|
||||
DisableFirewall: &peer.Meta.Flags.DisableFirewall,
|
||||
DisableServerRoutes: &peer.Meta.Flags.DisableServerRoutes,
|
||||
LazyConnectionEnabled: &peer.Meta.Flags.LazyConnectionEnabled,
|
||||
RosenpassEnabled: &peer.Meta.Flags.RosenpassEnabled,
|
||||
RosenpassPermissive: &peer.Meta.Flags.RosenpassPermissive,
|
||||
ServerSshAllowed: &peer.Meta.Flags.ServerSSHAllowed,
|
||||
RemoteJobsAllowed: &peer.Meta.Flags.RemoteJobsAllowed,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fqdn(peer *nbpeer.Peer, dnsDomain string) string {
|
||||
fqdn := peer.FQDN(dnsDomain)
|
||||
if fqdn == "" {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package peers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
peersmodule "github.com/netbirdio/netbird/management/internals/modules/peers"
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/shared/auth"
|
||||
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestGetPeersPagination(t *testing.T) {
|
||||
peer := &nbpeer.Peer{
|
||||
ID: testPeerID,
|
||||
Status: &nbpeer.PeerStatus{Connected: true},
|
||||
Meta: nbpeer.PeerSystemMeta{},
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestPath string
|
||||
setExpectations func(p *Handler)
|
||||
}{
|
||||
{
|
||||
name: "GetPeers default paging, filtering, and sorting",
|
||||
requestPath: "/peers",
|
||||
expectedStatus: http.StatusOK,
|
||||
setExpectations: func(p *Handler) {
|
||||
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
|
||||
gomock.Eq(store.PaginationState{Page: 1, PageSize: 100}), gomock.Any(), gomock.Any()).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GetPeers paging",
|
||||
requestPath: "/peers?page=10&page_size=250",
|
||||
expectedStatus: http.StatusOK,
|
||||
setExpectations: func(p *Handler) {
|
||||
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
|
||||
gomock.Eq(store.PaginationState{Page: 10, PageSize: 250}),
|
||||
gomock.Any(), gomock.Any()).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GetPeers paging with invalid page",
|
||||
requestPath: "/peers?page=aaa&page_size=250",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
{
|
||||
name: "GetPeers paging with invalid page size",
|
||||
requestPath: "/peers?page=10&page_size=aaa",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
{
|
||||
name: "GetPeers filtering all valid params",
|
||||
requestPath: "/peers?user_id=test-user&group_ids=1&group_ids=2&group_ids=3&group_ids=4&peer_ids=peer-1&peer_ids=peer-2&connected=true&approval_required=false&os=linux&os=windows&os=darwin&os=android&os=ios&os=js&ip=10.10.10.10&mac=00:11:22:33:44:55&name=test&hostname=test-host&kind=user_device",
|
||||
expectedStatus: http.StatusOK,
|
||||
setExpectations: func(p *Handler) {
|
||||
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
|
||||
gomock.Eq(store.PaginationState{Page: 1, PageSize: 100}),
|
||||
gomock.Eq(store.PeerFilters{
|
||||
UserId: "test-user",
|
||||
GroupIds: []string{"1", "2", "3", "4"},
|
||||
Connected: new(true),
|
||||
ApprovalRequired: new(false),
|
||||
Os: []string{"linux", "windows", "darwin", "android", "ios", "js"},
|
||||
IP: "10.10.10.10",
|
||||
MAC: "00:11:22:33:44:55",
|
||||
Name: "test",
|
||||
Hostname: "test-host",
|
||||
IsServer: new(false),
|
||||
PeersIds: []string{"peer-1", "peer-2"},
|
||||
}),
|
||||
gomock.Any()).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GetPeers filtering with kind server",
|
||||
requestPath: "/peers?kind=server",
|
||||
expectedStatus: http.StatusOK,
|
||||
setExpectations: func(p *Handler) {
|
||||
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
|
||||
gomock.Eq(store.PaginationState{Page: 1, PageSize: 100}),
|
||||
gomock.Eq(store.PeerFilters{IsServer: new(true)}),
|
||||
gomock.Any()).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GetPeers filtering with invalid connected value",
|
||||
requestPath: "/peers?connected=aaa",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
{
|
||||
name: "GetPeers filtering with invalid approval_required value",
|
||||
requestPath: "/peers?approval_required=aaa",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
{
|
||||
name: "GetPeers filtering with invalid os value",
|
||||
requestPath: "/peers?os=nosuchos",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
{
|
||||
name: "GetPeers filtering with invalid kind value",
|
||||
requestPath: "/peers?kind=nosuchkind",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
{
|
||||
name: "GetPeers filtering with conflicting user_id and kind",
|
||||
requestPath: "/peers?user_id=test-user&kind=server",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
{
|
||||
name: "GetPeers sorting preserves field ordering",
|
||||
requestPath: "/peers?sort_by=last_seen&sort_by=name&sort_by=version",
|
||||
expectedStatus: http.StatusOK,
|
||||
setExpectations: func(p *Handler) {
|
||||
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
|
||||
gomock.Eq(store.PaginationState{Page: 1, PageSize: 100}),
|
||||
gomock.Any(),
|
||||
gomock.Eq(store.PeerSorting{
|
||||
Fields: []store.PeerSortingKey{
|
||||
{Field: store.PeerSortByLastSeen, Order: store.AscOrderBy},
|
||||
{Field: store.PeerSortByName, Order: store.AscOrderBy},
|
||||
{Field: store.PeerSortByVersion, Order: store.AscOrderBy},
|
||||
},
|
||||
})).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GetPeers sorting with invalid field",
|
||||
requestPath: "/peers?sort_by=nosuchfield,asc",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
{
|
||||
name: "GetPeers sorting with invalid field",
|
||||
requestPath: "/peers?sort_by=nosuchfield,asc",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
{
|
||||
name: "GetPeers sorting with invalid sorting order",
|
||||
requestPath: "/peers?sort_by=name,nosuchorder&sort_by=version",
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
setExpectations: func(p *Handler) {},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := initTestMetaData(t, peer)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, tc.requestPath, nil)
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: "admin_user",
|
||||
Domain: "hotmail.com",
|
||||
AccountId: "test_id",
|
||||
})
|
||||
|
||||
v1validator, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
|
||||
assert.NoError(t, err)
|
||||
router := mux.NewRouter()
|
||||
router.Use(v1validator.Handler)
|
||||
router = p.WithEndpointsForRouter(router)
|
||||
|
||||
tc.setExpectations(p)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, tc.expectedStatus, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
peersmodule "github.com/netbirdio/netbird/management/internals/modules/peers"
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
@@ -127,7 +128,10 @@ func initTestMetaData(t *testing.T, peers ...*nbpeer.Peer) *Handler {
|
||||
}).
|
||||
AnyTimes()
|
||||
|
||||
peerManager := peersmodule.NewMockManager(ctrl2)
|
||||
|
||||
return &Handler{
|
||||
peersManager: peerManager,
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
UpdatePeerFunc: func(_ context.Context, accountID, userID string, update *nbpeer.Peer) (*nbpeer.Peer, error) {
|
||||
var p *nbpeer.Peer
|
||||
@@ -173,8 +177,8 @@ func initTestMetaData(t *testing.T, peers ...*nbpeer.Peer) *Handler {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
},
|
||||
GetPeersFunc: func(_ context.Context, accountID, userID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error) {
|
||||
return peers, nil
|
||||
GetPeersFunc: func(_ context.Context, accountID, userID string, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, int, error) {
|
||||
return peers, len(peers), nil
|
||||
},
|
||||
GetPeerGroupsFunc: func(ctx context.Context, accountID, peerID string) ([]*types.Group, error) {
|
||||
peersID := make([]string, len(peers))
|
||||
@@ -220,9 +224,7 @@ func initTestMetaData(t *testing.T, peers ...*nbpeer.Peer) *Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the GetAllPeers endpoint reachable in the route /api/peers
|
||||
// Use the metadata generated by initTestMetaData() to check for values
|
||||
func TestGetPeers(t *testing.T) {
|
||||
func TestGetPeer(t *testing.T) {
|
||||
|
||||
peer := &nbpeer.Peer{
|
||||
ID: testPeerID,
|
||||
@@ -262,23 +264,13 @@ func TestGetPeers(t *testing.T) {
|
||||
requestPath string
|
||||
contentType string
|
||||
requestBody io.Reader
|
||||
expectedArray bool
|
||||
expectedPeer *nbpeer.Peer
|
||||
}{
|
||||
{
|
||||
name: "GetPeersMetaData",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/peers",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedArray: true,
|
||||
expectedPeer: peer,
|
||||
},
|
||||
{
|
||||
name: "GetPeer with update channel",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/peers/" + testPeerID,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedArray: false,
|
||||
expectedPeer: peer,
|
||||
},
|
||||
{
|
||||
@@ -287,7 +279,6 @@ func TestGetPeers(t *testing.T) {
|
||||
requestPath: "/peers/" + testPeerID,
|
||||
contentType: "application/json",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedArray: false,
|
||||
requestBody: bytes.NewBufferString("{\"login_expiration_enabled\":true,\"name\":\"New Name\",\"ssh_enabled\":true, \"inactivity_expiration_enabled\":true}"),
|
||||
expectedPeer: expectedUpdatedPeer,
|
||||
},
|
||||
@@ -330,32 +321,123 @@ func TestGetPeers(t *testing.T) {
|
||||
t.Fatalf("I don't know what I expected; %v", err)
|
||||
}
|
||||
|
||||
var got *apiv1alpha1.Peer
|
||||
if tc.expectedArray {
|
||||
respBody := []*apiv1alpha1.Peer{}
|
||||
err = json.Unmarshal(content, &respBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
// hardcode this check for now as we only have two peers in this suite
|
||||
assert.Equal(t, len(respBody), 2)
|
||||
|
||||
for _, peer := range respBody {
|
||||
if peer.Id == testPeerID {
|
||||
got = peer
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
got = &apiv1alpha1.Peer{}
|
||||
err = json.Unmarshal(content, got)
|
||||
if err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
var got apiv1alpha1.Peer
|
||||
err = json.Unmarshal(content, &got)
|
||||
if err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
t.Log(got)
|
||||
assert.Equal(t, tc.expectedPeer.Name, got.Name)
|
||||
assert.Equal(t, tc.expectedPeer.Meta.WtVersion, got.Version)
|
||||
assert.Equal(t, tc.expectedPeer.IP.String(), got.Ip)
|
||||
assert.Equal(t, "OS core", got.Os)
|
||||
assert.Equal(t, tc.expectedPeer.LoginExpirationEnabled, got.LoginExpirationEnabled)
|
||||
assert.Equal(t, tc.expectedPeer.SSHEnabled, got.SshEnabled)
|
||||
assert.Equal(t, tc.expectedPeer.Status.Connected, got.Connected)
|
||||
assert.Equal(t, tc.expectedPeer.Meta.SystemSerialNumber, got.SerialNumber)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPeers(t *testing.T) {
|
||||
peer := &nbpeer.Peer{
|
||||
ID: testPeerID,
|
||||
Key: "key",
|
||||
IP: netip.MustParseAddr("100.64.0.1"),
|
||||
IPv6: netip.MustParseAddr("fd00::1"),
|
||||
Status: &nbpeer.PeerStatus{Connected: true},
|
||||
Name: "PeerName",
|
||||
LoginExpirationEnabled: false,
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
Hostname: "hostname",
|
||||
GoOS: "GoOS",
|
||||
Kernel: "kernel",
|
||||
Core: "core",
|
||||
Platform: "platform",
|
||||
OS: "OS",
|
||||
WtVersion: "development",
|
||||
SystemSerialNumber: "C02XJ0J0JGH7",
|
||||
},
|
||||
}
|
||||
|
||||
peer1 := peer.Copy()
|
||||
peer1.ID = noUpdateChannelTestPeerID
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
requestType string
|
||||
requestPath string
|
||||
contentType string
|
||||
expectedPeer *nbpeer.Peer
|
||||
}{
|
||||
{
|
||||
name: "GetPeersMetaData",
|
||||
requestType: http.MethodGet,
|
||||
requestPath: "/peers",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedPeer: peer,
|
||||
},
|
||||
}
|
||||
|
||||
p := initTestMetaData(t, peer, peer1)
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: "admin_user",
|
||||
Domain: "hotmail.com",
|
||||
AccountId: "test_id",
|
||||
})
|
||||
if tc.contentType != "" {
|
||||
req.Header.Set("Content-Type", tc.contentType)
|
||||
}
|
||||
|
||||
v1validator, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
|
||||
assert.NoError(t, err)
|
||||
router := mux.NewRouter()
|
||||
router.Use(v1validator.Handler)
|
||||
|
||||
router = p.WithEndpointsForRouter(router)
|
||||
|
||||
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return([]*nbpeer.Peer{peer, peer1}, 2, nil)
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if status := recorder.Code; status != tc.expectedStatus {
|
||||
t.Fatalf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("I don't know what I expected; %v", err)
|
||||
}
|
||||
|
||||
var got *apiv1alpha1.Peer
|
||||
respBody := apiv1alpha1.PeerBatch{}
|
||||
err = json.Unmarshal(content, &respBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, respBody.Page, 1)
|
||||
assert.Equal(t, respBody.PageSize, 100)
|
||||
assert.Equal(t, respBody.PeerCount, 2)
|
||||
assert.Equal(t, len(respBody.Peers), 2)
|
||||
|
||||
for _, peer := range respBody.Peers {
|
||||
if peer.Id == testPeerID {
|
||||
got = &peer
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expectedPeer.Name, got.Name)
|
||||
assert.Equal(t, tc.expectedPeer.Meta.WtVersion, got.Version)
|
||||
|
||||
@@ -78,8 +78,8 @@ func initGroupTestData(initGroups ...*types.Group) *handler {
|
||||
|
||||
return nil, status.Errorf(status.NotFound, "unknown group name")
|
||||
},
|
||||
GetPeersFunc: func(ctx context.Context, accountID, userID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error) {
|
||||
return maps.Values(TestPeers), nil
|
||||
GetPeersFunc: func(ctx context.Context, accountID, userID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, int, error) {
|
||||
return maps.Values(TestPeers), len(maps.Values(TestPeers)), nil
|
||||
},
|
||||
DeleteGroupFunc: func(_ context.Context, accountID, userId, groupID string) error {
|
||||
if groupID == "linked-grp" {
|
||||
|
||||
@@ -173,8 +173,8 @@ func initTestMetaData(t *testing.T, peers ...*nbpeer.Peer) *Handler {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
},
|
||||
GetPeersFunc: func(_ context.Context, accountID, userID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error) {
|
||||
return peers, nil
|
||||
GetPeersFunc: func(_ context.Context, accountID, userID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, int, error) {
|
||||
return peers, len(peers), nil
|
||||
},
|
||||
GetPeerGroupsFunc: func(ctx context.Context, accountID, peerID string) ([]*types.Group, error) {
|
||||
peersID := make([]string, len(peers))
|
||||
|
||||
@@ -39,7 +39,7 @@ type MockAccountManager struct {
|
||||
GetAccountIDByUserIdFunc func(ctx context.Context, userAuth auth.UserAuth) (string, error)
|
||||
GetUserFromUserAuthFunc func(ctx context.Context, userAuth auth.UserAuth) (*types.User, error)
|
||||
ListUsersFunc func(ctx context.Context, accountID string) ([]*types.User, error)
|
||||
GetPeersFunc func(ctx context.Context, accountID, userID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error)
|
||||
GetPeersFunc func(ctx context.Context, accountID, userID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, int, error)
|
||||
MarkPeerConnectedFunc func(ctx context.Context, peerKey string, accountID string, sessionStartedAt int64, nmap *types.NetworkMap) error
|
||||
MarkPeerDisconnectedFunc func(ctx context.Context, peerKey string, accountID string, sessionStartedAt int64) error
|
||||
SyncAndMarkPeerFunc func(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*nmdata.PostureChecks, int64, error)
|
||||
@@ -820,7 +820,8 @@ func (am *MockAccountManager) GetAccountIDFromUserAuth(ctx context.Context, user
|
||||
// GetPeers mocks GetPeers of the AccountManager interface
|
||||
func (am *MockAccountManager) GetPeers(ctx context.Context, accountID, userID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error) {
|
||||
if am.GetPeersFunc != nil {
|
||||
return am.GetPeersFunc(ctx, accountID, userID, nameFilter, ipFilter, macFilter)
|
||||
peers, _, err := am.GetPeersFunc(ctx, accountID, userID, nameFilter, ipFilter, macFilter)
|
||||
return peers, err
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPeers is not implemented")
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
|
||||
return []*nbpeer.Peer{}, nil
|
||||
}
|
||||
|
||||
return am.Store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID)
|
||||
peers, err := am.Store.GetUserPeers(ctx, store.LockingStrengthNone, accountID, userID)
|
||||
|
||||
return peers, err
|
||||
}
|
||||
|
||||
// MarkPeerConnected marks a peer as connected with optimistic-locked
|
||||
|
||||
@@ -730,7 +730,6 @@ func TestDefaultAccountManager_GetPeers(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assert.NotNil(t, peers)
|
||||
|
||||
assert.Len(t, peers, testCase.expectedPeerCount)
|
||||
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -494,7 +495,6 @@ func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength Locking
|
||||
return &peer, nil
|
||||
}
|
||||
|
||||
// GetAccountPeers retrieves peers for an account.
|
||||
func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error) {
|
||||
var peers []*nbpeer.Peer
|
||||
tx := s.db
|
||||
@@ -514,7 +514,6 @@ func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStre
|
||||
if macFilter != "" {
|
||||
query = query.Where("meta_network_addresses LIKE ?", "%"+macFilter+"%")
|
||||
}
|
||||
|
||||
if err := query.Find(&peers).Error; err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err)
|
||||
return nil, status.Errorf(status.Internal, "failed to get peers from store")
|
||||
@@ -523,6 +522,162 @@ func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStre
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
// GetAccountPeers retrieves peers for an account.
|
||||
func (s *SqlStore) GetAccountPeersPaginated(ctx context.Context, lockStrength LockingStrength, accountID string, pagination PaginationState, filters PeerFilters, sorting PeerSorting) ([]*nbpeer.Peer, int, error) {
|
||||
// main query
|
||||
var peers []*nbpeer.Peer
|
||||
{
|
||||
tx := s.db
|
||||
if lockStrength != LockingStrengthNone {
|
||||
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
||||
}
|
||||
query := tx.Where("peers.account_id=?", accountID)
|
||||
query = query.Where("(peers.proxy_meta_embedded=false or peers.proxy_meta_embedded is null)")
|
||||
query = filters.AsConditions(query)
|
||||
query = pagination.AsScope(query)
|
||||
query = sorting.AsOrderBy(ctx, query)
|
||||
|
||||
if err := query.Distinct().Find(&peers).Error; err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err)
|
||||
return nil, 0, status.Errorf(status.Internal, "failed to get peers from store")
|
||||
}
|
||||
}
|
||||
|
||||
// total number of records
|
||||
var peerCount int64
|
||||
{
|
||||
tx := s.db
|
||||
if lockStrength != LockingStrengthNone {
|
||||
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
||||
}
|
||||
query := tx.Where("peers.account_id=?", accountID)
|
||||
query = query.Where("(peers.proxy_meta_embedded=false or peers.proxy_meta_embedded is null)")
|
||||
query = filters.AsConditions(query)
|
||||
|
||||
if err := query.Table("peers").Select("count(distinct(peers.id))").Count(&peerCount).Error; err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get total number of peers from the store: %s", err)
|
||||
return nil, 0, status.Errorf(status.Internal, "failed to get total number of peers from store")
|
||||
}
|
||||
}
|
||||
|
||||
return peers, int(peerCount), nil
|
||||
}
|
||||
|
||||
type PaginationState struct {
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
func (p *PaginationState) AsScope(db *gorm.DB) *gorm.DB {
|
||||
// empty pagination state
|
||||
if p.PageSize == 0 {
|
||||
return db
|
||||
}
|
||||
offset := max(p.Page-1, 0) * p.PageSize
|
||||
return db.Offset(offset).Limit(p.PageSize)
|
||||
}
|
||||
|
||||
type PeerFilters struct {
|
||||
UserId string
|
||||
GroupIds []string
|
||||
Connected *bool
|
||||
ApprovalRequired *bool
|
||||
Os []string
|
||||
IP string
|
||||
MAC string
|
||||
Hostname string
|
||||
Name string
|
||||
IsServer *bool
|
||||
PeersIds []string
|
||||
}
|
||||
|
||||
func (f *PeerFilters) AsConditions(db *gorm.DB) *gorm.DB {
|
||||
if len(f.GroupIds) > 0 {
|
||||
db = db.Joins("join group_peers on peers.id=group_peers.peer_id and peers.account_id=group_peers.account_id").Where("group_peers.group_id in (?)", f.GroupIds)
|
||||
}
|
||||
if f.UserId != "" {
|
||||
db = db.Where("user_id=?", f.UserId)
|
||||
}
|
||||
if f.Connected != nil {
|
||||
db = db.Where("peer_status_connected=?", *f.Connected)
|
||||
}
|
||||
if f.ApprovalRequired != nil {
|
||||
db = db.Where("peer_status_requires_approval=?", *f.ApprovalRequired)
|
||||
}
|
||||
if len(f.Os) > 0 {
|
||||
db = db.Where("meta_go_os in (?)", f.Os)
|
||||
}
|
||||
if f.IP != "" {
|
||||
db = db.Where("ip like ? or lower(ipv6) like ?", fmt.Sprintf("%%%s%%", f.IP), fmt.Sprintf("%%%s%%", strings.ToLower(f.IP)))
|
||||
}
|
||||
if f.MAC != "" {
|
||||
db = db.Where("lower(meta_network_addresses) like ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.MAC)))
|
||||
}
|
||||
if f.Hostname != "" {
|
||||
db = db.Where("lower(meta_hostname) like ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Hostname)))
|
||||
}
|
||||
if f.Name != "" {
|
||||
db = db.Where("lower(name) like ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Name)))
|
||||
}
|
||||
if f.IsServer != nil {
|
||||
if *f.IsServer {
|
||||
db = db.Where("(user_id='' or user_id is null)")
|
||||
} else {
|
||||
db = db.Where("user_id<>'' and user_id is not null")
|
||||
}
|
||||
}
|
||||
if len(f.PeersIds) > 0 {
|
||||
db = db.Where("id in (?)", f.PeersIds)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
type PeerSortByField int
|
||||
|
||||
const (
|
||||
PeerSortByName PeerSortByField = iota
|
||||
PeerSortByLastSeen
|
||||
PeerSortByVersion
|
||||
)
|
||||
|
||||
type OrderBy int
|
||||
|
||||
const (
|
||||
AscOrderBy OrderBy = iota
|
||||
DescOrderBy
|
||||
)
|
||||
|
||||
type PeerSortingKey struct {
|
||||
Field PeerSortByField
|
||||
Order OrderBy
|
||||
}
|
||||
|
||||
type PeerSorting struct {
|
||||
Fields []PeerSortingKey
|
||||
}
|
||||
|
||||
func (s *PeerSorting) AsOrderBy(ctx context.Context, db *gorm.DB) *gorm.DB {
|
||||
order := make([]clause.OrderByColumn, 0)
|
||||
|
||||
for _, key := range s.Fields {
|
||||
switch key.Field {
|
||||
case PeerSortByName:
|
||||
order = append(order, clause.OrderByColumn{Column: clause.Column{Name: "peers.name"}, Desc: key.Order == DescOrderBy})
|
||||
case PeerSortByLastSeen:
|
||||
order = append(order, clause.OrderByColumn{Column: clause.Column{Name: "peers.peer_status_last_seen"}, Desc: key.Order == DescOrderBy})
|
||||
case PeerSortByVersion:
|
||||
order = append(order, clause.OrderByColumn{Column: clause.Column{Name: "meta_wt_version"}, Desc: key.Order == DescOrderBy})
|
||||
default:
|
||||
log.WithContext(ctx).Warnf("invalid order by column '%v', skipping", key.Field)
|
||||
}
|
||||
}
|
||||
|
||||
// always order/break ties by id asc order
|
||||
order = append(order, clause.OrderByColumn{Column: clause.Column{Name: "peers.id"}, Desc: false})
|
||||
return db.Order(clause.OrderBy{Columns: order})
|
||||
}
|
||||
|
||||
// GetUserPeers retrieves peers for a user.
|
||||
func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) {
|
||||
tx := s.db
|
||||
|
||||
@@ -512,7 +512,7 @@ func TestSqlStore_GetAccountPeers(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
peers, err := store.GetAccountPeers(context.Background(), LockingStrengthNone, tt.accountID, tt.nameFilter, tt.ipFilter, "")
|
||||
peers, _, err := store.GetAccountPeersPaginated(context.Background(), LockingStrengthNone, tt.accountID, PaginationState{}, PeerFilters{Hostname: tt.nameFilter, IP: tt.ipFilter}, PeerSorting{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, peers, tt.expectedCount)
|
||||
})
|
||||
@@ -555,7 +555,7 @@ func TestSqlStore_GetAccountPeers_FilterByMac(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
peers, err := store.GetAccountPeers(ctx, LockingStrengthNone, accountID, "", "", tt.macFilter)
|
||||
peers, _, err := store.GetAccountPeersPaginated(ctx, LockingStrengthNone, accountID, PaginationState{}, PeerFilters{MAC: tt.macFilter}, PeerSorting{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, peers, tt.expectedCount)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSqlStore_FilterPeers(t *testing.T) {
|
||||
runTestForAllEnginesNoSqliteSeed(t, "../testdata/peers_pagination.sql", func(t *testing.T, engine string, store Store) {
|
||||
var tests = []struct {
|
||||
description string
|
||||
filters PeerFilters
|
||||
expectedLength int
|
||||
expectedPeers []string
|
||||
}{
|
||||
{
|
||||
description: "filter by user_id",
|
||||
filters: PeerFilters{UserId: "user-id-1"},
|
||||
expectedLength: 1,
|
||||
expectedPeers: []string{"Peer-1-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by connected status",
|
||||
filters: PeerFilters{Connected: new(true)},
|
||||
expectedLength: 2,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by approval required status",
|
||||
filters: PeerFilters{ApprovalRequired: new(true)},
|
||||
expectedLength: 2,
|
||||
expectedPeers: []string{"peer-2-name", "peer-3-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by ipv4",
|
||||
filters: PeerFilters{IP: "10.10.10"},
|
||||
expectedLength: 2,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by ipv6",
|
||||
filters: PeerFilters{IP: "ba80:6aa5:89f1:44d7:8701:869"},
|
||||
expectedLength: 2,
|
||||
expectedPeers: []string{"peer-2-name", "peer-3-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by ipv6 (uppercase)",
|
||||
filters: PeerFilters{IP: "BA80:6AA5:89f1:44d7:8701:869"},
|
||||
expectedLength: 2,
|
||||
expectedPeers: []string{"peer-2-name", "peer-3-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by mac",
|
||||
filters: PeerFilters{MAC: ":15:5d:24:0c"},
|
||||
expectedLength: 3,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name", "peer-3-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by mac (uppercase)",
|
||||
filters: PeerFilters{MAC: ":15:5D:24:0C"},
|
||||
expectedLength: 3,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name", "peer-3-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by kind (server)",
|
||||
filters: PeerFilters{IsServer: new(true), Os: []string{"linux"}},
|
||||
expectedLength: 3,
|
||||
expectedPeers: []string{"peer-4-name", "peer-5-name", "peer-6-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by kind (user_device)",
|
||||
filters: PeerFilters{IsServer: new(false)},
|
||||
expectedLength: 3,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name", "peer-3-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by group_ids",
|
||||
filters: PeerFilters{GroupIds: []string{"group-one-resource-id", "group-two-resources-id"}},
|
||||
expectedLength: 2,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by os",
|
||||
filters: PeerFilters{Os: []string{"linux", "ios"}},
|
||||
expectedLength: 5,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name", "peer-4-name", "peer-5-name", "peer-6-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by peer id",
|
||||
filters: PeerFilters{PeersIds: []string{"peer-id-one", "peer-id-five", "peer-id-three"}},
|
||||
expectedLength: 3,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-3-name", "peer-5-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by name",
|
||||
filters: PeerFilters{Name: "peer-1"},
|
||||
expectedLength: 1,
|
||||
expectedPeers: []string{"Peer-1-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by name (upper-case)",
|
||||
filters: PeerFilters{Name: "PEER-1"},
|
||||
expectedLength: 1,
|
||||
expectedPeers: []string{"Peer-1-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by hostname",
|
||||
filters: PeerFilters{Hostname: "one-name.local"},
|
||||
expectedLength: 1,
|
||||
expectedPeers: []string{"Peer-1-name"},
|
||||
},
|
||||
{
|
||||
description: "filter by hostname (upper-case)",
|
||||
filters: PeerFilters{Hostname: "ONE-NAME.LOCAL"},
|
||||
expectedLength: 1,
|
||||
expectedPeers: []string{"Peer-1-name"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description+" "+engine, func(t *testing.T) {
|
||||
t.Helper()
|
||||
peers, count, err := store.GetAccountPeersPaginated(context.Background(), LockingStrengthNone, "account-1",
|
||||
PaginationState{}, tt.filters, PeerSorting{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, peers, tt.expectedLength)
|
||||
require.Equal(t, tt.expectedLength, count)
|
||||
actualPeerNames := make([]string, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
actualPeerNames = append(actualPeerNames, p.Name)
|
||||
}
|
||||
require.ElementsMatch(t, actualPeerNames, tt.expectedPeers)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_PeersPaging(t *testing.T) {
|
||||
runTestForAllEnginesNoSqliteSeed(t, "../testdata/peers_pagination.sql", func(t *testing.T, engine string, store Store) {
|
||||
var tests = []struct {
|
||||
description string
|
||||
page int
|
||||
pageSize int
|
||||
expectedLength int
|
||||
expectedCount int
|
||||
expectedPeers []string
|
||||
}{
|
||||
{
|
||||
description: "page 1, size 4",
|
||||
page: 1,
|
||||
pageSize: 4,
|
||||
expectedLength: 4,
|
||||
expectedCount: 6,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name", "peer-3-name", "peer-4-name"},
|
||||
},
|
||||
{
|
||||
description: "page 2, size 4",
|
||||
page: 2,
|
||||
pageSize: 4,
|
||||
expectedLength: 2,
|
||||
expectedCount: 6,
|
||||
expectedPeers: []string{"peer-5-name", "peer-6-name"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description+" "+engine, func(t *testing.T) {
|
||||
t.Helper()
|
||||
peers, count, err := store.GetAccountPeersPaginated(context.Background(), LockingStrengthNone, "account-1",
|
||||
PaginationState{Page: tt.page, PageSize: tt.pageSize}, PeerFilters{}, PeerSorting{Fields: []PeerSortingKey{{Field: PeerSortByName}}})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, peers, tt.expectedLength)
|
||||
require.Equal(t, tt.expectedCount, count)
|
||||
actualPeerNames := make([]string, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
actualPeerNames = append(actualPeerNames, p.Name)
|
||||
}
|
||||
require.ElementsMatch(t, actualPeerNames, tt.expectedPeers)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_PeersSorting(t *testing.T) {
|
||||
runTestForAllEnginesNoSqliteSeed(t, "../testdata/peers_pagination.sql", func(t *testing.T, engine string, store Store) {
|
||||
var tests = []struct {
|
||||
description string
|
||||
sorting PeerSorting
|
||||
expectedLength int
|
||||
expectedPeers []string
|
||||
}{
|
||||
{
|
||||
description: "default order (by id ascending)",
|
||||
sorting: PeerSorting{Fields: []PeerSortingKey{}},
|
||||
expectedLength: 6,
|
||||
expectedPeers: []string{"peer-5-name", "peer-4-name", "Peer-1-name", "peer-6-name", "peer-3-name", "peer-2-name"},
|
||||
},
|
||||
{
|
||||
description: "by name ascending",
|
||||
sorting: PeerSorting{Fields: []PeerSortingKey{{Field: PeerSortByName}}},
|
||||
expectedLength: 6,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name", "peer-3-name", "peer-4-name", "peer-5-name", "peer-6-name"},
|
||||
},
|
||||
{
|
||||
description: "by name descending",
|
||||
sorting: PeerSorting{Fields: []PeerSortingKey{{Field: PeerSortByName, Order: DescOrderBy}}},
|
||||
expectedLength: 6,
|
||||
expectedPeers: []string{"peer-6-name", "peer-5-name", "peer-4-name", "peer-3-name", "peer-2-name", "Peer-1-name"},
|
||||
},
|
||||
{
|
||||
description: "by last_seen ascending",
|
||||
sorting: PeerSorting{Fields: []PeerSortingKey{{Field: PeerSortByLastSeen}}},
|
||||
expectedLength: 6,
|
||||
expectedPeers: []string{"peer-6-name", "peer-4-name", "peer-5-name", "peer-3-name", "peer-2-name", "Peer-1-name"},
|
||||
},
|
||||
{
|
||||
description: "by last_seen descending",
|
||||
sorting: PeerSorting{Fields: []PeerSortingKey{{Field: PeerSortByLastSeen, Order: DescOrderBy}}},
|
||||
expectedLength: 6,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name", "peer-3-name", "peer-5-name", "peer-4-name", "peer-6-name"},
|
||||
},
|
||||
{
|
||||
description: "by version ascending",
|
||||
sorting: PeerSorting{Fields: []PeerSortingKey{{Field: PeerSortByVersion}}},
|
||||
expectedLength: 6,
|
||||
expectedPeers: []string{"Peer-1-name", "peer-2-name", "peer-4-name", "peer-3-name", "peer-6-name", "peer-5-name"},
|
||||
},
|
||||
{
|
||||
description: "by version descending",
|
||||
sorting: PeerSorting{Fields: []PeerSortingKey{{Field: PeerSortByVersion, Order: DescOrderBy}}},
|
||||
expectedLength: 6,
|
||||
expectedPeers: []string{"peer-5-name", "peer-6-name", "peer-3-name", "peer-4-name", "peer-2-name", "Peer-1-name"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description+" "+engine, func(t *testing.T) {
|
||||
t.Helper()
|
||||
peers, _, err := store.GetAccountPeersPaginated(context.Background(), LockingStrengthNone, "account-1",
|
||||
PaginationState{}, PeerFilters{}, tt.sorting)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, peers, tt.expectedLength)
|
||||
actualPeerNames := make([]string, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
actualPeerNames = append(actualPeerNames, p.Name)
|
||||
}
|
||||
require.Equal(t, tt.expectedPeers, actualPeerNames)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -42,6 +42,22 @@ func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func runTestForAllEnginesNoSqliteSeed(t *testing.T, testDataFile string, f func(t *testing.T, engine string, store Store)) {
|
||||
t.Helper()
|
||||
for _, engine := range supportedEngines {
|
||||
if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) {
|
||||
continue
|
||||
}
|
||||
t.Setenv("NETBIRD_STORE_ENGINE", string(engine))
|
||||
store, cleanUp, err := NewTestStore(context.Background(), testDataFile, t.TempDir())
|
||||
assert.NoError(t, err, "engine: ", string(engine))
|
||||
t.Cleanup(cleanUp)
|
||||
assert.NoError(t, err)
|
||||
f(t, string(engine), store)
|
||||
os.Unsetenv("NETBIRD_STORE_ENGINE")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NewStore(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("The SQLite store is not properly supported by Windows yet")
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
|
||||
@@ -161,6 +162,7 @@ type Store interface {
|
||||
AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error
|
||||
GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error)
|
||||
GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error)
|
||||
GetAccountPeersPaginated(ctx context.Context, lockStrength LockingStrength, accountID string, pagination PaginationState, filters PeerFilters, sorting PeerSorting) ([]*nbpeer.Peer, int, error)
|
||||
GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error)
|
||||
GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) (*nbpeer.Peer, error)
|
||||
GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error)
|
||||
@@ -723,7 +725,7 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) (
|
||||
}
|
||||
|
||||
if filename != "" {
|
||||
err = LoadSQL(conn.DB(nil), filename)
|
||||
err = LoadSQL(conn.DB(nil), filename, translaterForSql(kind))
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, nil, fmt.Errorf("failed to load SQL file: %v", err)
|
||||
@@ -747,7 +749,7 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) (
|
||||
|
||||
maxRetries := 2
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
sqlStore, cleanup, err = getSqlStoreEngine(ctx, store, kind)
|
||||
sqlStore, cleanup, err = getSqlStoreEngine(ctx, store, kind, dataDir)
|
||||
if err == nil {
|
||||
return sqlStore, cleanup, nil
|
||||
}
|
||||
@@ -759,6 +761,59 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) (
|
||||
return nil, nil, fmt.Errorf("failed to create test store after %d attempts: %v", maxRetries, err)
|
||||
}
|
||||
|
||||
func NewTestStore(ctx context.Context, filename string, dataDir string) (Store, func(), error) {
|
||||
kind := getStoreEngineFromEnv()
|
||||
if kind == "" {
|
||||
kind = types.SqliteStoreEngine
|
||||
}
|
||||
|
||||
var sqlStore *SqlStore
|
||||
var cleanup func()
|
||||
var err error
|
||||
|
||||
maxRetries := 2
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
sqlStore, cleanup, err = getSqlStoreEngine(ctx, nil, kind, dataDir)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if i < maxRetries-1 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create test store after %d attempts: %v", maxRetries, err)
|
||||
}
|
||||
|
||||
if filename != "" {
|
||||
err = LoadSQL(sqlStore.db, filename, translaterForSql(kind))
|
||||
if err != nil {
|
||||
_ = sqlStore.Close(ctx)
|
||||
return nil, nil, fmt.Errorf("failed to load SQL file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = addAllGroupToAccount(ctx, sqlStore)
|
||||
if err != nil {
|
||||
_ = sqlStore.Close(ctx)
|
||||
return nil, nil, fmt.Errorf("failed to add all group to account: %v", err)
|
||||
}
|
||||
|
||||
return sqlStore, cleanup, nil
|
||||
}
|
||||
|
||||
// use to translate double-quote escaped reserved names (ansi)
|
||||
// to mysql tick-escaped ones
|
||||
func translaterForSql(kind types.Engine) *strings.Replacer {
|
||||
if kind != types.MysqlStoreEngine {
|
||||
return strings.NewReplacer()
|
||||
}
|
||||
return strings.NewReplacer(
|
||||
"\"key\"", "`key`",
|
||||
"\"groups\"", "`groups`",
|
||||
)
|
||||
}
|
||||
|
||||
func addAllGroupToAccount(ctx context.Context, store Store) error {
|
||||
allAccounts := store.GetAllAccounts(ctx)
|
||||
for _, account := range allAccounts {
|
||||
@@ -782,8 +837,8 @@ func addAllGroupToAccount(ctx context.Context, store Store) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSqlStoreEngine(ctx context.Context, sqliteStore *SqlStore, kind types.Engine) (Store, func(), error) {
|
||||
store := sqliteStore
|
||||
func getSqlStoreEngine(ctx context.Context, sqliteStore *SqlStore, kind types.Engine, dataDir string) (*SqlStore, func(), error) {
|
||||
var store *SqlStore
|
||||
var cleanup func()
|
||||
var err error
|
||||
switch kind {
|
||||
@@ -795,6 +850,15 @@ func getSqlStoreEngine(ctx context.Context, sqliteStore *SqlStore, kind types.En
|
||||
cleanup = func() {
|
||||
// sqlite doesn't need to be cleaned up
|
||||
}
|
||||
store = sqliteStore
|
||||
var conn *db.Conn
|
||||
if store == nil {
|
||||
conn, err = db.OpenSqliteFile(ctx, dataDir, db.SqliteFileName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create test store: %v", err)
|
||||
}
|
||||
store, err = NewSqlStore(ctx, conn, nil, false)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, cleanup, fmt.Errorf("failed to create test store: %v", err)
|
||||
@@ -803,7 +867,7 @@ func getSqlStoreEngine(ctx context.Context, sqliteStore *SqlStore, kind types.En
|
||||
closeConnection := func() {
|
||||
cleanup()
|
||||
store.Close(ctx)
|
||||
if store != sqliteStore {
|
||||
if sqliteStore != nil && store != sqliteStore {
|
||||
// The sqlite store only seeded the engine under test; without this
|
||||
// every test leaks its connection and the opener goroutines.
|
||||
sqliteStore.Close(ctx)
|
||||
@@ -813,7 +877,7 @@ func getSqlStoreEngine(ctx context.Context, sqliteStore *SqlStore, kind types.En
|
||||
return store, closeConnection, nil
|
||||
}
|
||||
|
||||
func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
|
||||
func newReusedPostgresStore(ctx context.Context, sqliteStore *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
|
||||
dsn, ok := lookupDSNEnv(PostgresDsnEnv, PostgresDsnEnvLegacy)
|
||||
if !ok || dsn == "" {
|
||||
var err error
|
||||
@@ -846,16 +910,25 @@ func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Eng
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
store, err = newPostgresqlStoreFromSqlStore(ctx, store, dsn, nil, true)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
var store *SqlStore
|
||||
if sqliteStore != nil {
|
||||
store, err = newPostgresqlStoreFromSqlStore(ctx, sqliteStore, dsn, nil, true)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
store, err = NewPostgresqlStoreForTests(ctx, dsn, nil, false)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return store, cleanup, nil
|
||||
}
|
||||
|
||||
func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
|
||||
func newReusedMysqlStore(ctx context.Context, sqliteStore *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
|
||||
dsn, ok := lookupDSNEnv(mysqlDsnEnv, mysqlDsnEnvLegacy)
|
||||
if !ok || dsn == "" {
|
||||
var err error
|
||||
@@ -900,10 +973,19 @@ func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind types.Engine
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
store, err = newMysqlStoreFromSqlStore(ctx, store, dsn, nil, true)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
var store *SqlStore
|
||||
if sqliteStore != nil {
|
||||
store, err = newMysqlStoreFromSqlStore(ctx, sqliteStore, dsn, nil, true)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
store, err = NewMysqlStore(ctx, dsn, nil, false)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return store, cleanup, nil
|
||||
@@ -1135,6 +1217,7 @@ func createRandomDB(dsn string, admin *gorm.DB, engine types.Engine, template st
|
||||
dropDB, err = gorm.Open(postgres.Open(originalDSN), &gorm.Config{
|
||||
SkipDefaultTransaction: true,
|
||||
PrepareStmt: false,
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to connect for dropping database %s: %v", dbName, err)
|
||||
@@ -1205,7 +1288,7 @@ func replaceDBName(dsn, newDBName string) string {
|
||||
return re.ReplaceAllString(dsn, `${pre}`+newDBName+`${post}`)
|
||||
}
|
||||
|
||||
func LoadSQL(db *gorm.DB, filepath string) error {
|
||||
func LoadSQL(db *gorm.DB, filepath string, translator *strings.Replacer) error {
|
||||
sqlContent, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1214,7 +1297,7 @@ func LoadSQL(db *gorm.DB, filepath string) error {
|
||||
queries := strings.Split(string(sqlContent), ";")
|
||||
|
||||
for _, query := range queries {
|
||||
query = strings.TrimSpace(query)
|
||||
query = translator.Replace(strings.TrimSpace(query))
|
||||
if query != "" {
|
||||
err := db.Exec(query).Error
|
||||
if err != nil {
|
||||
|
||||
@@ -1344,6 +1344,22 @@ func (mr *MockStoreMockRecorder) GetAccountPeers(ctx, lockStrength, accountID, n
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPeers", reflect.TypeOf((*MockStore)(nil).GetAccountPeers), ctx, lockStrength, accountID, nameFilter, ipFilter, macFilter)
|
||||
}
|
||||
|
||||
// GetAccountPeersPaginated mocks base method.
|
||||
func (m *MockStore) GetAccountPeersPaginated(ctx context.Context, lockStrength LockingStrength, accountID string, pagination PaginationState, filters PeerFilters, sorting PeerSorting) ([]*peer.Peer, int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAccountPeersPaginated", ctx, lockStrength, accountID, pagination, filters, sorting)
|
||||
ret0, _ := ret[0].([]*peer.Peer)
|
||||
ret1, _ := ret[1].(int)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// GetAccountPeersPaginated indicates an expected call of GetAccountPeersPaginated.
|
||||
func (mr *MockStoreMockRecorder) GetAccountPeersPaginated(ctx, lockStrength, accountID, pagination, filters, sorting any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPeersPaginated", reflect.TypeOf((*MockStore)(nil).GetAccountPeersPaginated), ctx, lockStrength, accountID, pagination, filters, sorting)
|
||||
}
|
||||
|
||||
// GetAccountPeersWithExpiration mocks base method.
|
||||
func (m *MockStore) GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*peer.Peer, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
insert into accounts (id, domain, network_identifier, network_net, network_net_v6, network_dns, network_serial,dns_settings_disabled_management_groups,
|
||||
settings_peer_login_expiration_enabled, settings_peer_login_expiration, settings_peer_inactivity_expiration_enabled,
|
||||
settings_peer_inactivity_expiration, settings_dns_domain, settings_ipv6_enabled_groups, settings_routing_peer_dns_resolution_enabled,
|
||||
settings_lazy_connection_enabled, settings_auto_update_version, settings_auto_update_always, settings_metrics_push_enabled)
|
||||
VALUES('account-1','account-1-test.com','network-1','{"IP":"100.103.0.0","Mask":"//8AAA=="}','{"IP":"fdde:e995:fd38:a465::","Mask":"//////////8AAAAAAAAAAA=="}','',1,'["disabled-group-1","disabled-group-2"]',
|
||||
true, 86400000000000, false,
|
||||
86400000000000, null, '["group-one-resource-id"]', false,
|
||||
false, 'disabled', false, false);
|
||||
insert into accounts (id, domain,network_identifier, network_net, network_net_v6, network_dns, network_serial,dns_settings_disabled_management_groups,
|
||||
settings_peer_login_expiration_enabled, settings_peer_login_expiration, settings_peer_inactivity_expiration_enabled,
|
||||
settings_peer_inactivity_expiration, settings_dns_domain, settings_ipv6_enabled_groups, settings_routing_peer_dns_resolution_enabled,
|
||||
settings_lazy_connection_enabled, settings_auto_update_version, settings_auto_update_always, settings_metrics_push_enabled)
|
||||
VALUES('account-2','account-2-test.com','network-2','{"IP":"110.0.0.0","Mask":"//8AAA=="}','{"IP":"fddf:e995:fd38:a465::","Mask":"//////////8AAAAAAAAAAA=="}','',2,null,
|
||||
true, 86400000000000, false,
|
||||
86400000000000, null, '["group-two-resources-id"]', false,
|
||||
false, 'disabled', false, false);
|
||||
insert into "groups" (id, account_id, name, resources, public_id) VALUES('group-one-resource-id','account-1','group-1-name', '[{"ID":"host-id-1","Type":"host"}]','group-one-resource-id-public');
|
||||
insert into "groups" (id, account_id, name, resources, public_id) VALUES('group-two-resources-id','account-1','group-2-name', '[{"ID":"subnet-id-1","Type":"subnet"}, {"ID":"host-id-2","Type":"host"}]','group-two-resources-id-public');
|
||||
insert into "groups" (id, account_id, name, resources, public_id) VALUES('group-no-resources-id','account-1','group-3-name', null,'group-no-resources-id-public');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-1','peer-id-one','group-one-resource-id');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-1','peer-id-one','group-two-resources-id');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-1','peer-id-two','group-two-resources-id');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-1','peer-id-three','group-no-resources-id');
|
||||
insert into peers (id, account_id, "key", ssh_key, name, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, peer_status_last_seen, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
meta_hostname,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-one','account-1','key-1','ssh-key-1','Peer-1-name','peer-1','["extra-peer-1"]','user-id-1',true,true,'2026-08-06 13:25:59.12999','"10.10.10.1"','"fdf4:ba80:6aa5:89f1:44d7:8701:8677:4940"',
|
||||
false,true,false,'2026-08-06 13:25:59.12999','cluster-1.netbird.services',
|
||||
'0.76.0','linux','26.4.1','6.8.0-134-generic','[{"NetIP":"fe80::8b4c:973f:a76b:3771/64","Mac":"00:15:5d:24:0c:ac"},{"NetIP":"192.168.16.1/20","Mac":"00:15:5d:24:0c:ac"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'peer-one-name.local', 'DE','Berlin','"46.201.148.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, name, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, peer_status_last_seen, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
meta_hostname,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-two','account-1','key-2','ssh-key-2','peer-2-name','peer-2','["extra-peer-2"]','user-id-2',true,true,'2026-08-06 14:25:59.12999','"10.10.100.1"','"fdf5:ba80:6aa5:89f1:44d7:8701:8698:4940"',
|
||||
true,true,false,'2026-08-06 12:25:59.12999','cluster-2.netbird.services',
|
||||
'0.76.1','ios','26.4.2','6.8.0-135-generic','[{"NetIP":"fe81::8b4c:973f:a76b:3771/64","Mac":"00:15:5d:24:0c:ad"},{"NetIP":"192.168.17.1/20","Mac":"00:15:5d:24:0c:ad"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',0,
|
||||
'peer-two-name.local', 'DE','Berlin','"46.201.149.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, name, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, peer_status_last_seen, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses,
|
||||
meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version, meta_hostname,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-three','account-1','key-3','ssh-key-3','peer-3-name','peer-3','["extra-peer-3"]','user-id-3',true,true,'2026-08-06 12:25:59.12999','"10.10.200.1"','"fdf6:ba80:6aa5:89f1:44d7:8701:8699:4940"',
|
||||
true,false,false,'2026-08-06 11:25:59.12999','cluster-3.netbird.services',
|
||||
'0.76.3','windows','26.4.3','6.8.0-136-generic','[{"NetIP":"fe82::8b4c:973f:a76b:3771/64","Mac":"00:15:5d:24:0c:ae"},{"NetIP":"192.168.18.1/20","Mac":"00:15:5d:24:0c:ae"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'peer-three-name.local','DE','Berlin','"46.201.150.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, name, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, peer_status_last_seen, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
meta_hostname,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-four','account-1','key-4','ssh-key-4','peer-4-name','peer-4','["extra-peer-4"]','',true,true,'2026-08-06 12:25:59.12999','"10.10.20.1"','"fdf6:ba80:6aa5:89f1:44d7:8701:8669:4940"',
|
||||
false,false,false,'2026-08-06 07:25:59.12999','cluster-4.netbird.services',
|
||||
'0.76.2','linux','26.4.3','6.8.0-136-generic','[{"NetIP":"fe82::8b4c:973f:a76b:3771/64","Mac":"00:15:7d:44:0c:ae"},{"NetIP":"192.168.18.1/20","Mac":"00:15:6d:34:0c:ae"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'peer-four-name.local','DE','Berlin','"46.201.150.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, name, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, peer_status_last_seen, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
meta_hostname,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-five','account-1','key-5','ssh-key-5','peer-5-name','peer-5','["extra-peer-5"]','',true,true,'2026-08-06 12:25:59.12999','"10.10.9.2"','"fdf6:ba80:6aa5:89f1:44d7:8701:8669:4941"',
|
||||
false,false,false,'2026-08-06 09:25:59.12999','cluster-4.netbird.services',
|
||||
'0.78.2','linux','26.4.3','6.8.0-136-generic','[{"NetIP":"fe82::8b4c:973f:a76b:3771/64","Mac":"00:15:7d:44:0c:ae"},{"NetIP":"192.168.18.1/20","Mac":"00:15:6d:34:0c:ae"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'peer-five-name.local','DE','Berlin','"46.201.150.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, name, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, peer_status_last_seen, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
meta_hostname,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-six','account-1','key-6','ssh-key-6','peer-6-name','peer-6','["extra-peer-6"]','',true,true,'2026-08-06 12:25:59.12999','"10.10.8.3"','"fdf6:ba80:6aa5:89f1:44d7:8701:8669:4942"',
|
||||
false,false,false,'2026-08-06 03:25:59.12999','cluster-4.netbird.services',
|
||||
'0.77.2','linux','26.4.3','6.8.0-136-generic','[{"NetIP":"fe82::8b4c:973f:a76b:3771/64","Mac":"00:15:7d:44:0c:ae"},{"NetIP":"192.168.18.1/20","Mac":"00:15:6d:34:0c:ae"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'peer-six-name.local','DE','Berlin','"46.201.150.187"');
|
||||
-- peer-id-8 should never appear in any of the results, as we are filtering out proxy peers
|
||||
insert into peers (id,account_id,"key", ssh_key, name, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, peer_status_last_seen, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
meta_hostname,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-8','account-1','key-8','ssh-key-8','peer-8-name','peer-8','["extra-peer-8"]','',true,true,'2026-08-06 12:25:59.12999','"10.10.88.3"','"fdf6:ba80:6aa5:89f1:44d7:8701:8669:4943"',
|
||||
false,false,true,'2026-08-06 03:25:59.12999','cluster-4.netbird.services',
|
||||
'0.77.2','linux','26.4.3','6.8.0-136-generic','[{"NetIP":"fe82::8b4c:973f:a76b:3771/64","Mac":"00:15:7d:44:0c:ae"},{"NetIP":"192.168.18.1/20","Mac":"00:15:6d:34:0c:ae"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'peer-eight-name.local','DE','Berlin','"46.201.150.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, name, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, peer_status_last_seen, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
meta_hostname,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-7','account-2','key-7','ssh-key-7','peer-7-name','peer-7','["extra-peer-7"]','',true,true,'2026-08-06 12:25:59.12999','"10.10.8.3"','"fdf6:ba80:6aa5:89f1:44d7:8701:8669:4942"',
|
||||
false,false,false,'2026-08-06 03:25:59.12999','cluster-4.netbird.services',
|
||||
'0.77.2','linux','26.4.3','6.8.0-136-generic','[{"NetIP":"fe82::8b4c:973f:a76b:3771/64","Mac":"00:15:7d:44:0c:ae"},{"NetIP":"192.168.18.1/20","Mac":"00:15:6d:34:0c:ae"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'peer-seven-name.local','DE','Berlin','"46.201.150.187"');
|
||||
Reference in New Issue
Block a user