mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-07 14:09:07 +02:00
[management] validate peer existence when adding to group (#7486)
This commit is contained in:
@@ -101,10 +101,8 @@ func (am *DefaultAccountManager) CreateGroup(ctx context.Context, accountID, use
|
|||||||
return status.Errorf(status.Internal, "failed to create group: %v", err)
|
return status.Errorf(status.Internal, "failed to create group: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, peerID := range newGroup.Peers {
|
if err = syncGroupMembership(ctx, transaction, accountID, newGroup.ID, newGroup.Peers, nil); err != nil {
|
||||||
if err := transaction.AddPeerToGroup(ctx, accountID, peerID, newGroup.ID); err != nil {
|
return err
|
||||||
return status.Errorf(status.Internal, "failed to add peer %s to group %s: %v", peerID, newGroup.ID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
snap, err = affectedpeers.Load(ctx, transaction, accountID, change)
|
snap, err = affectedpeers.Load(ctx, transaction, accountID, change)
|
||||||
@@ -200,6 +198,9 @@ func (am *DefaultAccountManager) UpdateGroup(ctx context.Context, accountID, use
|
|||||||
|
|
||||||
// syncGroupMembership applies the peer membership delta for a group within a transaction.
|
// syncGroupMembership applies the peer membership delta for a group within a transaction.
|
||||||
func syncGroupMembership(ctx context.Context, transaction store.Store, accountID, groupID string, peersToAdd, peersToRemove []string) error {
|
func syncGroupMembership(ctx context.Context, transaction store.Store, accountID, groupID string, peersToAdd, peersToRemove []string) error {
|
||||||
|
if err := validateGroupPeers(ctx, transaction, accountID, peersToAdd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, peerID := range peersToAdd {
|
for _, peerID := range peersToAdd {
|
||||||
if err := transaction.AddPeerToGroup(ctx, accountID, peerID, groupID); err != nil {
|
if err := transaction.AddPeerToGroup(ctx, accountID, peerID, groupID); err != nil {
|
||||||
return status.Errorf(status.Internal, "failed to add peer %s to group %s: %v", peerID, groupID, err)
|
return status.Errorf(status.Internal, "failed to add peer %s to group %s: %v", peerID, groupID, err)
|
||||||
@@ -213,6 +214,25 @@ func syncGroupMembership(ctx context.Context, transaction store.Store, accountID
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateGroupPeers(ctx context.Context, transaction store.Store, accountID string, peerIDs []string) error {
|
||||||
|
if len(peerIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := transaction.GetPeersByIDs(ctx, store.LockingStrengthNone, accountID, peerIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peerID := range peerIDs {
|
||||||
|
if _, ok := peers[peerID]; !ok {
|
||||||
|
return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateGroups adds new groups to the account.
|
// CreateGroups adds new groups to the account.
|
||||||
// Note: This function does not acquire the global lock.
|
// Note: This function does not acquire the global lock.
|
||||||
// It is the caller's responsibility to ensure proper locking is in place before invoking this method.
|
// It is the caller's responsibility to ensure proper locking is in place before invoking this method.
|
||||||
@@ -540,7 +560,7 @@ func (am *DefaultAccountManager) GroupAddPeer(ctx context.Context, accountID, gr
|
|||||||
change := affectedpeers.Change{OutputPeerIDs: []string{peerID}, LinkGroups: []string{groupID}}
|
change := affectedpeers.Change{OutputPeerIDs: []string{peerID}, LinkGroups: []string{groupID}}
|
||||||
|
|
||||||
err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||||
if err := transaction.AddPeerToGroup(ctx, accountID, peerID, groupID); err != nil {
|
if err := syncGroupMembership(ctx, transaction, accountID, groupID, []string{peerID}, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/mock/gomock"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
@@ -1236,3 +1236,82 @@ func Test_IncrementNetworkSerial(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, totalPeers, int(account.Network.Serial), "Expected %d serial increases in account %s, got %d", totalPeers, accountID, account.Network.Serial)
|
assert.Equal(t, totalPeers, int(account.Network.Serial), "Expected %d serial increases in account %s, got %d", totalPeers, accountID, account.Network.Serial)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultAccountManager_GroupPeersMustBelongToAccount(t *testing.T) {
|
||||||
|
manager, _, account, peer1, _, _ := setupNetworkMapTest(t)
|
||||||
|
|
||||||
|
otherAccount, err := createAccount(manager, "other_account", "other_user", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
foreignPeer := &peer2.Peer{
|
||||||
|
ID: "foreign-peer",
|
||||||
|
AccountID: otherAccount.Id,
|
||||||
|
Key: "foreign-key",
|
||||||
|
DNSLabel: "foreign-peer",
|
||||||
|
IP: uint32ToIP(1),
|
||||||
|
}
|
||||||
|
require.NoError(t, manager.Store.AddPeerToAccount(context.Background(), foreignPeer))
|
||||||
|
|
||||||
|
assertRejected := func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.Error(t, err)
|
||||||
|
s, ok := status.FromError(err)
|
||||||
|
require.True(t, ok, "expected status error, got %v", err)
|
||||||
|
assert.Equal(t, status.InvalidArgument, s.Type(), "peer outside the account should be rejected as invalid argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("create rejects foreign peer", func(t *testing.T) {
|
||||||
|
err := manager.CreateGroup(context.Background(), account.Id, userID, &types.Group{
|
||||||
|
Name: "foreign",
|
||||||
|
Issued: types.GroupIssuedAPI,
|
||||||
|
Peers: []string{peer1.ID, foreignPeer.ID},
|
||||||
|
})
|
||||||
|
assertRejected(t, err)
|
||||||
|
|
||||||
|
_, err = manager.Store.GetGroupByName(context.Background(), store.LockingStrengthNone, account.Id, "foreign")
|
||||||
|
assert.Error(t, err, "rejected create must not persist the group")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update rejects foreign and unknown peers", func(t *testing.T) {
|
||||||
|
group := &types.Group{ID: "own", Name: "own", Issued: types.GroupIssuedAPI, Peers: []string{peer1.ID}}
|
||||||
|
require.NoError(t, manager.CreateGroup(context.Background(), account.Id, userID, group))
|
||||||
|
|
||||||
|
group.Peers = []string{peer1.ID, foreignPeer.ID}
|
||||||
|
assertRejected(t, manager.UpdateGroup(context.Background(), account.Id, userID, group))
|
||||||
|
|
||||||
|
group.Peers = []string{peer1.ID, "does-not-exist"}
|
||||||
|
assertRejected(t, manager.UpdateGroup(context.Background(), account.Id, userID, group))
|
||||||
|
|
||||||
|
stored, err := manager.Store.GetGroupByID(context.Background(), store.LockingStrengthNone, account.Id, group.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{peer1.ID}, stored.Peers, "rejected updates must not change membership")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update tolerates and drops pre-existing dangling members", func(t *testing.T) {
|
||||||
|
group := &types.Group{ID: "polluted", Name: "polluted", Issued: types.GroupIssuedAPI, Peers: []string{peer1.ID}}
|
||||||
|
require.NoError(t, manager.CreateGroup(context.Background(), account.Id, userID, group))
|
||||||
|
require.NoError(t, manager.Store.AddPeerToGroup(context.Background(), account.Id, foreignPeer.ID, group.ID))
|
||||||
|
|
||||||
|
group.Peers = []string{peer1.ID, foreignPeer.ID}
|
||||||
|
assert.NoError(t, manager.UpdateGroup(context.Background(), account.Id, userID, group), "keeping an existing member must not be rejected")
|
||||||
|
|
||||||
|
group.Peers = []string{peer1.ID}
|
||||||
|
require.NoError(t, manager.UpdateGroup(context.Background(), account.Id, userID, group))
|
||||||
|
|
||||||
|
stored, err := manager.Store.GetGroupByID(context.Background(), store.LockingStrengthNone, account.Id, group.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{peer1.ID}, stored.Peers, "dangling member should be removed once omitted")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("direct add rejects foreign and unknown peers", func(t *testing.T) {
|
||||||
|
group := &types.Group{ID: "direct", Name: "direct", Issued: types.GroupIssuedAPI, Peers: []string{peer1.ID}}
|
||||||
|
require.NoError(t, manager.CreateGroup(context.Background(), account.Id, userID, group))
|
||||||
|
|
||||||
|
assertRejected(t, manager.GroupAddPeer(context.Background(), account.Id, group.ID, foreignPeer.ID))
|
||||||
|
assertRejected(t, manager.GroupAddPeer(context.Background(), account.Id, group.ID, "does-not-exist"))
|
||||||
|
|
||||||
|
stored, err := manager.Store.GetGroupByID(context.Background(), store.LockingStrengthNone, account.Id, group.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{peer1.ID}, stored.Peers, "rejected direct adds must not change membership")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3473,7 +3473,7 @@ func (s *SqlStore) GetPeerGroups(ctx context.Context, lockStrength LockingStreng
|
|||||||
var groups []*types.Group
|
var groups []*types.Group
|
||||||
query := tx.
|
query := tx.
|
||||||
Joins("JOIN group_peers ON group_peers.group_id = groups.id").
|
Joins("JOIN group_peers ON group_peers.group_id = groups.id").
|
||||||
Where("group_peers.peer_id = ?", peerId).
|
Where("groups.account_id = ? AND group_peers.peer_id = ?", accountId, peerId).
|
||||||
Preload(clause.Associations).
|
Preload(clause.Associations).
|
||||||
Find(&groups)
|
Find(&groups)
|
||||||
|
|
||||||
@@ -5053,7 +5053,7 @@ func (s *SqlStore) GetPeersByGroupIDs(ctx context.Context, accountID string, gro
|
|||||||
Select("DISTINCT peer_id").
|
Select("DISTINCT peer_id").
|
||||||
Where("account_id = ? AND group_id IN ?", accountID, groupIDs)
|
Where("account_id = ? AND group_id IN ?", accountID, groupIDs)
|
||||||
|
|
||||||
result := s.db.Where("id IN (?)", peerIDsSubquery).Find(&peers)
|
result := s.db.Where("account_id = ? AND id IN (?)", accountID, peerIDsSubquery).Find(&peers)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
log.WithContext(ctx).Errorf("failed to get peers by group IDs: %s", result.Error)
|
log.WithContext(ctx).Errorf("failed to get peers by group IDs: %s", result.Error)
|
||||||
return nil, status.Errorf(status.Internal, "failed to get peers by group IDs")
|
return nil, status.Errorf(status.Internal, "failed to get peers by group IDs")
|
||||||
|
|||||||
@@ -2844,6 +2844,14 @@ func TestSqlStore_GetPeerGroups(t *testing.T) {
|
|||||||
groups, err = store.GetPeerGroups(context.Background(), LockingStrengthNone, accountID, peerID)
|
groups, err = store.GetPeerGroups(context.Background(), LockingStrengthNone, accountID, peerID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, groups, 2)
|
assert.Len(t, groups, 2)
|
||||||
|
|
||||||
|
foreignPeerID := "foreign-peer"
|
||||||
|
err = store.AddPeerToGroup(context.Background(), accountID, foreignPeerID, "cfefqs706sqkneg59g4h")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
groups, err = store.GetPeerGroups(context.Background(), LockingStrengthNone, "other-account", foreignPeerID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, groups, "groups of another account must not be returned")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSqlStore_GetAccountPeers(t *testing.T) {
|
func TestSqlStore_GetAccountPeers(t *testing.T) {
|
||||||
@@ -4039,9 +4047,15 @@ func TestSqlStore_GetPeersByGroupIDs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, store.CreateGroups(ctx, accountID, groups))
|
require.NoError(t, store.CreateGroups(ctx, accountID, groups))
|
||||||
|
|
||||||
|
otherAccount := newAccountWithId(ctx, "other-account", "other-user", "")
|
||||||
|
require.NoError(t, store.SaveAccount(ctx, otherAccount))
|
||||||
|
foreignPeer := &nbpeer.Peer{ID: "foreign-peer", AccountID: otherAccount.Id}
|
||||||
|
require.NoError(t, store.AddPeerToAccount(ctx, foreignPeer))
|
||||||
|
|
||||||
require.NoError(t, store.AddPeerToGroup(ctx, accountID, peer1, group1ID))
|
require.NoError(t, store.AddPeerToGroup(ctx, accountID, peer1, group1ID))
|
||||||
require.NoError(t, store.AddPeerToGroup(ctx, accountID, peer2, group1ID))
|
require.NoError(t, store.AddPeerToGroup(ctx, accountID, peer2, group1ID))
|
||||||
require.NoError(t, store.AddPeerToGroup(ctx, accountID, peer1, group2ID))
|
require.NoError(t, store.AddPeerToGroup(ctx, accountID, peer1, group2ID))
|
||||||
|
require.NoError(t, store.AddPeerToGroup(ctx, accountID, foreignPeer.ID, group1ID))
|
||||||
|
|
||||||
peers, err := store.GetPeersByGroupIDs(ctx, accountID, tt.groupIDs)
|
peers, err := store.GetPeersByGroupIDs(ctx, accountID, tt.groupIDs)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user