mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +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)
|
||||
}
|
||||
|
||||
for _, peerID := range newGroup.Peers {
|
||||
if err := transaction.AddPeerToGroup(ctx, accountID, peerID, newGroup.ID); err != nil {
|
||||
return status.Errorf(status.Internal, "failed to add peer %s to group %s: %v", peerID, newGroup.ID, err)
|
||||
}
|
||||
if err = syncGroupMembership(ctx, transaction, accountID, newGroup.ID, newGroup.Peers, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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.
|
||||
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 {
|
||||
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)
|
||||
@@ -213,6 +214,25 @@ func syncGroupMembership(ctx context.Context, transaction store.Store, accountID
|
||||
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.
|
||||
// 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.
|
||||
@@ -540,7 +560,7 @@ func (am *DefaultAccountManager) GroupAddPeer(ctx context.Context, accountID, gr
|
||||
change := affectedpeers.Change{OutputPeerIDs: []string{peerID}, LinkGroups: []string{groupID}}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
query := tx.
|
||||
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).
|
||||
Find(&groups)
|
||||
|
||||
@@ -5053,7 +5053,7 @@ func (s *SqlStore) GetPeersByGroupIDs(ctx context.Context, accountID string, gro
|
||||
Select("DISTINCT peer_id").
|
||||
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 {
|
||||
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")
|
||||
|
||||
@@ -2844,6 +2844,14 @@ func TestSqlStore_GetPeerGroups(t *testing.T) {
|
||||
groups, err = store.GetPeerGroups(context.Background(), LockingStrengthNone, accountID, peerID)
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
@@ -4039,9 +4047,15 @@ func TestSqlStore_GetPeersByGroupIDs(t *testing.T) {
|
||||
}
|
||||
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, peer2, group1ID))
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user