diff --git a/hscontrol/policy/route_approval_test.go b/hscontrol/policy/route_approval_test.go new file mode 100644 index 00000000..90d5f98e --- /dev/null +++ b/hscontrol/policy/route_approval_test.go @@ -0,0 +1,809 @@ +package policy + +import ( + "fmt" + "net/netip" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestNodeCanApproveRoute(t *testing.T) { + users := []types.User{ + {Name: "user1", Model: gorm.Model{ID: 1}}, + {Name: "user2", Model: gorm.Model{ID: 2}}, + {Name: "user3", Model: gorm.Model{ID: 3}}, + } + + // Create standard node setups used across tests + normalNode := types.Node{ + ID: 1, + Hostname: "user1-device", + IPv4: ap("100.64.0.1"), + UserID: 1, + User: users[0], + } + + exitNode := types.Node{ + ID: 2, + Hostname: "user2-device", + IPv4: ap("100.64.0.2"), + UserID: 2, + User: users[1], + } + + taggedNode := types.Node{ + ID: 3, + Hostname: "tagged-server", + IPv4: ap("100.64.0.3"), + UserID: 3, + User: users[2], + ForcedTags: []string{"tag:router"}, + } + + multiTagNode := types.Node{ + ID: 4, + Hostname: "multi-tag-node", + IPv4: ap("100.64.0.4"), + UserID: 2, + User: users[1], + ForcedTags: []string{"tag:router", "tag:server"}, + } + + tests := []struct { + name string + node types.Node + route netip.Prefix + policy string + canApprove bool + skipV1 bool + }{ + { + name: "allow-all-routes-for-admin-user", + node: normalNode, + route: p("192.168.1.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "192.168.0.0/16": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "deny-route-that-doesnt-match-autoApprovers", + node: normalNode, + route: p("10.0.0.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "192.168.0.0/16": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "user-not-in-group", + node: exitNode, + route: p("192.168.1.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "192.168.0.0/16": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "tagged-node-can-approve", + node: taggedNode, + route: p("10.0.0.0/8"), + policy: `{ + "tagOwners": { + "tag:router": ["user3@"] + }, + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "10.0.0.0/8": ["tag:router"] + } + } + }`, + canApprove: true, + }, + { + name: "multiple-routes-in-policy", + node: normalNode, + route: p("172.16.10.0/24"), + policy: `{ + "tagOwners": { + "tag:router": ["user3@"] + }, + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "192.168.0.0/16": ["group:admin"], + "172.16.0.0/12": ["group:admin"], + "10.0.0.0/8": ["tag:router"] + } + } + }`, + canApprove: true, + }, + { + name: "match-specific-route-within-range", + node: normalNode, + route: p("192.168.5.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "192.168.0.0/16": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "ip-address-within-range", + node: normalNode, + route: p("192.168.1.5/32"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "192.168.1.0/24": ["group:admin"], + "192.168.1.128/25": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "all-IPv4-routes-(0.0.0.0/0)-approval", + node: normalNode, + route: p("0.0.0.0/0"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "0.0.0.0/0": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "all-IPv4-routes-exitnode-approval", + node: normalNode, + route: p("0.0.0.0/0"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "exitNode": ["group:admin"] + } + }`, + canApprove: true, + }, + { + name: "all-IPv6-routes-exitnode-approval", + node: normalNode, + route: p("::/0"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "exitNode": ["group:admin"] + } + }`, + canApprove: true, + }, + { + name: "specific-IPv4-route-with-exitnode-only-approval", + node: normalNode, + route: p("192.168.1.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "exitNode": ["group:admin"] + } + }`, + canApprove: false, + }, + { + name: "specific-IPv6-route-with-exitnode-only-approval", + node: normalNode, + route: p("fd00::/8"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "exitNode": ["group:admin"] + } + }`, + canApprove: false, + }, + { + name: "specific-IPv4-route-with-all-routes-policy", + node: normalNode, + route: p("10.0.0.0/8"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "0.0.0.0/0": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "all-IPv6-routes-(::0/0)-approval", + node: normalNode, + route: p("::/0"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "::/0": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "specific-IPv6-route-with-all-routes-policy", + node: normalNode, + route: p("fd00::/8"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "::/0": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "IPv6-route-with-IPv4-all-routes-policy", + node: normalNode, + route: p("fd00::/8"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "0.0.0.0/0": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "IPv4-route-with-IPv6-all-routes-policy", + node: normalNode, + route: p("10.0.0.0/8"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "::/0": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "both-IPv4-and-IPv6-all-routes-policy", + node: normalNode, + route: p("192.168.1.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "0.0.0.0/0": ["group:admin"], + "::/0": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "ip-address-with-all-routes-policy", + node: normalNode, + route: p("192.168.101.5/32"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "0.0.0.0/0": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "specific-IPv6-host-route-with-all-routes-policy", + node: normalNode, + route: p("2001:db8::1/128"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "::/0": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "multiple-groups-allowed-to-approve-same-route", + node: normalNode, + route: p("192.168.1.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"], + "group:netadmin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "192.168.1.0/24": ["group:admin", "group:netadmin"] + } + } + }`, + canApprove: true, + }, + { + name: "overlapping-routes-with-different-groups", + node: normalNode, + route: p("192.168.1.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"], + "group:restricted": ["user2@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "192.168.0.0/16": ["group:restricted"], + "192.168.1.0/24": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "unique-local-IPv6-address-with-all-routes-policy", + node: normalNode, + route: p("fc00::/7"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "::/0": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "exact-prefix-match-in-policy", + node: normalNode, + route: p("203.0.113.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "203.0.113.0/24": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "narrower-range-than-policy", + node: normalNode, + route: p("203.0.113.0/26"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "203.0.113.0/24": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "wider-range-than-policy-should-fail", + node: normalNode, + route: p("203.0.113.0/23"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "203.0.113.0/24": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "adjacent-route-to-policy-route-should-fail", + node: normalNode, + route: p("203.0.114.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "203.0.113.0/24": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "combined-routes-and-exitnode-approvers-specific-route", + node: normalNode, + route: p("192.168.1.0/24"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "exitNode": ["group:admin"], + "routes": { + "192.168.1.0/24": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "partly-overlapping-route-with-policy-should-fail", + node: normalNode, + route: p("203.0.113.128/23"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "203.0.113.0/24": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "multiple-routes-with-aggregatable-ranges", + node: normalNode, + route: p("10.0.0.0/8"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "10.0.0.0/9": ["group:admin"], + "10.128.0.0/9": ["group:admin"] + } + } + }`, + canApprove: false, + }, + { + name: "non-standard-IPv6-notation", + node: normalNode, + route: p("2001:db8::1/128"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "2001:db8::/32": ["group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "node-with-multiple-tags-all-required", + node: multiTagNode, + route: p("10.10.0.0/16"), + policy: `{ + "tagOwners": { + "tag:router": ["user2@"], + "tag:server": ["user2@"] + }, + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "10.10.0.0/16": ["tag:router", "tag:server"] + } + } + }`, + canApprove: true, + }, + { + name: "node-with-multiple-tags-one-matching-is-sufficient", + node: multiTagNode, + route: p("10.10.0.0/16"), + policy: `{ + "tagOwners": { + "tag:router": ["user2@"], + "tag:server": ["user2@"] + }, + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "10.10.0.0/16": ["tag:router", "group:admin"] + } + } + }`, + canApprove: true, + }, + { + name: "node-with-multiple-tags-missing-required-tag", + node: multiTagNode, + route: p("10.10.0.0/16"), + policy: `{ + "tagOwners": { + "tag:othertag": ["user1@"] + }, + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "10.10.0.0/16": ["tag:othertag"] + } + } + }`, + canApprove: false, + }, + { + name: "node-with-tag-and-group-membership", + node: normalNode, + route: p("10.20.0.0/16"), + policy: `{ + "tagOwners": { + "tag:router": ["user3@"] + }, + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "routes": { + "10.20.0.0/16": ["group:admin", "tag:router"] + } + } + }`, + canApprove: true, + }, + { + name: "small-subnet-with-exitnode-only-approval", + node: normalNode, + route: p("192.168.1.1/32"), + policy: `{ + "groups": { + "group:admin": ["user1@"] + }, + "acls": [ + {"action": "accept", "src": ["group:admin"], "dst": ["*:*"]} + ], + "autoApprovers": { + "exitNode": ["group:admin"] + } + }`, + canApprove: false, + }, + { + name: "empty-policy", + node: normalNode, + route: p("192.168.1.0/24"), + policy: `{"acls":[{"action":"accept","src":["*"],"dst":["*:*"]}]}`, + canApprove: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize all policy manager implementations + policyManagers, err := PolicyManagersForTest([]byte(tt.policy), users, types.Nodes{&tt.node}) + if tt.name == "empty policy" { + // We expect this one to have a valid but empty policy + require.NoError(t, err) + if err != nil { + return + } + } else { + require.NoError(t, err) + } + + for i, pm := range policyManagers { + versionNum := i + 1 + if versionNum == 1 && tt.skipV1 { + // Skip V1 policy manager for specific tests + continue + } + + t.Run(fmt.Sprintf("PolicyV%d", versionNum), func(t *testing.T) { + result := pm.NodeCanApproveRoute(&tt.node, tt.route) + + if diff := cmp.Diff(tt.canApprove, result); diff != "" { + t.Errorf("NodeCanApproveRoute() mismatch (-want +got):\n%s", diff) + } + assert.Equal(t, tt.canApprove, result, "Unexpected route approval result") + }) + } + }) + } +} diff --git a/hscontrol/policy/v2/policy.go b/hscontrol/policy/v2/policy.go index 4dec2bd4..80235354 100644 --- a/hscontrol/policy/v2/policy.go +++ b/hscontrol/policy/v2/policy.go @@ -31,6 +31,8 @@ type PolicyManager struct { tagOwnerMapHash deephash.Sum tagOwnerMap map[Tag]*netipx.IPSet + exitSetHash deephash.Sum + exitSet *netipx.IPSet autoApproveMapHash deephash.Sum autoApproveMap map[netip.Prefix]*netipx.IPSet @@ -97,7 +99,7 @@ func (pm *PolicyManager) updateLocked() (bool, error) { pm.tagOwnerMap = tagMap pm.tagOwnerMapHash = tagOwnerMapHash - autoMap, err := resolveAutoApprovers(pm.pol, pm.users, pm.nodes) + autoMap, exitSet, err := resolveAutoApprovers(pm.pol, pm.users, pm.nodes) if err != nil { return false, fmt.Errorf("resolving auto approvers map: %w", err) } @@ -107,8 +109,13 @@ func (pm *PolicyManager) updateLocked() (bool, error) { pm.autoApproveMap = autoMap pm.autoApproveMapHash = autoApproveMapHash + exitSetHash := deephash.Hash(&autoMap) + exitSetChanged := exitSetHash != pm.exitSetHash + pm.exitSet = exitSet + pm.exitSetHash = exitSetHash + // If neither of the calculated values changed, no need to update nodes - if !filterChanged && !tagOwnerChanged && !autoApproveChanged { + if !filterChanged && !tagOwnerChanged && !autoApproveChanged && !exitSetChanged { return false, nil } @@ -207,6 +214,23 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi return false } + // If the route to-be-approved is an exit route, then we need to check + // if the node is in allowed to approve it. This is treated differently + // than the auto-approvers, as the auto-approvers are not allowed to + // approve the whole /0 range. + // However, an auto approver might be /0, meaning that they can approve + // all routes available, just not exit nodes. + if tsaddr.IsExitRoute(route) { + if pm.exitSet == nil { + return false + } + if slices.ContainsFunc(node.IPs(), pm.exitSet.Contains) { + return true + } + + return false + } + pm.mu.Lock() defer pm.mu.Unlock() @@ -224,14 +248,6 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi // cannot just lookup in the prefix map and have to check // if there is a "parent" prefix available. for prefix, approveAddrs := range pm.autoApproveMap { - // We do not want the exit node entry to approve all - // sorts of routes. The logic here is that it would be - // unexpected behaviour to have specific routes approved - // just because the node is allowed to designate itself as - // an exit. - if tsaddr.IsExitRoute(prefix) { - continue - } // Check if prefix is larger (so containing) and then overlaps // the route to see if the node can approve a subset of an autoapprover diff --git a/hscontrol/policy/v2/types.go b/hscontrol/policy/v2/types.go index 78b1fdbe..a49f55de 100644 --- a/hscontrol/policy/v2/types.go +++ b/hscontrol/policy/v2/types.go @@ -862,10 +862,11 @@ type AutoApproverPolicy struct { // resolveAutoApprovers resolves the AutoApprovers to a map of netip.Prefix to netipx.IPSet. // The resulting map can be used to quickly look up if a node can self-approve a route. // It is intended for internal use in a PolicyManager. -func resolveAutoApprovers(p *Policy, users types.Users, nodes types.Nodes) (map[netip.Prefix]*netipx.IPSet, error) { +func resolveAutoApprovers(p *Policy, users types.Users, nodes types.Nodes) (map[netip.Prefix]*netipx.IPSet, *netipx.IPSet, error) { if p == nil { - return nil, nil + return nil, nil, nil } + var err error routes := make(map[netip.Prefix]*netipx.IPSetBuilder) @@ -877,7 +878,7 @@ func resolveAutoApprovers(p *Policy, users types.Users, nodes types.Nodes) (map[ aa, ok := autoApprover.(Alias) if !ok { // Should never happen - return nil, fmt.Errorf("autoApprover %v is not an Alias", autoApprover) + return nil, nil, fmt.Errorf("autoApprover %v is not an Alias", autoApprover) } // If it does not resolve, that means the autoApprover is not associated with any IP addresses. ips, _ := aa.Resolve(p, users, nodes) @@ -891,7 +892,7 @@ func resolveAutoApprovers(p *Policy, users types.Users, nodes types.Nodes) (map[ aa, ok := autoApprover.(Alias) if !ok { // Should never happen - return nil, fmt.Errorf("autoApprover %v is not an Alias", autoApprover) + return nil, nil, fmt.Errorf("autoApprover %v is not an Alias", autoApprover) } // If it does not resolve, that means the autoApprover is not associated with any IP addresses. ips, _ := aa.Resolve(p, users, nodes) @@ -903,22 +904,20 @@ func resolveAutoApprovers(p *Policy, users types.Users, nodes types.Nodes) (map[ for prefix, builder := range routes { ipSet, err := builder.IPSet() if err != nil { - return nil, err + return nil, nil, err } ret[prefix] = ipSet } + var exitNodeSet *netipx.IPSet if len(p.AutoApprovers.ExitNode) > 0 { - exitNodeSet, err := exitNodeSetBuilder.IPSet() + exitNodeSet, err = exitNodeSetBuilder.IPSet() if err != nil { - return nil, err + return nil, nil, err } - - ret[tsaddr.AllIPv4()] = exitNodeSet - ret[tsaddr.AllIPv6()] = exitNodeSet } - return ret, nil + return ret, exitNodeSet, nil } type ACL struct { diff --git a/hscontrol/policy/v2/types_test.go b/hscontrol/policy/v2/types_test.go index c25c14a9..3808b547 100644 --- a/hscontrol/policy/v2/types_test.go +++ b/hscontrol/policy/v2/types_test.go @@ -1024,10 +1024,11 @@ func TestResolveAutoApprovers(t *testing.T) { } tests := []struct { - name string - policy *Policy - want map[netip.Prefix]*netipx.IPSet - wantErr bool + name string + policy *Policy + want map[netip.Prefix]*netipx.IPSet + wantAllIPRoutes *netipx.IPSet + wantErr bool }{ { name: "single-route", @@ -1041,7 +1042,8 @@ func TestResolveAutoApprovers(t *testing.T) { want: map[netip.Prefix]*netipx.IPSet{ mp("10.0.0.0/24"): mustIPSet("100.64.0.1/32"), }, - wantErr: false, + wantAllIPRoutes: nil, + wantErr: false, }, { name: "multiple-routes", @@ -1057,7 +1059,8 @@ func TestResolveAutoApprovers(t *testing.T) { mp("10.0.0.0/24"): mustIPSet("100.64.0.1/32"), mp("10.0.1.0/24"): mustIPSet("100.64.0.2/32"), }, - wantErr: false, + wantAllIPRoutes: nil, + wantErr: false, }, { name: "exit-node", @@ -1066,11 +1069,9 @@ func TestResolveAutoApprovers(t *testing.T) { ExitNode: AutoApprovers{ptr.To(Username("user1@"))}, }, }, - want: map[netip.Prefix]*netipx.IPSet{ - tsaddr.AllIPv4(): mustIPSet("100.64.0.1/32"), - tsaddr.AllIPv6(): mustIPSet("100.64.0.1/32"), - }, - wantErr: false, + want: map[netip.Prefix]*netipx.IPSet{}, + wantAllIPRoutes: mustIPSet("100.64.0.1/32"), + wantErr: false, }, { name: "group-route", @@ -1087,7 +1088,8 @@ func TestResolveAutoApprovers(t *testing.T) { want: map[netip.Prefix]*netipx.IPSet{ mp("10.0.0.0/24"): mustIPSet("100.64.0.1/32", "100.64.0.2/32"), }, - wantErr: false, + wantAllIPRoutes: nil, + wantErr: false, }, { name: "tag-route-and-exit", @@ -1113,10 +1115,9 @@ func TestResolveAutoApprovers(t *testing.T) { }, want: map[netip.Prefix]*netipx.IPSet{ mp("10.0.1.0/24"): mustIPSet("100.64.0.4/32"), - tsaddr.AllIPv4(): mustIPSet("100.64.0.5/32"), - tsaddr.AllIPv6(): mustIPSet("100.64.0.5/32"), }, - wantErr: false, + wantAllIPRoutes: mustIPSet("100.64.0.5/32"), + wantErr: false, }, { name: "mixed-routes-and-exit-nodes", @@ -1135,10 +1136,9 @@ func TestResolveAutoApprovers(t *testing.T) { want: map[netip.Prefix]*netipx.IPSet{ mp("10.0.0.0/24"): mustIPSet("100.64.0.1/32", "100.64.0.2/32"), mp("10.0.1.0/24"): mustIPSet("100.64.0.3/32"), - tsaddr.AllIPv4(): mustIPSet("100.64.0.1/32"), - tsaddr.AllIPv6(): mustIPSet("100.64.0.1/32"), }, - wantErr: false, + wantAllIPRoutes: mustIPSet("100.64.0.1/32"), + wantErr: false, }, } @@ -1146,7 +1146,7 @@ func TestResolveAutoApprovers(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := resolveAutoApprovers(tt.policy, users, nodes) + got, gotAllIPRoutes, err := resolveAutoApprovers(tt.policy, users, nodes) if (err != nil) != tt.wantErr { t.Errorf("resolveAutoApprovers() error = %v, wantErr %v", err, tt.wantErr) return @@ -1154,6 +1154,15 @@ func TestResolveAutoApprovers(t *testing.T) { if diff := cmp.Diff(tt.want, got, cmps...); diff != "" { t.Errorf("resolveAutoApprovers() mismatch (-want +got):\n%s", diff) } + if tt.wantAllIPRoutes != nil { + if gotAllIPRoutes == nil { + t.Error("resolveAutoApprovers() expected non-nil allIPRoutes, got nil") + } else if diff := cmp.Diff(tt.wantAllIPRoutes, gotAllIPRoutes, cmps...); diff != "" { + t.Errorf("resolveAutoApprovers() allIPRoutes mismatch (-want +got):\n%s", diff) + } + } else if gotAllIPRoutes != nil { + t.Error("resolveAutoApprovers() expected nil allIPRoutes, got non-nil") + } }) } }