mirror of
https://github.com/juanfont/headscale.git
synced 2025-05-18 01:16:48 +02:00
policy/v2: separate exit node and 0.0.0.0/0 routes (#2578)
* policy: add tests for route auto approval Reproduce #2568 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy/v2: separate exit node and 0.0.0.0/0 routes Fixes #2568 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
377b854dd8
commit
37dc0dad35
809
hscontrol/policy/route_approval_test.go
Normal file
809
hscontrol/policy/route_approval_test.go
Normal file
@ -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")
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user