diff --git a/management/internals/controllers/network_map/controller/controller.go b/management/internals/controllers/network_map/controller/controller.go index f21f878a4..d72ba439d 100644 --- a/management/internals/controllers/network_map/controller/controller.go +++ b/management/internals/controllers/network_map/controller/controller.go @@ -577,7 +577,7 @@ func peerPostureChecksFromData(nmData *networkmap.NetworkMapData, peerID string) if policy == nil || !policy.Enabled || len(policy.SourcePostureChecks) == 0 { continue } - if !isPeerInPolicySourceGroupsFromData(nmData, peerID, policy) { + if !isPeerInPolicySourcesFromData(nmData, peerID, policy) { continue } for _, checkID := range policy.SourcePostureChecks { @@ -590,11 +590,14 @@ func peerPostureChecksFromData(nmData *networkmap.NetworkMapData, peerID string) return maps.Values(peerPostureChecks) } -func isPeerInPolicySourceGroupsFromData(nmData *networkmap.NetworkMapData, peerID string, policy *nmdata.Policy) bool { +func isPeerInPolicySourcesFromData(nmData *networkmap.NetworkMapData, peerID string, policy *nmdata.Policy) bool { for _, rule := range policy.Rules { if rule == nil || !rule.Enabled { continue } + if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID == peerID { + return true + } for _, groupID := range rule.Sources { if group := nmData.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) { return true @@ -1314,7 +1317,7 @@ func computeForwarderPortFromVersions(wtVersions []string, requiredVersion strin // addPolicyPostureChecks adds posture checks from a policy to the peer posture checks map if the peer is in the policy's source groups. func addPolicyPostureChecks(account *types.Account, peerID string, policy *types.Policy, peerPostureChecks map[string]*posture.Checks) error { - isInGroup, err := isPeerInPolicySourceGroups(account, peerID, policy) + isInGroup, err := isPeerInPolicySources(account, peerID, policy) if err != nil { return err } @@ -1334,13 +1337,17 @@ func addPolicyPostureChecks(account *types.Account, peerID string, policy *types return nil } -// isPeerInPolicySourceGroups checks if a peer is present in any of the policy rule source groups. -func isPeerInPolicySourceGroups(account *types.Account, peerID string, policy *types.Policy) (bool, error) { +// isPeerInPolicySources checks if a peer is a source of the policy, directly or through a source group. +func isPeerInPolicySources(account *types.Account, peerID string, policy *types.Policy) (bool, error) { for _, rule := range policy.Rules { if !rule.Enabled { continue } + if rule.SourceResource.Type == types.ResourceTypePeer && rule.SourceResource.ID == peerID { + return true, nil + } + for _, sourceGroup := range rule.Sources { group := account.GetGroup(sourceGroup) if group == nil { diff --git a/management/internals/controllers/network_map/controller/posture_twin_test.go b/management/internals/controllers/network_map/controller/posture_twin_test.go index d5c9035e0..98e0991d0 100644 --- a/management/internals/controllers/network_map/controller/posture_twin_test.go +++ b/management/internals/controllers/network_map/controller/posture_twin_test.go @@ -4,35 +4,65 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/shared/management/networkmap" "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" + "github.com/netbirdio/netbird/shared/management/types" ) -func TestPeerPostureChecksFromData_ReturnsTwinsUnchanged(t *testing.T) { - check := &nmdata.PostureChecks{ - ID: "pc1", - Checks: nmdata.ChecksDefinition{ - NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: "0.30.0"}, - OSVersionCheck: &nmdata.OSVersionCheck{Linux: &nmdata.MinKernelVersionCheck{MinKernelVersion: "6.1"}}, +func postureSelectionData(policies ...*nmdata.Policy) *networkmap.NetworkMapData { + return &networkmap.NetworkMapData{ + Groups: map[string]*nmdata.Group{"g-src": {ID: "g-src", Peers: []string{"peer-group"}}}, + Policies: policies, + PostureChecks: map[string]*nmdata.PostureChecks{ + "pc1": {ID: "pc1", Checks: nmdata.ChecksDefinition{NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: "0.30.0"}}}, }, } - nmData := &networkmap.NetworkMapData{ - Groups: map[string]*nmdata.Group{"g1": {ID: "g1", Peers: []string{"peer1"}}}, - Policies: []*nmdata.Policy{{ - ID: "policy1", - Enabled: true, - SourcePostureChecks: []string{"pc1"}, - Rules: []*nmdata.PolicyRule{{ID: "rule1", Enabled: true, Sources: []string{"g1"}}}, - }}, - PostureChecks: map[string]*nmdata.PostureChecks{"pc1": check}, - } - - got := peerPostureChecksFromData(nmData, "peer1") - require.Len(t, got, 1) - assert.Same(t, check, got[0]) - assert.Len(t, got[0].GetChecks(), 2) - - assert.Empty(t, peerPostureChecksFromData(nmData, "peer-outside-source-group")) +} + +func gatedPolicy(id string, rule *nmdata.PolicyRule, checkIDs ...string) *nmdata.Policy { + return &nmdata.Policy{ID: id, Enabled: true, SourcePostureChecks: checkIDs, Rules: []*nmdata.PolicyRule{rule}} +} + +func checkIDs(checks []*nmdata.PostureChecks) []string { + ids := make([]string, 0, len(checks)) + for _, c := range checks { + ids = append(ids, c.ID) + } + return ids +} + +func TestPeerPostureChecksFromData_SelectsPolicySourcePeers(t *testing.T) { + groupRule := &nmdata.PolicyRule{ID: "r-group", Enabled: true, Sources: []string{"g-src"}, Destinations: []string{"g-dst"}} + directRule := &nmdata.PolicyRule{ID: "r-direct", Enabled: true, SourceResource: nmdata.Resource{ID: "peer-direct", Type: string(types.ResourceTypePeer)}, Destinations: []string{"g-dst"}} + + t.Run("source group member and direct source peer both get the checks", func(t *testing.T) { + nmData := postureSelectionData(gatedPolicy("p1", groupRule, "pc1"), gatedPolicy("p2", directRule, "pc1")) + + assert.Equal(t, []string{"pc1"}, checkIDs(peerPostureChecksFromData(nmData, "peer-group"))) + assert.Equal(t, []string{"pc1"}, checkIDs(peerPostureChecksFromData(nmData, "peer-direct"))) + assert.Empty(t, peerPostureChecksFromData(nmData, "peer-elsewhere")) + }) + + t.Run("source resource of a non-peer type never matches a peer", func(t *testing.T) { + hostRule := &nmdata.PolicyRule{ID: "r-host", Enabled: true, SourceResource: nmdata.Resource{ID: "peer-direct", Type: string(types.ResourceTypeHost)}, Destinations: []string{"g-dst"}} + nmData := postureSelectionData(gatedPolicy("p1", hostRule, "pc1")) + + assert.Empty(t, peerPostureChecksFromData(nmData, "peer-direct")) + }) + + t.Run("same check through two policies is returned once", func(t *testing.T) { + nmData := postureSelectionData(gatedPolicy("p1", groupRule, "pc1"), gatedPolicy("p2", groupRule, "pc1")) + + assert.Equal(t, []string{"pc1"}, checkIDs(peerPostureChecksFromData(nmData, "peer-group"))) + }) + + t.Run("disabled policy, disabled rule and dangling check are ignored", func(t *testing.T) { + disabledPolicy := gatedPolicy("p-off", groupRule, "pc1") + disabledPolicy.Enabled = false + disabledRule := &nmdata.PolicyRule{ID: "r-off", Enabled: false, Sources: []string{"g-src"}} + nmData := postureSelectionData(disabledPolicy, gatedPolicy("p-rule-off", disabledRule, "pc1"), gatedPolicy("p-dangling", groupRule, "pc-missing")) + + assert.Empty(t, peerPostureChecksFromData(nmData, "peer-group")) + }) } diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/case.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/case.json new file mode 100644 index 000000000..cdf31c413 --- /dev/null +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/case.json @@ -0,0 +1,5 @@ +{ + "description": "A peer named directly as a rule source or destination is subject to approval exactly like a group member: unvalidated peer-b is neither a source for peer-c nor a destination for peer-a, while the validated direct source peer-a reaches peer-c. Legacy mode is excluded: the frozen legacynmap copy still carries the direct-peer bypass.", + "peers": ["peer-a", "peer-c"], + "modes": ["full", "envelope"] +} diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/golden/peer-a.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/golden/peer-a.json new file mode 100644 index 000000000..e0605525f --- /dev/null +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/golden/peer-a.json @@ -0,0 +1,64 @@ +{ + "Serial": "22", + "peerConfig": { + "address": "100.64.0.1/10", + "sshConfig": {}, + "fqdn": "peer-a.netbird.test", + "autoUpdate": {} + }, + "remotePeers": [ + { + "wgPubKey": "4deEImv8zGvsyBmmfC2G0eQkbyMzyGuz/YK7pcYETwM=", + "allowedIps": [ + "100.64.0.3/32" + ], + "sshConfig": {}, + "fqdn": "peer-c.netbird.test", + "agentVersion": "0.60.0" + } + ], + "DNSConfig": { + "ServiceEnable": true, + "CustomZones": [ + { + "Domain": "netbird.test.", + "Records": [ + { + "Name": "peer-a.netbird.test", + "Type": "1", + "Class": "IN", + "TTL": "300", + "RData": "100.64.0.1" + }, + { + "Name": "peer-c.netbird.test", + "Type": "1", + "Class": "IN", + "TTL": "300", + "RData": "100.64.0.3" + } + ] + } + ], + "ForwarderPort": "22054" + }, + "FirewallRules": [ + { + "PeerIP": "100.64.0.3", + "Protocol": "TCP", + "Port": "443", + "PolicyID": "cG9sLWRpcmVjdC1vaw==" + }, + { + "PeerIP": "100.64.0.3", + "Direction": "OUT", + "Protocol": "TCP", + "Port": "443", + "PolicyID": "cG9sLWRpcmVjdC1vaw==" + } + ], + "routesFirewallRulesIsEmpty": true, + "sshAuth": { + "UserIDClaim": "sub" + } +} diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/golden/peer-c.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/golden/peer-c.json new file mode 100644 index 000000000..f2b3e9357 --- /dev/null +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/golden/peer-c.json @@ -0,0 +1,64 @@ +{ + "Serial": "22", + "peerConfig": { + "address": "100.64.0.3/10", + "sshConfig": {}, + "fqdn": "peer-c.netbird.test", + "autoUpdate": {} + }, + "remotePeers": [ + { + "wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=", + "allowedIps": [ + "100.64.0.1/32" + ], + "sshConfig": {}, + "fqdn": "peer-a.netbird.test", + "agentVersion": "0.60.0" + } + ], + "DNSConfig": { + "ServiceEnable": true, + "CustomZones": [ + { + "Domain": "netbird.test.", + "Records": [ + { + "Name": "peer-a.netbird.test", + "Type": "1", + "Class": "IN", + "TTL": "300", + "RData": "100.64.0.1" + }, + { + "Name": "peer-c.netbird.test", + "Type": "1", + "Class": "IN", + "TTL": "300", + "RData": "100.64.0.3" + } + ] + } + ], + "ForwarderPort": "22054" + }, + "FirewallRules": [ + { + "PeerIP": "100.64.0.1", + "Protocol": "TCP", + "Port": "443", + "PolicyID": "cG9sLWRpcmVjdC1vaw==" + }, + { + "PeerIP": "100.64.0.1", + "Direction": "OUT", + "Protocol": "TCP", + "Port": "443", + "PolicyID": "cG9sLWRpcmVjdC1vaw==" + } + ], + "routesFirewallRulesIsEmpty": true, + "sshAuth": { + "UserIDClaim": "sub" + } +} diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/nmdata.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/nmdata.json new file mode 100644 index 000000000..283df304c --- /dev/null +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/policy-direct-peer-unvalidated/nmdata.json @@ -0,0 +1,63 @@ +{ + "Network": {"Serial": 22}, + "Peers": { + "peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}}, + "peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}}, + "peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}} + }, + "ValidatedPeers": {"peer-a": {}, "peer-c": {}}, + "Groups": { + "grp-dev": {"Peers": ["peer-a"]}, + "grp-ops": {"Peers": ["peer-c"]} + }, + "Policies": [ + { + "ID": "pol-direct-ok", + "PublicID": "pol-direct-ok-pub", + "Enabled": true, + "Rules": [ + { + "Enabled": true, + "Action": "accept", + "Protocol": "tcp", + "Ports": ["443"], + "Bidirectional": true, + "SourceResource": {"ID": "peer-a", "Type": "peer"}, + "Destinations": ["grp-ops"] + } + ] + }, + { + "ID": "pol-src-unval", + "PublicID": "pol-src-unval-pub", + "Enabled": true, + "Rules": [ + { + "Enabled": true, + "Action": "accept", + "Protocol": "tcp", + "Ports": ["8443"], + "Bidirectional": true, + "SourceResource": {"ID": "peer-b", "Type": "peer"}, + "Destinations": ["grp-ops"] + } + ] + }, + { + "ID": "pol-dst-unval", + "PublicID": "pol-dst-unval-pub", + "Enabled": true, + "Rules": [ + { + "Enabled": true, + "Action": "accept", + "Protocol": "tcp", + "Ports": ["9443"], + "Bidirectional": true, + "Sources": ["grp-dev"], + "DestinationResource": {"ID": "peer-b", "Type": "peer"} + } + ] + } + ] +} diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/case.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/case.json new file mode 100644 index 000000000..8d7460721 --- /dev/null +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/case.json @@ -0,0 +1,5 @@ +{ + "description": "A peer named directly as a rule source is gated by the policy's posture checks exactly like a group member: peer-b (0.40.0) fails the 0.45.0 minimum, so it gets no connectivity and peer-c must not see it, while the compliant direct source peer-a reaches peer-c. Legacy mode is excluded: the frozen legacynmap copy still carries the direct-peer bypass.", + "peers": ["peer-b", "peer-c"], + "modes": ["full", "envelope"] +} diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/golden/peer-b.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/golden/peer-b.json new file mode 100644 index 000000000..240358e40 --- /dev/null +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/golden/peer-b.json @@ -0,0 +1,33 @@ +{ + "Serial": "21", + "peerConfig": { + "address": "100.64.0.2/10", + "sshConfig": {}, + "fqdn": "peer-b.netbird.test", + "autoUpdate": {} + }, + "remotePeersIsEmpty": true, + "DNSConfig": { + "ServiceEnable": true, + "CustomZones": [ + { + "Domain": "netbird.test.", + "Records": [ + { + "Name": "peer-b.netbird.test", + "Type": "1", + "Class": "IN", + "TTL": "300", + "RData": "100.64.0.2" + } + ] + } + ], + "ForwarderPort": "5353" + }, + "firewallRulesIsEmpty": true, + "routesFirewallRulesIsEmpty": true, + "sshAuth": { + "UserIDClaim": "sub" + } +} diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/golden/peer-c.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/golden/peer-c.json new file mode 100644 index 000000000..85573ed35 --- /dev/null +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/golden/peer-c.json @@ -0,0 +1,64 @@ +{ + "Serial": "21", + "peerConfig": { + "address": "100.64.0.3/10", + "sshConfig": {}, + "fqdn": "peer-c.netbird.test", + "autoUpdate": {} + }, + "remotePeers": [ + { + "wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=", + "allowedIps": [ + "100.64.0.1/32" + ], + "sshConfig": {}, + "fqdn": "peer-a.netbird.test", + "agentVersion": "0.60.0" + } + ], + "DNSConfig": { + "ServiceEnable": true, + "CustomZones": [ + { + "Domain": "netbird.test.", + "Records": [ + { + "Name": "peer-a.netbird.test", + "Type": "1", + "Class": "IN", + "TTL": "300", + "RData": "100.64.0.1" + }, + { + "Name": "peer-c.netbird.test", + "Type": "1", + "Class": "IN", + "TTL": "300", + "RData": "100.64.0.3" + } + ] + } + ], + "ForwarderPort": "5353" + }, + "FirewallRules": [ + { + "PeerIP": "100.64.0.1", + "Protocol": "TCP", + "Port": "443", + "PolicyID": "cG9sLWRpcmVjdC1vaw==" + }, + { + "PeerIP": "100.64.0.1", + "Direction": "OUT", + "Protocol": "TCP", + "Port": "443", + "PolicyID": "cG9sLWRpcmVjdC1vaw==" + } + ], + "routesFirewallRulesIsEmpty": true, + "sshAuth": { + "UserIDClaim": "sub" + } +} diff --git a/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/nmdata.json b/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/nmdata.json new file mode 100644 index 000000000..e6b99bfdd --- /dev/null +++ b/management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/nmdata.json @@ -0,0 +1,51 @@ +{ + "Network": {"Serial": 21}, + "Peers": { + "peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}}, + "peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.40.0"}}, + "peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}} + }, + "Groups": { + "grp-ops": {"Peers": ["peer-c"]} + }, + "PostureChecks": { + "chk-ver": {"Checks": {"NBVersionCheck": {"MinVersion": "0.45.0"}}} + }, + "PostureCheckXIDToPublicID": {"chk-ver": "chk-ver-pub"}, + "Policies": [ + { + "ID": "pol-direct-ok", + "PublicID": "pol-direct-ok-pub", + "Enabled": true, + "SourcePostureChecks": ["chk-ver"], + "Rules": [ + { + "Enabled": true, + "Action": "accept", + "Protocol": "tcp", + "Ports": ["443"], + "Bidirectional": true, + "SourceResource": {"ID": "peer-a", "Type": "peer"}, + "Destinations": ["grp-ops"] + } + ] + }, + { + "ID": "pol-direct-denied", + "PublicID": "pol-direct-denied-pub", + "Enabled": true, + "SourcePostureChecks": ["chk-ver"], + "Rules": [ + { + "Enabled": true, + "Action": "accept", + "Protocol": "tcp", + "Ports": ["8443"], + "Bidirectional": true, + "SourceResource": {"ID": "peer-b", "Type": "peer"}, + "Destinations": ["grp-ops"] + } + ] + } + ] +} diff --git a/management/server/affected_peers_router_paths_test.go b/management/server/affected_peers_router_paths_test.go index 7fef1ab35..d5868a5c1 100644 --- a/management/server/affected_peers_router_paths_test.go +++ b/management/server/affected_peers_router_paths_test.go @@ -146,7 +146,7 @@ func TestAffectedPeers_GroupAddResource_RefreshesRoutingPeer(t *testing.T) { assert.NotContains(t, affected, s.unrelatedPeerID, "unrelated peer must not be affected") } -func (s *routerScenario) createPostureCheckGatedPolicy(t *testing.T, ctx context.Context) string { +func (s *routerScenario) createPostureCheckGatedPolicy(t *testing.T, ctx context.Context, policy *types.Policy) string { t.Helper() check, err := s.manager.SavePostureChecks(ctx, s.accountID, userID, &posture.Checks{ @@ -157,7 +157,6 @@ func (s *routerScenario) createPostureCheckGatedPolicy(t *testing.T, ctx context }, true) require.NoError(t, err) - policy := peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID) policy.SourcePostureChecks = []string{check.ID} _, err = s.manager.SavePolicy(ctx, s.accountID, userID, policy, true) require.NoError(t, err) @@ -169,7 +168,7 @@ func TestAffectedPeers_E2E_SavePostureCheck_RefreshesRoutingPeer(t *testing.T) { s := setupRouterScenario(t, true) ctx := context.Background() - checkID := s.createPostureCheckGatedPolicy(t, ctx) + checkID := s.createPostureCheckGatedPolicy(t, ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID)) srcCh := s.updateManager.CreateChannel(ctx, s.sourcePeerID) routerCh := s.updateManager.CreateChannel(ctx, s.routerPeerID) @@ -347,11 +346,28 @@ func TestAffectedPeers_PeerChange_RouterInOtherNetworkNotAffected(t *testing.T) // shortcut (the denied peer's map holds no router) and the allow direction // depends on which meta field moved, leaving the routers with a stale map. func TestAffectedPeers_E2E_PostureFlip_RefreshesRoutingPeer(t *testing.T) { + runPostureFlipRefreshesRoutingPeer(t, func(s *routerScenario) *types.Policy { + return peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID) + }) +} + +// TestAffectedPeers_E2E_PostureFlip_DirectSourcePeer_RefreshesRoutingPeer is the same +// scenario with the source peer named directly in the rule: it must receive its posture +// checks and have its flips detected exactly like a group member. +func TestAffectedPeers_E2E_PostureFlip_DirectSourcePeer_RefreshesRoutingPeer(t *testing.T) { + runPostureFlipRefreshesRoutingPeer(t, func(s *routerScenario) *types.Policy { + return peerToResourcePolicyByPeer(s.sourcePeerID, s.resourceGroupID) + }) +} + +func runPostureFlipRefreshesRoutingPeer(t *testing.T, policyFor func(s *routerScenario) *types.Policy) { + t.Helper() + manager, updateManager := createManagerWithNetworkMapStore(t) s := buildRouterScenario(t, manager, updateManager, true) ctx := context.Background() - s.createPostureCheckGatedPolicy(t, ctx) + s.createPostureCheckGatedPolicy(t, ctx, policyFor(s)) source, err := s.manager.Store.GetPeerByID(ctx, store.LockingStrengthNone, s.accountID, s.sourcePeerID) require.NoError(t, err) diff --git a/management/server/affected_peers_router_test.go b/management/server/affected_peers_router_test.go index 9ecfaed69..7e3f02b27 100644 --- a/management/server/affected_peers_router_test.go +++ b/management/server/affected_peers_router_test.go @@ -173,6 +173,23 @@ func peerToResourcePolicyByGroup(sourceGroupID, resourceGroupID string) *types.P } } +// peerToResourcePolicyByPeer builds a policy naming the source peer directly via +// SourceResource rather than through a group. +func peerToResourcePolicyByPeer(sourcePeerID, resourceGroupID string) *types.Policy { + return &types.Policy{ + Enabled: true, + Name: "peer-to-resource-by-peer", + Rules: []*types.PolicyRule{ + { + Enabled: true, + SourceResource: types.Resource{ID: sourcePeerID, Type: types.ResourceTypePeer}, + Destinations: []string{resourceGroupID}, + Action: types.PolicyTrafficActionAccept, + }, + }, + } +} + // peerToResourcePolicyByResource builds a policy referencing the resource // directly via DestinationResource rather than its group. func peerToResourcePolicyByResource(sourceGroupID, resourceID string) *types.Policy { diff --git a/management/server/peer.go b/management/server/peer.go index 87ca57c2b..07619f51e 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -1349,7 +1349,7 @@ func getPeerLoginInfo(ctx context.Context, transaction store.Store, accountID st return nil, nil, false, err } - postureChecks, err := getPeerPostureChecks(ctx, transaction, accountID, peerGroupIDs, policies) + postureChecks, err := getPeerPostureChecks(ctx, transaction, accountID, peer.ID, peerGroupIDs, policies) if err != nil { return nil, nil, false, err } @@ -1371,7 +1371,7 @@ func isPeerSSHEnabled(ctx context.Context, peer *nbpeer.Peer, policies []*types. } // getPeerPostureChecks returns the posture checks for the peer. -func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountID string, peerGroupIDs []string, policies []*types.Policy) ([]*nmdata.PostureChecks, error) { +func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountID, peerID string, peerGroupIDs []string, policies []*types.Policy) ([]*nmdata.PostureChecks, error) { if len(policies) == 0 { return nil, nil } @@ -1383,7 +1383,7 @@ func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountI continue } - postureChecksIDs := processPeerPostureChecks(policy, peerGroupIDs) + postureChecksIDs := processPeerPostureChecks(policy, peerID, peerGroupIDs) peerPostureChecksIDs = append(peerPostureChecksIDs, postureChecksIDs...) } @@ -1395,13 +1395,17 @@ func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountI return types.TwinPostureChecksList(maps.Values(peerPostureChecks)), nil } -// processPeerPostureChecks checks if the peer is in the source group of the policy and returns the posture checks. -func processPeerPostureChecks(policy *types.Policy, peerGroupIDs []string) []string { +// processPeerPostureChecks returns the policy's posture checks when the peer is a source of the policy, directly or through a source group. +func processPeerPostureChecks(policy *types.Policy, peerID string, peerGroupIDs []string) []string { for _, rule := range policy.Rules { if !rule.Enabled { continue } + if rule.SourceResource.Type == types.ResourceTypePeer && rule.SourceResource.ID == peerID { + return policy.SourcePostureChecks + } + for _, sourceGroup := range rule.Sources { if slices.Contains(peerGroupIDs, sourceGroup) { return policy.SourcePostureChecks diff --git a/management/server/peer_posture_test.go b/management/server/peer_posture_test.go index 6b298f5d1..88662e2fa 100644 --- a/management/server/peer_posture_test.go +++ b/management/server/peer_posture_test.go @@ -9,6 +9,7 @@ import ( nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" ) @@ -181,3 +182,22 @@ func TestMetaDiffAffectsPosture_NoChecks(t *testing.T) { ) assert.False(t, metaDiffAffectsPosture(diff, nil)) } + +func TestProcessPeerPostureChecks(t *testing.T) { + policy := &types.Policy{ + Enabled: true, + SourcePostureChecks: []string{"pc1"}, + Rules: []*types.PolicyRule{ + {Enabled: false, Sources: []string{"g-disabled"}, SourceResource: types.Resource{ID: "peer-disabled", Type: types.ResourceTypePeer}}, + {Enabled: true, Sources: []string{"g-src"}, Destinations: []string{"g-dst"}}, + {Enabled: true, SourceResource: types.Resource{ID: "peer-direct", Type: types.ResourceTypePeer}, Destinations: []string{"g-dst"}}, + {Enabled: true, SourceResource: types.Resource{ID: "peer-as-host", Type: types.ResourceTypeHost}, Destinations: []string{"g-dst"}}, + }, + } + + assert.Equal(t, []string{"pc1"}, processPeerPostureChecks(policy, "peer-in-group", []string{"g-src"}), "source group member") + assert.Equal(t, []string{"pc1"}, processPeerPostureChecks(policy, "peer-direct", nil), "direct source peer") + assert.Empty(t, processPeerPostureChecks(policy, "peer-elsewhere", []string{"g-dst"}), "destination-only peer") + assert.Empty(t, processPeerPostureChecks(policy, "peer-disabled", []string{"g-disabled"}), "disabled rule") + assert.Empty(t, processPeerPostureChecks(policy, "peer-as-host", nil), "source resource of a non-peer type") +} diff --git a/management/server/types/account.go b/management/server/types/account.go index 522bb8be6..d689b0175 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -909,13 +909,13 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P var peerInSources, peerInDestinations bool if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" { - sourcePeers, peerInSources = a.getPeerFromResource(rule.SourceResource, peer.ID) + sourcePeers, peerInSources = a.getPeerFromResource(ctx, rule.SourceResource, peer.ID, policy.SourcePostureChecks, validatedPeersMap) } else { sourcePeers, peerInSources = a.getAllPeersFromGroups(ctx, rule.Sources, peer.ID, policy.SourcePostureChecks, validatedPeersMap) } if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" { - destinationPeers, peerInDestinations = a.getPeerFromResource(rule.DestinationResource, peer.ID) + destinationPeers, peerInDestinations = a.getPeerFromResource(ctx, rule.DestinationResource, peer.ID, nil, validatedPeersMap) } else { destinationPeers, peerInDestinations = a.getAllPeersFromGroups(ctx, rule.Destinations, peer.ID, nil, validatedPeersMap) } @@ -1120,8 +1120,17 @@ func ruleHasDestination(rule *PolicyRule, peerID string, peerGroupIDs map[string // Important: Posture checks are applicable only to source group peers, // for destination group peers, call this method with an empty list of sourcePostureChecksIDs func (a *Account) getAllPeersFromGroups(ctx context.Context, groups []string, peerID string, sourcePostureChecksIDs []string, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, bool) { + return a.filterPolicyPeers(ctx, a.getUniquePeerIDsFromGroupsIDs(ctx, groups), peerID, sourcePostureChecksIDs, validatedPeersMap) +} + +// getPeerFromResource resolves a rule side that names a peer directly, admitting it +// like a member of a group holding only that peer. +func (a *Account) getPeerFromResource(ctx context.Context, resource Resource, peerID string, sourcePostureChecksIDs []string, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, bool) { + return a.filterPolicyPeers(ctx, []string{resource.ID}, peerID, sourcePostureChecksIDs, validatedPeersMap) +} + +func (a *Account) filterPolicyPeers(ctx context.Context, uniquePeerIDs []string, peerID string, sourcePostureChecksIDs []string, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, bool) { peerInGroups := false - uniquePeerIDs := a.getUniquePeerIDsFromGroupsIDs(ctx, groups) filteredPeers := make([]*nbpeer.Peer, 0, len(uniquePeerIDs)) for _, p := range uniquePeerIDs { peer, ok := a.Peers[p] @@ -1150,19 +1159,6 @@ func (a *Account) getAllPeersFromGroups(ctx context.Context, groups []string, pe return filteredPeers, peerInGroups } -func (a *Account) getPeerFromResource(resource Resource, peerID string) ([]*nbpeer.Peer, bool) { - peer := a.GetPeer(resource.ID) - if peer == nil { - return []*nbpeer.Peer{}, false - } - - if peer.ID == peerID { - return []*nbpeer.Peer{}, true - } - - return []*nbpeer.Peer{peer}, false -} - // validatePostureChecksOnPeer validates the posture checks on a peer func (a *Account) validatePostureChecksOnPeer(ctx context.Context, sourcePostureChecksID []string, peerID string) bool { peer, ok := a.Peers[peerID] diff --git a/management/server/types/networkmap_components_correctness_test.go b/management/server/types/networkmap_components_correctness_test.go index eb3e4fe3b..35b5f7149 100644 --- a/management/server/types/networkmap_components_correctness_test.go +++ b/management/server/types/networkmap_components_correctness_test.go @@ -875,6 +875,89 @@ func TestComponents_PeerAsSourceResource(t *testing.T) { assert.True(t, has443, "peer-0 as source resource should have port 443 rule") } +func hasFirewallRuleTo(nm *types.NetworkMap, peerIP, port string) bool { + for _, rule := range nm.FirewallRules { + if rule.PeerIP == peerIP && rule.Port == port { + return true + } + } + return false +} + +// TestComponents_PeerAsSourceResource_PostureChecks verifies that a directly referenced +// source peer is gated by the policy's posture checks like a member of a group holding only +// that peer: peer-1 (0.25.0) fails the 0.26.0 minimum, peer-2 (0.40.0) passes. +func TestComponents_PeerAsSourceResource_PostureChecks(t *testing.T) { + account, validatedPeers := scalableTestAccountWithoutDefaultPolicy(20, 2) + + for _, sourcePeerID := range []string{"peer-1", "peer-2"} { + account.Policies = append(account.Policies, &types.Policy{ + ID: "policy-peer-src-" + sourcePeerID, Name: "Peer Source " + sourcePeerID, Enabled: true, AccountID: "test-account", + SourcePostureChecks: []string{"posture-check-ver"}, + Rules: []*types.PolicyRule{{ + ID: "rule-peer-src-" + sourcePeerID, Enabled: true, + Action: types.PolicyTrafficActionAccept, + Protocol: types.PolicyRuleProtocolTCP, + Bidirectional: true, + Ports: []string{"9443"}, + SourceResource: types.Resource{ID: sourcePeerID, Type: types.ResourceTypePeer}, + Destinations: []string{"group-0"}, + }}, + }) + } + + nm0 := componentsNetworkMap(account, "peer-0", validatedPeers) + require.NotNil(t, nm0) + assert.False(t, hasFirewallRuleTo(nm0, "100.64.0.1", "9443"), "destination must not see the direct source peer failing the posture check") + assert.True(t, hasFirewallRuleTo(nm0, "100.64.0.2", "9443"), "destination must see the direct source peer passing the posture check") + + nm1 := componentsNetworkMap(account, "peer-1", validatedPeers) + require.NotNil(t, nm1) + assert.False(t, hasFirewallRuleTo(nm1, "100.64.0.0", "9443"), "a direct source peer failing the posture check gets no policy connectivity") + + nm2 := componentsNetworkMap(account, "peer-2", validatedPeers) + require.NotNil(t, nm2) + assert.True(t, hasFirewallRuleTo(nm2, "100.64.0.0", "9443"), "a direct source peer passing the posture check gets policy connectivity") +} + +// TestComponents_PeerAsResource_Unvalidated verifies that a directly referenced peer is +// subject to approval like a group member, whether it is the rule's source or destination. +func TestComponents_PeerAsResource_Unvalidated(t *testing.T) { + account, validatedPeers := scalableTestAccountWithoutDefaultPolicy(20, 2) + delete(validatedPeers, "peer-2") + + account.Policies = append(account.Policies, + &types.Policy{ + ID: "policy-unval-src", Name: "Unvalidated Source", Enabled: true, AccountID: "test-account", + Rules: []*types.PolicyRule{{ + ID: "rule-unval-src", Enabled: true, + Action: types.PolicyTrafficActionAccept, Protocol: types.PolicyRuleProtocolTCP, Bidirectional: true, + Ports: []string{"9443"}, + SourceResource: types.Resource{ID: "peer-2", Type: types.ResourceTypePeer}, + Destinations: []string{"group-0"}, + }}, + }, + &types.Policy{ + ID: "policy-unval-dst", Name: "Unvalidated Destination", Enabled: true, AccountID: "test-account", + Rules: []*types.PolicyRule{{ + ID: "rule-unval-dst", Enabled: true, + Action: types.PolicyTrafficActionAccept, Protocol: types.PolicyRuleProtocolTCP, Bidirectional: true, + Ports: []string{"9444"}, + Sources: []string{"group-0"}, + DestinationResource: types.Resource{ID: "peer-2", Type: types.ResourceTypePeer}, + }}, + }, + ) + + nm0 := componentsNetworkMap(account, "peer-0", validatedPeers) + require.NotNil(t, nm0) + assert.False(t, hasFirewallRuleTo(nm0, "100.64.0.2", "9443"), "an unvalidated direct source peer must not be admitted") + assert.False(t, hasFirewallRuleTo(nm0, "100.64.0.2", "9444"), "an unvalidated direct destination peer must not be admitted") + for _, p := range nm0.Peers { + assert.NotEqual(t, "peer-2", p.ID, "an unvalidated direct peer must not be shipped as a remote peer") + } +} + // TestComponents_PeerAsDestinationResource verifies that a policy with DestinationResource.Type=Peer // targets only that specific peer as the destination. func TestComponents_PeerAsDestinationResource(t *testing.T) { diff --git a/shared/management/networkmap/networkmapcompute.go b/shared/management/networkmap/networkmapcompute.go index 1cf7aeef4..65e76d097 100644 --- a/shared/management/networkmap/networkmapcompute.go +++ b/shared/management/networkmap/networkmapcompute.go @@ -324,19 +324,13 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes( var peerInSources, peerInDestinations bool if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID != "" { - sourcePeers = []string{rule.SourceResource.ID} - if rule.SourceResource.ID == peerID { - peerInSources = true - } + sourcePeers, peerInSources = nmd.getPeerFromResource(rule.SourceResource, peerID, policy.SourcePostureChecks, postureFailedPeers) } else { sourcePeers, peerInSources = nmd.getPeersFromGroups(rule.Sources, peerID, policy.SourcePostureChecks, postureFailedPeers) } if rule.DestinationResource.Type == string(types.ResourceTypePeer) && rule.DestinationResource.ID != "" { - destinationPeers = []string{rule.DestinationResource.ID} - if rule.DestinationResource.ID == peerID { - peerInDestinations = true - } + destinationPeers, peerInDestinations = nmd.getPeerFromResource(rule.DestinationResource, peerID, nil, postureFailedPeers) } else { destinationPeers, peerInDestinations = nmd.getPeersFromGroups(rule.Destinations, peerID, nil, postureFailedPeers) } @@ -403,30 +397,16 @@ func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, so filteredPeerIDs = make([]string, 0, len(group.Peers)) peerInGroups = false for _, pid := range group.Peers { - peer, ok := nmd.Peers[pid] - if !ok || peer == nil { + if !nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) { continue } - if _, ok := nmd.ValidatedPeers[peer.ID]; !ok { - continue - } - - isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, peer.ID) - if !isValid && len(pname) > 0 { - if _, ok := (*postureFailedPeers)[pname]; !ok { - (*postureFailedPeers)[pname] = make(map[string]struct{}) - } - (*postureFailedPeers)[pname][peer.ID] = struct{}{} - continue - } - - if peer.ID == peerID { + if pid == peerID { peerInGroups = true continue } - filteredPeerIDs = append(filteredPeerIDs, peer.ID) + filteredPeerIDs = append(filteredPeerIDs, pid) } return filteredPeerIDs, peerInGroups } @@ -436,36 +416,59 @@ func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, so continue } seenPeerIds[pid] = struct{}{} - peer, ok := nmd.Peers[pid] - if !ok || peer == nil { + if !nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) { continue } - if _, ok := nmd.ValidatedPeers[peer.ID]; !ok { - continue - } - - isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, peer.ID) - if !isValid && len(pname) > 0 { - if _, ok := (*postureFailedPeers)[pname]; !ok { - (*postureFailedPeers)[pname] = make(map[string]struct{}) - } - (*postureFailedPeers)[pname][peer.ID] = struct{}{} - continue - } - - if peer.ID == peerID { + if pid == peerID { peerInGroups = true continue } - filteredPeerIDs = append(filteredPeerIDs, peer.ID) + filteredPeerIDs = append(filteredPeerIDs, pid) } } return filteredPeerIDs, peerInGroups } +// getPeerFromResource resolves a rule side that names a peer directly, admitting it +// like a member of a group holding only that peer. +func (nmd *NetworkMapData) getPeerFromResource(resource nmdata.Resource, peerID string, sourcePostureChecksIDs []string, + postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) { + if !nmd.admitPolicyPeer(resource.ID, sourcePostureChecksIDs, postureFailedPeers) { + return nil, false + } + if resource.ID == peerID { + return nil, true + } + return []string{resource.ID}, false +} + +// admitPolicyPeer applies the per-peer admission of a rule side: the peer must exist, +// be validated and pass the rule's posture checks. A failed check is recorded in +// postureFailedPeers. +func (nmd *NetworkMapData) admitPolicyPeer(pid string, sourcePostureChecksIDs []string, postureFailedPeers *map[string]map[string]struct{}) bool { + peer, ok := nmd.Peers[pid] + if !ok || peer == nil { + return false + } + + if _, ok := nmd.ValidatedPeers[pid]; !ok { + return false + } + + isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, pid) + if !isValid && len(pname) > 0 { + if _, ok := (*postureFailedPeers)[pname]; !ok { + (*postureFailedPeers)[pname] = make(map[string]struct{}) + } + (*postureFailedPeers)[pname][pid] = struct{}{} + return false + } + return true +} + func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChecksID []string, peerID string) (bool, string) { peer, ok := nmd.Peers[peerID] if !ok || peer == nil { diff --git a/shared/management/networkmap/networkmapcompute_test.go b/shared/management/networkmap/networkmapcompute_test.go index 8c9add8c1..ad21fd70f 100644 --- a/shared/management/networkmap/networkmapcompute_test.go +++ b/shared/management/networkmap/networkmapcompute_test.go @@ -448,10 +448,9 @@ func TestGetPeerNetworkMapComponents_PeerResourceRules(t *testing.T) { assert.ElementsMatch(t, []string{targetID, remote.ID}, peerIDSet(c.Peers)) }) - // Legacy parity: directly referenced peers bypass the ValidatedPeers gate - // and posture checks that group-derived peers go through; the client-side - // Calculate shares this behavior via getPeerFromResource. - t.Run("unvalidated source resource peer still connects", func(t *testing.T) { + // A directly referenced peer is admitted like a member of a group holding only + // that peer: the ValidatedPeers gate and the posture checks apply equally. + t.Run("unvalidated source resource peer is excluded", func(t *testing.T) { target := newPeer(targetID, 1) unval := newPeer("peer-unval", 2) nmd := newNMD(target, unval) @@ -463,10 +462,10 @@ func TestGetPeerNetworkMapComponents_PeerResourceRules(t *testing.T) { c := compute(nmd, targetID) - assert.ElementsMatch(t, []string{targetID, unval.ID}, peerIDSet(c.Peers)) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) }) - t.Run("source resource peer bypasses posture checks", func(t *testing.T) { + t.Run("source resource peer failing posture checks is excluded", func(t *testing.T) { target := newPeer(targetID, 1) failing := newPeer("peer-failing", 2) failing.Meta.WtVersion = failingVersion @@ -481,10 +480,65 @@ func TestGetPeerNetworkMapComponents_PeerResourceRules(t *testing.T) { c := compute(nmd, targetID) - assert.ElementsMatch(t, []string{targetID, failing.ID}, peerIDSet(c.Peers)) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) assert.Empty(t, c.PostureFailedPeers) }) + t.Run("direct source peer failure recorded when connected via another policy", func(t *testing.T) { + target := newPeer(targetID, 1) + failing := newPeer("peer-failing", 2) + failing.Meta.WtVersion = failingVersion + nmd := newNMD(target, failing) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-dst", targetID) + checkedRule := newRule(nil, []string{"g-dst"}) + checkedRule.SourceResource = peerResource(failing.ID) + checked := newPolicy("p-checked", checkedRule) + checked.SourcePostureChecks = []string{"pc-1"} + openRule := newRule(nil, []string{"g-dst"}) + openRule.SourceResource = peerResource(failing.ID) + nmd.Policies = []*nmdata.Policy{checked, newPolicy("p-open", openRule)} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, failing.ID}, peerIDSet(c.Peers)) + assert.Equal(t, map[string]map[string]struct{}{"pc-1": {failing.ID: {}}}, c.PostureFailedPeers) + }) + + t.Run("target as source resource failing posture checks gets no policy", func(t *testing.T) { + target := newPeer(targetID, 1) + target.Meta.WtVersion = failingVersion + dst := newPeer("peer-dst", 2) + nmd := newNMD(target, dst) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-dst", dst.ID) + rule := newRule(nil, []string{"g-dst"}) + rule.SourceResource = peerResource(targetID) + p := newPolicy("p-1", rule) + p.SourcePostureChecks = []string{"pc-1"} + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.Empty(t, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) + + t.Run("unvalidated destination resource peer is excluded", func(t *testing.T) { + target := newPeer(targetID, 1) + unval := newPeer("peer-unval", 2) + nmd := newNMD(target, unval) + delete(nmd.ValidatedPeers, unval.ID) + addGroup(nmd, "g-src", targetID) + rule := newRule([]string{"g-src"}, nil) + rule.DestinationResource = peerResource(unval.ID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", rule)} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) + t.Run("unrelated peer resource rule ignored", func(t *testing.T) { target := newPeer(targetID, 1) a := newPeer("peer-a", 2) diff --git a/shared/management/types/networkmap_components.go b/shared/management/types/networkmap_components.go index d008ece83..e18db4ec0 100644 --- a/shared/management/types/networkmap_components.go +++ b/shared/management/types/networkmap_components.go @@ -230,13 +230,13 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ( var peerInSources, peerInDestinations bool if rule.SourceResource.Type == string(ResourceTypePeer) && rule.SourceResource.ID != "" { - sourcePeers, peerInSources = c.getPeerFromResource(rule.SourceResource, targetPeerID) + sourcePeers, peerInSources = c.getPeerFromResource(rule.SourceResource, targetPeerID, policy.SourcePostureChecks) } else { sourcePeers, peerInSources = c.getAllPeersFromGroups(rule.Sources, targetPeerID, policy.SourcePostureChecks) } if rule.DestinationResource.Type == string(ResourceTypePeer) && rule.DestinationResource.ID != "" { - destinationPeers, peerInDestinations = c.getPeerFromResource(rule.DestinationResource, targetPeerID) + destinationPeers, peerInDestinations = c.getPeerFromResource(rule.DestinationResource, targetPeerID, nil) } else { destinationPeers, peerInDestinations = c.getAllPeersFromGroups(rule.Destinations, targetPeerID, nil) } @@ -373,8 +373,21 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nmdata.Peer) ( } func (c *NetworkMapComponents) getAllPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string) ([]*nmdata.Peer, bool) { + return c.filterPolicyPeers(c.getUniquePeerIDsFromGroupsIDs(groups), peerID, sourcePostureChecksIDs) +} + +// getPeerFromResource resolves a rule side that names a peer directly. The peer is +// subject to the same admission as a group member, so a direct peer behaves exactly +// like a group holding only that peer. +func (c *NetworkMapComponents) getPeerFromResource(resource nmdata.Resource, peerID string, sourcePostureChecksIDs []string) ([]*nmdata.Peer, bool) { + return c.filterPolicyPeers([]string{resource.ID}, peerID, sourcePostureChecksIDs) +} + +// filterPolicyPeers admits the peers of one rule side: known to the components and +// passing the rule's posture checks. It reports the admitted peers other than peerID +// and whether peerID itself is admitted on that side. +func (c *NetworkMapComponents) filterPolicyPeers(uniquePeerIDs []string, peerID string, sourcePostureChecksIDs []string) ([]*nmdata.Peer, bool) { peerInGroups := false - uniquePeerIDs := c.getUniquePeerIDsFromGroupsIDs(groups) filteredPeers := make([]*nmdata.Peer, 0, len(uniquePeerIDs)) for _, p := range uniquePeerIDs { @@ -427,19 +440,6 @@ func (c *NetworkMapComponents) getUniquePeerIDsFromGroupsIDs(groups []string) [] return ids } -func (c *NetworkMapComponents) getPeerFromResource(resource nmdata.Resource, peerID string) ([]*nmdata.Peer, bool) { - if resource.ID == peerID { - return []*nmdata.Peer{}, true - } - - peerInfo := c.GetPeerInfo(resource.ID) - if peerInfo == nil { - return []*nmdata.Peer{}, false - } - - return []*nmdata.Peer{peerInfo}, false -} - func (c *NetworkMapComponents) filterPeersByLoginExpiration(aclPeers []*nmdata.Peer) ([]*nmdata.Peer, []*nmdata.Peer) { peersToConnect := make([]*nmdata.Peer, 0, len(aclPeers)) var expiredPeers []*nmdata.Peer