mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	* update users doc on unique constraints Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * simplify finduser func Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * add initial tests for findUserFromToken Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * add changelog Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
		
			
				
	
	
		
			4361 lines
		
	
	
		
			102 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			4361 lines
		
	
	
		
			102 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package policy
 | |
| 
 | |
| import (
 | |
| 	"database/sql"
 | |
| 	"errors"
 | |
| 	"math/rand/v2"
 | |
| 	"net/netip"
 | |
| 	"slices"
 | |
| 	"sort"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| 	"github.com/juanfont/headscale/hscontrol/types"
 | |
| 	"github.com/juanfont/headscale/hscontrol/util"
 | |
| 	"github.com/rs/zerolog/log"
 | |
| 	"github.com/spf13/viper"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| 	"go4.org/netipx"
 | |
| 	"gopkg.in/check.v1"
 | |
| 	"gorm.io/gorm"
 | |
| 	"tailscale.com/net/tsaddr"
 | |
| 	"tailscale.com/tailcfg"
 | |
| )
 | |
| 
 | |
| var iap = func(ipStr string) *netip.Addr {
 | |
| 	ip := netip.MustParseAddr(ipStr)
 | |
| 	return &ip
 | |
| }
 | |
| 
 | |
| func Test(t *testing.T) {
 | |
| 	check.TestingT(t)
 | |
| }
 | |
| 
 | |
| var _ = check.Suite(&Suite{})
 | |
| 
 | |
| type Suite struct{}
 | |
| 
 | |
| func (s *Suite) TestWrongPath(c *check.C) {
 | |
| 	_, err := LoadACLPolicyFromPath("asdfg")
 | |
| 	c.Assert(err, check.NotNil)
 | |
| }
 | |
| 
 | |
| func TestParsing(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		format  string
 | |
| 		acl     string
 | |
| 		want    []tailcfg.FilterRule
 | |
| 		wantErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name:   "invalid-hujson",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {
 | |
| 		`,
 | |
| 			want:    []tailcfg.FilterRule{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "valid-hujson-invalid-content",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {
 | |
|   "valid_json": true,
 | |
|   "but_a_policy_though": false
 | |
| }
 | |
| 				`,
 | |
| 			want:    []tailcfg.FilterRule{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "invalid-cidr",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {"example-host-1": "100.100.100.100/42"}
 | |
| 				`,
 | |
| 			want:    []tailcfg.FilterRule{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "basic-rule",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {
 | |
| 	"hosts": {
 | |
| 		"host-1": "100.100.100.100",
 | |
| 		"subnet-1": "100.100.101.100/24",
 | |
| 	},
 | |
| 
 | |
| 	"acls": [
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"subnet-1",
 | |
| 				"192.168.1.0/24"
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"*:22,3389",
 | |
| 				"host-1:*",
 | |
| 			],
 | |
| 		},
 | |
| 	],
 | |
| }
 | |
| 		`,
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{"100.100.101.0/24", "192.168.1.0/24"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{IP: "0.0.0.0/0", Ports: tailcfg.PortRange{First: 22, Last: 22}},
 | |
| 						{IP: "0.0.0.0/0", Ports: tailcfg.PortRange{First: 3389, Last: 3389}},
 | |
| 						{IP: "::/0", Ports: tailcfg.PortRange{First: 22, Last: 22}},
 | |
| 						{IP: "::/0", Ports: tailcfg.PortRange{First: 3389, Last: 3389}},
 | |
| 						{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "parse-protocol",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {
 | |
| 	"hosts": {
 | |
| 		"host-1": "100.100.100.100",
 | |
| 		"subnet-1": "100.100.101.100/24",
 | |
| 	},
 | |
| 
 | |
| 	"acls": [
 | |
| 		{
 | |
| 			"Action": "accept",
 | |
| 			"src": [
 | |
| 				"*",
 | |
| 			],
 | |
| 			"proto": "tcp",
 | |
| 			"dst": [
 | |
| 				"host-1:*",
 | |
| 			],
 | |
| 		},
 | |
| 		{
 | |
| 			"Action": "accept",
 | |
| 			"src": [
 | |
| 				"*",
 | |
| 			],
 | |
| 			"proto": "udp",
 | |
| 			"dst": [
 | |
| 				"host-1:53",
 | |
| 			],
 | |
| 		},
 | |
| 		{
 | |
| 			"Action": "accept",
 | |
| 			"src": [
 | |
| 				"*",
 | |
| 			],
 | |
| 			"proto": "icmp",
 | |
| 			"dst": [
 | |
| 				"host-1:*",
 | |
| 			],
 | |
| 		},
 | |
| 	],
 | |
| }`,
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{"0.0.0.0/0", "::/0"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
 | |
| 					},
 | |
| 					IPProto: []int{protocolTCP},
 | |
| 				},
 | |
| 				{
 | |
| 					SrcIPs: []string{"0.0.0.0/0", "::/0"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{IP: "100.100.100.100/32", Ports: tailcfg.PortRange{First: 53, Last: 53}},
 | |
| 					},
 | |
| 					IPProto: []int{protocolUDP},
 | |
| 				},
 | |
| 				{
 | |
| 					SrcIPs: []string{"0.0.0.0/0", "::/0"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
 | |
| 					},
 | |
| 					IPProto: []int{protocolICMP, protocolIPv6ICMP},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "port-wildcard",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {
 | |
| 	"hosts": {
 | |
| 		"host-1": "100.100.100.100",
 | |
| 		"subnet-1": "100.100.101.100/24",
 | |
| 	},
 | |
| 
 | |
| 	"acls": [
 | |
| 		{
 | |
| 			"Action": "accept",
 | |
| 			"src": [
 | |
| 				"*",
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"host-1:*",
 | |
| 			],
 | |
| 		},
 | |
| 	],
 | |
| }
 | |
| `,
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{"0.0.0.0/0", "::/0"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "port-range",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {
 | |
| 	"hosts": {
 | |
| 		"host-1": "100.100.100.100",
 | |
| 		"subnet-1": "100.100.101.100/24",
 | |
| 	},
 | |
| 
 | |
| 	"acls": [
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"subnet-1",
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"host-1:5400-5500",
 | |
| 			],
 | |
| 		},
 | |
| 	],
 | |
| }
 | |
| `,
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{"100.100.101.0/24"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "100.100.100.100/32",
 | |
| 							Ports: tailcfg.PortRange{First: 5400, Last: 5500},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "port-group",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {
 | |
| 	"groups": {
 | |
| 		"group:example": [
 | |
| 			"testuser",
 | |
| 		],
 | |
| 	},
 | |
| 
 | |
| 	"hosts": {
 | |
| 		"host-1": "100.100.100.100",
 | |
| 		"subnet-1": "100.100.101.100/24",
 | |
| 	},
 | |
| 
 | |
| 	"acls": [
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"group:example",
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"host-1:*",
 | |
| 			],
 | |
| 		},
 | |
| 	],
 | |
| }
 | |
| `,
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{"200.200.200.200/32"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "port-user",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {
 | |
| 	"hosts": {
 | |
| 		"host-1": "100.100.100.100",
 | |
| 		"subnet-1": "100.100.101.100/24",
 | |
| 	},
 | |
| 
 | |
| 	"acls": [
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"testuser",
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"host-1:*",
 | |
| 			],
 | |
| 		},
 | |
| 	],
 | |
| }
 | |
| `,
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{"200.200.200.200/32"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "ipv6",
 | |
| 			format: "hujson",
 | |
| 			acl: `
 | |
| {
 | |
| 	"hosts": {
 | |
| 		"host-1": "100.100.100.100/32",
 | |
| 		"subnet-1": "100.100.101.100/24",
 | |
| 	},
 | |
| 
 | |
| 	"acls": [
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"*",
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"host-1:*",
 | |
| 			],
 | |
| 		},
 | |
| 	],
 | |
| }
 | |
| `,
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{"0.0.0.0/0", "::/0"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			pol, err := LoadACLPolicyFromBytes([]byte(tt.acl))
 | |
| 
 | |
| 			if tt.wantErr && err == nil {
 | |
| 				t.Errorf("parsing() error = %v, wantErr %v", err, tt.wantErr)
 | |
| 
 | |
| 				return
 | |
| 			} else if !tt.wantErr && err != nil {
 | |
| 				t.Errorf("parsing() error = %v, wantErr %v", err, tt.wantErr)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			if err != nil {
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			user := types.User{
 | |
| 				Model: gorm.Model{ID: 1},
 | |
| 				Name:  "testuser",
 | |
| 			}
 | |
| 			rules, err := pol.CompileFilterRules(
 | |
| 				[]types.User{
 | |
| 					user,
 | |
| 				},
 | |
| 				types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.100.100.100"),
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4:     iap("200.200.200.200"),
 | |
| 						User:     user,
 | |
| 						Hostinfo: &tailcfg.Hostinfo{},
 | |
| 					},
 | |
| 				})
 | |
| 
 | |
| 			if (err != nil) != tt.wantErr {
 | |
| 				t.Errorf("parsing() error = %v, wantErr %v", err, tt.wantErr)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			if diff := cmp.Diff(tt.want, rules); diff != "" {
 | |
| 				t.Errorf("parsing() unexpected result (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *Suite) TestRuleInvalidGeneration(c *check.C) {
 | |
| 	acl := []byte(`
 | |
| {
 | |
| 	// Declare static groups of users beyond those in the identity service.
 | |
| 	"groups": {
 | |
| 		"group:example": [
 | |
| 			"user1@example.com",
 | |
| 			"user2@example.com",
 | |
| 		],
 | |
| 	},
 | |
| 	// Declare hostname aliases to use in place of IP addresses or subnets.
 | |
| 	"hosts": {
 | |
| 		"example-host-1": "100.100.100.100",
 | |
| 		"example-host-2": "100.100.101.100/24",
 | |
| 	},
 | |
| 	// Define who is allowed to use which tags.
 | |
| 	"tagOwners": {
 | |
| 		// Everyone in the montreal-admins or global-admins group are
 | |
| 		// allowed to tag servers as montreal-webserver.
 | |
| 		"tag:montreal-webserver": [
 | |
| 			"group:montreal-admins",
 | |
| 			"group:global-admins",
 | |
| 		],
 | |
| 		// Only a few admins are allowed to create API servers.
 | |
| 		"tag:api-server": [
 | |
| 			"group:global-admins",
 | |
| 			"example-host-1",
 | |
| 		],
 | |
| 	},
 | |
| 	// Access control lists.
 | |
| 	"acls": [
 | |
| 		// Engineering users, plus the president, can access port 22 (ssh)
 | |
| 		// and port 3389 (remote desktop protocol) on all servers, and all
 | |
| 		// ports on git-server or ci-server.
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"group:engineering",
 | |
| 				"president@example.com"
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"*:22,3389",
 | |
| 				"git-server:*",
 | |
| 				"ci-server:*"
 | |
| 			],
 | |
| 		},
 | |
| 		// Allow engineer users to access any port on a device tagged with
 | |
| 		// tag:production.
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"group:engineers"
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"tag:production:*"
 | |
| 			],
 | |
| 		},
 | |
| 		// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
 | |
| 		// on both networks.
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"my-subnet",
 | |
| 				"192.168.1.0/24"
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"my-subnet:*",
 | |
| 				"192.168.1.0/24:*"
 | |
| 			],
 | |
| 		},
 | |
| 		// Allow every user of your network to access anything on the network.
 | |
| 		// Comment out this section if you want to define specific ACL
 | |
| 		// restrictions above.
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"*"
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"*:*"
 | |
| 			],
 | |
| 		},
 | |
| 		// All users in Montreal are allowed to access the Montreal web
 | |
| 		// servers.
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"group:montreal-users"
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"tag:montreal-webserver:80,443"
 | |
| 			],
 | |
| 		},
 | |
| 		// Montreal web servers are allowed to make outgoing connections to
 | |
| 		// the API servers, but only on https port 443.
 | |
| 		// In contrast, this doesn't grant API servers the right to initiate
 | |
| 		// any connections.
 | |
| 		{
 | |
| 			"action": "accept",
 | |
| 			"src": [
 | |
| 				"tag:montreal-webserver"
 | |
| 			],
 | |
| 			"dst": [
 | |
| 				"tag:api-server:443"
 | |
| 			],
 | |
| 		},
 | |
| 	],
 | |
| 	// Declare tests to check functionality of ACL rules
 | |
| 	"tests": [
 | |
| 		{
 | |
| 			"src": "user1@example.com",
 | |
| 			"accept": [
 | |
| 				"example-host-1:22",
 | |
| 				"example-host-2:80"
 | |
| 			],
 | |
| 			"deny": [
 | |
| 				"example-host-2:100"
 | |
| 			],
 | |
| 		},
 | |
| 		{
 | |
| 			"src": "user2@example.com",
 | |
| 			"accept": [
 | |
| 				"100.60.3.4:22"
 | |
| 			],
 | |
| 		},
 | |
| 	],
 | |
| }
 | |
| 	`)
 | |
| 	pol, err := LoadACLPolicyFromBytes(acl)
 | |
| 	c.Assert(pol.ACLs, check.HasLen, 6)
 | |
| 	c.Assert(err, check.IsNil)
 | |
| 
 | |
| 	rules, err := pol.CompileFilterRules([]types.User{}, types.Nodes{})
 | |
| 	c.Assert(err, check.NotNil)
 | |
| 	c.Assert(rules, check.IsNil)
 | |
| }
 | |
| 
 | |
| // TODO(kradalby): Make tests values safe, independent and descriptive.
 | |
| func (s *Suite) TestInvalidAction(c *check.C) {
 | |
| 	pol := &ACLPolicy{
 | |
| 		ACLs: []ACL{
 | |
| 			{
 | |
| 				Action:       "invalidAction",
 | |
| 				Sources:      []string{"*"},
 | |
| 				Destinations: []string{"*:*"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	_, _, err := GenerateFilterAndSSHRulesForTests(
 | |
| 		pol,
 | |
| 		&types.Node{},
 | |
| 		types.Nodes{},
 | |
| 		[]types.User{},
 | |
| 	)
 | |
| 	c.Assert(errors.Is(err, ErrInvalidAction), check.Equals, true)
 | |
| }
 | |
| 
 | |
| func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
 | |
| 	// this ACL is wrong because the group in Sources sections doesn't exist
 | |
| 	pol := &ACLPolicy{
 | |
| 		Groups: Groups{
 | |
| 			"group:test":  []string{"foo"},
 | |
| 			"group:error": []string{"foo", "group:test"},
 | |
| 		},
 | |
| 		ACLs: []ACL{
 | |
| 			{
 | |
| 				Action:       "accept",
 | |
| 				Sources:      []string{"group:error"},
 | |
| 				Destinations: []string{"*:*"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	_, _, err := GenerateFilterAndSSHRulesForTests(
 | |
| 		pol,
 | |
| 		&types.Node{},
 | |
| 		types.Nodes{},
 | |
| 		[]types.User{},
 | |
| 	)
 | |
| 	c.Assert(errors.Is(err, ErrInvalidGroup), check.Equals, true)
 | |
| }
 | |
| 
 | |
| func (s *Suite) TestInvalidTagOwners(c *check.C) {
 | |
| 	// this ACL is wrong because no tagOwners own the requested tag for the server
 | |
| 	pol := &ACLPolicy{
 | |
| 		ACLs: []ACL{
 | |
| 			{
 | |
| 				Action:       "accept",
 | |
| 				Sources:      []string{"tag:foo"},
 | |
| 				Destinations: []string{"*:*"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	_, _, err := GenerateFilterAndSSHRulesForTests(
 | |
| 		pol,
 | |
| 		&types.Node{},
 | |
| 		types.Nodes{},
 | |
| 		[]types.User{},
 | |
| 	)
 | |
| 	c.Assert(errors.Is(err, ErrInvalidTag), check.Equals, true)
 | |
| }
 | |
| 
 | |
| func Test_expandGroup(t *testing.T) {
 | |
| 	type field struct {
 | |
| 		pol ACLPolicy
 | |
| 	}
 | |
| 	type args struct {
 | |
| 		group      string
 | |
| 		stripEmail bool
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		field   field
 | |
| 		args    args
 | |
| 		want    []string
 | |
| 		wantErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "simple test",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					Groups: Groups{
 | |
| 						"group:test": []string{"user1", "user2", "user3"},
 | |
| 						"group:foo":  []string{"user2", "user3"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				group: "group:test",
 | |
| 			},
 | |
| 			want:    []string{"user1", "user2", "user3"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "InexistentGroup",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					Groups: Groups{
 | |
| 						"group:test": []string{"user1", "user2", "user3"},
 | |
| 						"group:foo":  []string{"user2", "user3"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				group: "group:undefined",
 | |
| 			},
 | |
| 			want:    []string{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Expand emails in group",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					Groups: Groups{
 | |
| 						"group:admin": []string{
 | |
| 							"joe.bar@gmail.com",
 | |
| 							"john.doe@yahoo.fr",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				group: "group:admin",
 | |
| 			},
 | |
| 			want:    []string{"joe.bar@gmail.com", "john.doe@yahoo.fr"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			viper.Set("oidc.strip_email_domain", test.args.stripEmail)
 | |
| 
 | |
| 			got, err := test.field.pol.expandUsersFromGroup(
 | |
| 				test.args.group,
 | |
| 			)
 | |
| 
 | |
| 			if (err != nil) != test.wantErr {
 | |
| 				t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			if diff := cmp.Diff(test.want, got); diff != "" {
 | |
| 				t.Errorf("expandGroup() unexpected result (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_expandTagOwners(t *testing.T) {
 | |
| 	type args struct {
 | |
| 		aclPolicy *ACLPolicy
 | |
| 		tag       string
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		args    args
 | |
| 		want    []string
 | |
| 		wantErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "simple tag expansion",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					TagOwners: TagOwners{"tag:test": []string{"user1"}},
 | |
| 				},
 | |
| 				tag: "tag:test",
 | |
| 			},
 | |
| 			want:    []string{"user1"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "expand with tag and group",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					Groups:    Groups{"group:foo": []string{"user1", "user2"}},
 | |
| 					TagOwners: TagOwners{"tag:test": []string{"group:foo"}},
 | |
| 				},
 | |
| 				tag: "tag:test",
 | |
| 			},
 | |
| 			want:    []string{"user1", "user2"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "expand with user and group",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					Groups:    Groups{"group:foo": []string{"user1", "user2"}},
 | |
| 					TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}},
 | |
| 				},
 | |
| 				tag: "tag:test",
 | |
| 			},
 | |
| 			want:    []string{"user1", "user2", "user3"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "invalid tag",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}},
 | |
| 				},
 | |
| 				tag: "tag:test",
 | |
| 			},
 | |
| 			want:    []string{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "invalid group",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					Groups:    Groups{"group:bar": []string{"user1", "user2"}},
 | |
| 					TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}},
 | |
| 				},
 | |
| 				tag: "tag:test",
 | |
| 			},
 | |
| 			want:    []string{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			got, err := expandOwnersFromTag(
 | |
| 				test.args.aclPolicy,
 | |
| 				test.args.tag,
 | |
| 			)
 | |
| 			if (err != nil) != test.wantErr {
 | |
| 				t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 			if diff := cmp.Diff(test.want, got); diff != "" {
 | |
| 				t.Errorf("expandTagOwners() = (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_expandPorts(t *testing.T) {
 | |
| 	type args struct {
 | |
| 		portsStr      string
 | |
| 		needsWildcard bool
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		args    args
 | |
| 		want    *[]tailcfg.PortRange
 | |
| 		wantErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "wildcard",
 | |
| 			args: args{portsStr: "*", needsWildcard: true},
 | |
| 			want: &[]tailcfg.PortRange{
 | |
| 				{First: portRangeBegin, Last: portRangeEnd},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "needs wildcard but does not require it",
 | |
| 			args: args{portsStr: "*", needsWildcard: false},
 | |
| 			want: &[]tailcfg.PortRange{
 | |
| 				{First: portRangeBegin, Last: portRangeEnd},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "needs wildcard but gets port",
 | |
| 			args:    args{portsStr: "80,443", needsWildcard: true},
 | |
| 			want:    nil,
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "two Destinations",
 | |
| 			args: args{portsStr: "80,443", needsWildcard: false},
 | |
| 			want: &[]tailcfg.PortRange{
 | |
| 				{First: 80, Last: 80},
 | |
| 				{First: 443, Last: 443},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "a range and a port",
 | |
| 			args: args{portsStr: "80-1024,443", needsWildcard: false},
 | |
| 			want: &[]tailcfg.PortRange{
 | |
| 				{First: 80, Last: 1024},
 | |
| 				{First: 443, Last: 443},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "out of bounds",
 | |
| 			args:    args{portsStr: "854038", needsWildcard: false},
 | |
| 			want:    nil,
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "wrong port",
 | |
| 			args:    args{portsStr: "85a38", needsWildcard: false},
 | |
| 			want:    nil,
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "wrong port in first",
 | |
| 			args:    args{portsStr: "a-80", needsWildcard: false},
 | |
| 			want:    nil,
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "wrong port in last",
 | |
| 			args:    args{portsStr: "80-85a38", needsWildcard: false},
 | |
| 			want:    nil,
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "wrong port format",
 | |
| 			args:    args{portsStr: "80-85a38-3", needsWildcard: false},
 | |
| 			want:    nil,
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			got, err := expandPorts(test.args.portsStr, test.args.needsWildcard)
 | |
| 			if (err != nil) != test.wantErr {
 | |
| 				t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 			if diff := cmp.Diff(test.want, got); diff != "" {
 | |
| 				t.Errorf("expandPorts() = (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_filterNodesByUser(t *testing.T) {
 | |
| 	users := []types.User{
 | |
| 		{Model: gorm.Model{ID: 1}, Name: "marc"},
 | |
| 		{Model: gorm.Model{ID: 2}, Name: "joe", Email: "joe@headscale.net"},
 | |
| 		{
 | |
| 			Model:              gorm.Model{ID: 3},
 | |
| 			Name:               "mikael",
 | |
| 			Email:              "mikael@headscale.net",
 | |
| 			ProviderIdentifier: sql.NullString{String: "http://oidc.org/1234", Valid: true},
 | |
| 		},
 | |
| 		{Model: gorm.Model{ID: 4}, Name: "mikael2", Email: "mikael@headscale.net"},
 | |
| 		{Model: gorm.Model{ID: 5}, Name: "mikael", Email: "mikael2@headscale.net"},
 | |
| 		{Model: gorm.Model{ID: 6}, Name: "http://oidc.org/1234", Email: "mikael@headscale.net"},
 | |
| 		{Model: gorm.Model{ID: 7}, Name: "1"},
 | |
| 		{Model: gorm.Model{ID: 8}, Name: "alex", Email: "alex@headscale.net"},
 | |
| 		{Model: gorm.Model{ID: 9}, Name: "alex@headscale.net"},
 | |
| 		{Model: gorm.Model{ID: 10}, Email: "http://oidc.org/1234"},
 | |
| 	}
 | |
| 
 | |
| 	type args struct {
 | |
| 		nodes types.Nodes
 | |
| 		user  string
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name string
 | |
| 		args args
 | |
| 		want types.Nodes
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "1 node in user",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{User: users[1]},
 | |
| 				},
 | |
| 				user: "joe",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{User: users[1]},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "3 nodes, 2 in user",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[1]},
 | |
| 					&types.Node{ID: 2, User: users[0]},
 | |
| 					&types.Node{ID: 3, User: users[0]},
 | |
| 				},
 | |
| 				user: "marc",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{ID: 2, User: users[0]},
 | |
| 				&types.Node{ID: 3, User: users[0]},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "5 nodes, 0 in user",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[1]},
 | |
| 					&types.Node{ID: 2, User: users[0]},
 | |
| 					&types.Node{ID: 3, User: users[0]},
 | |
| 					&types.Node{ID: 4, User: users[0]},
 | |
| 					&types.Node{ID: 5, User: users[0]},
 | |
| 				},
 | |
| 				user: "mickael",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "match-by-provider-ident",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[1]},
 | |
| 					&types.Node{ID: 2, User: users[2]},
 | |
| 				},
 | |
| 				user: "http://oidc.org/1234",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{ID: 2, User: users[2]},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "match-by-email",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[1]},
 | |
| 					&types.Node{ID: 2, User: users[2]},
 | |
| 					&types.Node{ID: 8, User: users[7]},
 | |
| 				},
 | |
| 				user: "joe@headscale.net",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{ID: 1, User: users[1]},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multi-match-is-zero",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[1]},
 | |
| 					&types.Node{ID: 2, User: users[2]},
 | |
| 					&types.Node{ID: 3, User: users[3]},
 | |
| 				},
 | |
| 				user: "mikael@headscale.net",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multi-email-first-match-is-zero",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					// First match email, then provider id
 | |
| 					&types.Node{ID: 3, User: users[3]},
 | |
| 					&types.Node{ID: 2, User: users[2]},
 | |
| 				},
 | |
| 				user: "mikael@headscale.net",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multi-username-first-match-is-zero",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					// First match username, then provider id
 | |
| 					&types.Node{ID: 4, User: users[3]},
 | |
| 					&types.Node{ID: 2, User: users[2]},
 | |
| 				},
 | |
| 				user: "mikael",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "all-users-duplicate-username-random-order",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[0]},
 | |
| 					&types.Node{ID: 2, User: users[1]},
 | |
| 					&types.Node{ID: 3, User: users[2]},
 | |
| 					&types.Node{ID: 4, User: users[3]},
 | |
| 					&types.Node{ID: 5, User: users[4]},
 | |
| 				},
 | |
| 				user: "mikael",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "all-users-unique-username-random-order",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[0]},
 | |
| 					&types.Node{ID: 2, User: users[1]},
 | |
| 					&types.Node{ID: 3, User: users[2]},
 | |
| 					&types.Node{ID: 4, User: users[3]},
 | |
| 					&types.Node{ID: 5, User: users[4]},
 | |
| 				},
 | |
| 				user: "marc",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{ID: 1, User: users[0]},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "all-users-no-username-random-order",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[0]},
 | |
| 					&types.Node{ID: 2, User: users[1]},
 | |
| 					&types.Node{ID: 3, User: users[2]},
 | |
| 					&types.Node{ID: 4, User: users[3]},
 | |
| 					&types.Node{ID: 5, User: users[4]},
 | |
| 				},
 | |
| 				user: "not-working",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "all-users-duplicate-email-random-order",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[0]},
 | |
| 					&types.Node{ID: 2, User: users[1]},
 | |
| 					&types.Node{ID: 3, User: users[2]},
 | |
| 					&types.Node{ID: 4, User: users[3]},
 | |
| 					&types.Node{ID: 5, User: users[4]},
 | |
| 				},
 | |
| 				user: "mikael@headscale.net",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "all-users-duplicate-email-random-order",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[0]},
 | |
| 					&types.Node{ID: 2, User: users[1]},
 | |
| 					&types.Node{ID: 3, User: users[2]},
 | |
| 					&types.Node{ID: 4, User: users[3]},
 | |
| 					&types.Node{ID: 5, User: users[4]},
 | |
| 					&types.Node{ID: 8, User: users[7]},
 | |
| 				},
 | |
| 				user: "joe@headscale.net",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{ID: 2, User: users[1]},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "email-as-username-duplicate",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[7]},
 | |
| 					&types.Node{ID: 2, User: users[8]},
 | |
| 				},
 | |
| 				user: "alex@headscale.net",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "all-users-no-email-random-order",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[0]},
 | |
| 					&types.Node{ID: 2, User: users[1]},
 | |
| 					&types.Node{ID: 3, User: users[2]},
 | |
| 					&types.Node{ID: 4, User: users[3]},
 | |
| 					&types.Node{ID: 5, User: users[4]},
 | |
| 				},
 | |
| 				user: "not-working@headscale.net",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "all-users-provider-id-random-order",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[0]},
 | |
| 					&types.Node{ID: 2, User: users[1]},
 | |
| 					&types.Node{ID: 3, User: users[2]},
 | |
| 					&types.Node{ID: 4, User: users[3]},
 | |
| 					&types.Node{ID: 5, User: users[4]},
 | |
| 					&types.Node{ID: 6, User: users[5]},
 | |
| 				},
 | |
| 				user: "http://oidc.org/1234",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{ID: 3, User: users[2]},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "all-users-no-provider-id-random-order",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{ID: 1, User: users[0]},
 | |
| 					&types.Node{ID: 2, User: users[1]},
 | |
| 					&types.Node{ID: 3, User: users[2]},
 | |
| 					&types.Node{ID: 4, User: users[3]},
 | |
| 					&types.Node{ID: 5, User: users[4]},
 | |
| 					&types.Node{ID: 6, User: users[5]},
 | |
| 				},
 | |
| 				user: "http://oidc.org/4321",
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			for range 1000 {
 | |
| 				ns := test.args.nodes
 | |
| 				rand.Shuffle(len(ns), func(i, j int) {
 | |
| 					ns[i], ns[j] = ns[j], ns[i]
 | |
| 				})
 | |
| 				us := users
 | |
| 				rand.Shuffle(len(us), func(i, j int) {
 | |
| 					us[i], us[j] = us[j], us[i]
 | |
| 				})
 | |
| 				got := filterNodesByUser(ns, us, test.args.user)
 | |
| 				sort.Slice(got, func(i, j int) bool {
 | |
| 					return got[i].ID < got[j].ID
 | |
| 				})
 | |
| 
 | |
| 				if diff := cmp.Diff(test.want, got, util.Comparers...); diff != "" {
 | |
| 					t.Errorf("filterNodesByUser() = (-want +got):\n%s", diff)
 | |
| 				}
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_expandAlias(t *testing.T) {
 | |
| 	set := func(ips []string, prefixes []string) *netipx.IPSet {
 | |
| 		var builder netipx.IPSetBuilder
 | |
| 
 | |
| 		for _, ip := range ips {
 | |
| 			builder.Add(netip.MustParseAddr(ip))
 | |
| 		}
 | |
| 
 | |
| 		for _, pre := range prefixes {
 | |
| 			builder.AddPrefix(netip.MustParsePrefix(pre))
 | |
| 		}
 | |
| 
 | |
| 		s, _ := builder.IPSet()
 | |
| 
 | |
| 		return s
 | |
| 	}
 | |
| 
 | |
| 	users := []types.User{
 | |
| 		{Model: gorm.Model{ID: 1}, Name: "joe"},
 | |
| 		{Model: gorm.Model{ID: 2}, Name: "marc"},
 | |
| 		{Model: gorm.Model{ID: 3}, Name: "mickael"},
 | |
| 	}
 | |
| 
 | |
| 	type field struct {
 | |
| 		pol ACLPolicy
 | |
| 	}
 | |
| 	type args struct {
 | |
| 		nodes     types.Nodes
 | |
| 		aclPolicy ACLPolicy
 | |
| 		alias     string
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		field   field
 | |
| 		args    args
 | |
| 		want    *netipx.IPSet
 | |
| 		wantErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "wildcard",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "*",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.78.84.227"),
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: set([]string{}, []string{
 | |
| 				"0.0.0.0/0",
 | |
| 				"::/0",
 | |
| 			}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple group",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					Groups: Groups{"group:accountant": []string{"joe", "marc"}},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "group:accountant",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: users[0],
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: users[0],
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: users[1],
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.4"),
 | |
| 						User: users[2],
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: set([]string{
 | |
| 				"100.64.0.1", "100.64.0.2", "100.64.0.3",
 | |
| 			}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "wrong group",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					Groups: Groups{"group:accountant": []string{"joe", "marc"}},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "group:hr",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: users[0],
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: users[0],
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: users[1],
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.4"),
 | |
| 						User: users[2],
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want:    set([]string{}, []string{}),
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple ipaddress",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "10.0.0.3",
 | |
| 				nodes: types.Nodes{},
 | |
| 			},
 | |
| 			want: set([]string{
 | |
| 				"10.0.0.3",
 | |
| 			}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple host by ip passed through",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "10.0.0.1",
 | |
| 				nodes: types.Nodes{},
 | |
| 			},
 | |
| 			want: set([]string{
 | |
| 				"10.0.0.1",
 | |
| 			}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple host by ipv4 single ipv4",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "10.0.0.1",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("10.0.0.1"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: set([]string{
 | |
| 				"10.0.0.1",
 | |
| 			}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple host by ipv4 single dual stack",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "10.0.0.1",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("10.0.0.1"),
 | |
| 						IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: set([]string{
 | |
| 				"10.0.0.1", "fd7a:115c:a1e0:ab12:4843:2222:6273:2222",
 | |
| 			}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple host by ipv6 single dual stack",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "fd7a:115c:a1e0:ab12:4843:2222:6273:2222",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("10.0.0.1"),
 | |
| 						IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: set([]string{
 | |
| 				"fd7a:115c:a1e0:ab12:4843:2222:6273:2222", "10.0.0.1",
 | |
| 			}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple host by hostname alias",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					Hosts: Hosts{
 | |
| 						"testy": netip.MustParsePrefix("10.0.0.132/32"),
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "testy",
 | |
| 				nodes: types.Nodes{},
 | |
| 			},
 | |
| 			want:    set([]string{}, []string{"10.0.0.132/32"}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "private network",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					Hosts: Hosts{
 | |
| 						"homeNetwork": netip.MustParsePrefix("192.168.1.0/24"),
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "homeNetwork",
 | |
| 				nodes: types.Nodes{},
 | |
| 			},
 | |
| 			want:    set([]string{}, []string{"192.168.1.0/24"}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple CIDR",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias:     "10.0.0.0/16",
 | |
| 				nodes:     types.Nodes{},
 | |
| 				aclPolicy: ACLPolicy{},
 | |
| 			},
 | |
| 			want:    set([]string{}, []string{"10.0.0.0/16"}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple tag",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "tag:hr-webserver",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: users[0],
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:hr-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: users[0],
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:hr-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: users[1],
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.4"),
 | |
| 						User: users[0],
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: set([]string{
 | |
| 				"100.64.0.1", "100.64.0.2",
 | |
| 			}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "No tag defined",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					Groups: Groups{"group:accountant": []string{"joe", "marc"}},
 | |
| 					TagOwners: TagOwners{
 | |
| 						"tag:accountant-webserver": []string{"group:accountant"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "tag:hr-webserver",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: types.User{Name: "marc"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.4"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want:    set([]string{}, []string{}),
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Forced tag defined",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "tag:hr-webserver",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4:       iap("100.64.0.1"),
 | |
| 						User:       users[0],
 | |
| 						ForcedTags: []string{"tag:hr-webserver"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4:       iap("100.64.0.2"),
 | |
| 						User:       users[0],
 | |
| 						ForcedTags: []string{"tag:hr-webserver"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: users[1],
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.4"),
 | |
| 						User: users[2],
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want:    set([]string{"100.64.0.1", "100.64.0.2"}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Forced tag with legitimate tagOwner",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					TagOwners: TagOwners{
 | |
| 						"tag:hr-webserver": []string{"joe"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "tag:hr-webserver",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4:       iap("100.64.0.1"),
 | |
| 						User:       users[0],
 | |
| 						ForcedTags: []string{"tag:hr-webserver"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: users[0],
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:hr-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: users[1],
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.4"),
 | |
| 						User: users[2],
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want:    set([]string{"100.64.0.1", "100.64.0.2"}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "list host in user without correctly tagged servers",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				alias: "joe",
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:accountant-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:accountant-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4:     iap("100.64.0.3"),
 | |
| 						User:     users[1],
 | |
| 						Hostinfo: &tailcfg.Hostinfo{},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4:     iap("100.64.0.4"),
 | |
| 						User:     users[0],
 | |
| 						Hostinfo: &tailcfg.Hostinfo{},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want:    set([]string{"100.64.0.4"}, []string{}),
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			got, err := test.field.pol.ExpandAlias(
 | |
| 				test.args.nodes,
 | |
| 				users,
 | |
| 				test.args.alias,
 | |
| 			)
 | |
| 			if (err != nil) != test.wantErr {
 | |
| 				t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 			if diff := cmp.Diff(test.want, got); diff != "" {
 | |
| 				t.Errorf("expandAlias() unexpected result (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
 | |
| 	type args struct {
 | |
| 		aclPolicy *ACLPolicy
 | |
| 		nodes     types.Nodes
 | |
| 		user      string
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		args    args
 | |
| 		want    types.Nodes
 | |
| 		wantErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "exclude nodes with valid tags",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
 | |
| 				},
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:accountant-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:accountant-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4:     iap("100.64.0.4"),
 | |
| 						User:     types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{},
 | |
| 					},
 | |
| 				},
 | |
| 				user: "joe",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4:     iap("100.64.0.4"),
 | |
| 					User:     types.User{Name: "joe"},
 | |
| 					Hostinfo: &tailcfg.Hostinfo{},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "exclude nodes with valid tags, and owner is in a group",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					Groups: Groups{
 | |
| 						"group:accountant": []string{"joe", "bar"},
 | |
| 					},
 | |
| 					TagOwners: TagOwners{
 | |
| 						"tag:accountant-webserver": []string{"group:accountant"},
 | |
| 					},
 | |
| 				},
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:accountant-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:accountant-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4:     iap("100.64.0.4"),
 | |
| 						User:     types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{},
 | |
| 					},
 | |
| 				},
 | |
| 				user: "joe",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4:     iap("100.64.0.4"),
 | |
| 					User:     types.User{Name: "joe"},
 | |
| 					Hostinfo: &tailcfg.Hostinfo{},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "exclude nodes with valid tags and with forced tags",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
 | |
| 				},
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "foo",
 | |
| 							RequestTags: []string{"tag:accountant-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4:       iap("100.64.0.2"),
 | |
| 						User:       types.User{Name: "joe"},
 | |
| 						ForcedTags: []string{"tag:accountant-webserver"},
 | |
| 						Hostinfo:   &tailcfg.Hostinfo{},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4:     iap("100.64.0.4"),
 | |
| 						User:     types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{},
 | |
| 					},
 | |
| 				},
 | |
| 				user: "joe",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4:     iap("100.64.0.4"),
 | |
| 					User:     types.User{Name: "joe"},
 | |
| 					Hostinfo: &tailcfg.Hostinfo{},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "all nodes have invalid tags, don't exclude them",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
 | |
| 				},
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "hr-web1",
 | |
| 							RequestTags: []string{"tag:hr-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{
 | |
| 							OS:          "centos",
 | |
| 							Hostname:    "hr-web2",
 | |
| 							RequestTags: []string{"tag:hr-webserver"},
 | |
| 						},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4:     iap("100.64.0.4"),
 | |
| 						User:     types.User{Name: "joe"},
 | |
| 						Hostinfo: &tailcfg.Hostinfo{},
 | |
| 					},
 | |
| 				},
 | |
| 				user: "joe",
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					User: types.User{Name: "joe"},
 | |
| 					Hostinfo: &tailcfg.Hostinfo{
 | |
| 						OS:          "centos",
 | |
| 						Hostname:    "hr-web1",
 | |
| 						RequestTags: []string{"tag:hr-webserver"},
 | |
| 					},
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					User: types.User{Name: "joe"},
 | |
| 					Hostinfo: &tailcfg.Hostinfo{
 | |
| 						OS:          "centos",
 | |
| 						Hostname:    "hr-web2",
 | |
| 						RequestTags: []string{"tag:hr-webserver"},
 | |
| 					},
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					IPv4:     iap("100.64.0.4"),
 | |
| 					User:     types.User{Name: "joe"},
 | |
| 					Hostinfo: &tailcfg.Hostinfo{},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			got := excludeCorrectlyTaggedNodes(
 | |
| 				test.args.aclPolicy,
 | |
| 				test.args.nodes,
 | |
| 				test.args.user,
 | |
| 			)
 | |
| 			if diff := cmp.Diff(test.want, got, util.Comparers...); diff != "" {
 | |
| 				t.Errorf("excludeCorrectlyTaggedNodes() (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestACLPolicy_generateFilterRules(t *testing.T) {
 | |
| 	type field struct {
 | |
| 		pol ACLPolicy
 | |
| 	}
 | |
| 	type args struct {
 | |
| 		nodes types.Nodes
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		field   field
 | |
| 		args    args
 | |
| 		want    []tailcfg.FilterRule
 | |
| 		wantErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name:    "no-policy",
 | |
| 			field:   field{},
 | |
| 			args:    args{},
 | |
| 			want:    nil,
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "allow-all",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					ACLs: []ACL{
 | |
| 						{
 | |
| 							Action:       "accept",
 | |
| 							Sources:      []string{"*"},
 | |
| 							Destinations: []string{"*:*"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"),
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{"0.0.0.0/0", "::/0"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP: "0.0.0.0/0",
 | |
| 							Ports: tailcfg.PortRange{
 | |
| 								First: 0,
 | |
| 								Last:  65535,
 | |
| 							},
 | |
| 						},
 | |
| 						{
 | |
| 							IP: "::/0",
 | |
| 							Ports: tailcfg.PortRange{
 | |
| 								First: 0,
 | |
| 								Last:  65535,
 | |
| 							},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "host1-can-reach-host2-full",
 | |
| 			field: field{
 | |
| 				pol: ACLPolicy{
 | |
| 					ACLs: []ACL{
 | |
| 						{
 | |
| 							Action:       "accept",
 | |
| 							Sources:      []string{"100.64.0.2"},
 | |
| 							Destinations: []string{"100.64.0.1:*"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0:ab12:4843:2222:6273:2222/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP: "100.64.0.1/32",
 | |
| 							Ports: tailcfg.PortRange{
 | |
| 								First: 0,
 | |
| 								Last:  65535,
 | |
| 							},
 | |
| 						},
 | |
| 						{
 | |
| 							IP: "fd7a:115c:a1e0:ab12:4843:2222:6273:2221/128",
 | |
| 							Ports: tailcfg.PortRange{
 | |
| 								First: 0,
 | |
| 								Last:  65535,
 | |
| 							},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			got, err := tt.field.pol.CompileFilterRules(
 | |
| 				[]types.User{},
 | |
| 				tt.args.nodes,
 | |
| 			)
 | |
| 			if (err != nil) != tt.wantErr {
 | |
| 				t.Errorf("ACLgenerateFilterRules() error = %v, wantErr %v", err, tt.wantErr)
 | |
| 
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			if diff := cmp.Diff(tt.want, got); diff != "" {
 | |
| 				log.Trace().Interface("got", got).Msg("result")
 | |
| 				t.Errorf("ACLgenerateFilterRules() unexpected result (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // tsExitNodeDest is the list of destination IP ranges that are allowed when
 | |
| // you dump the filter list from a Tailscale node connected to Tailscale SaaS.
 | |
| var tsExitNodeDest = []tailcfg.NetPortRange{
 | |
| 	{
 | |
| 		IP:    "0.0.0.0-9.255.255.255",
 | |
| 		Ports: tailcfg.PortRangeAny,
 | |
| 	},
 | |
| 	{
 | |
| 		IP:    "11.0.0.0-100.63.255.255",
 | |
| 		Ports: tailcfg.PortRangeAny,
 | |
| 	},
 | |
| 	{
 | |
| 		IP:    "100.128.0.0-169.253.255.255",
 | |
| 		Ports: tailcfg.PortRangeAny,
 | |
| 	},
 | |
| 	{
 | |
| 		IP:    "169.255.0.0-172.15.255.255",
 | |
| 		Ports: tailcfg.PortRangeAny,
 | |
| 	},
 | |
| 	{
 | |
| 		IP:    "172.32.0.0-192.167.255.255",
 | |
| 		Ports: tailcfg.PortRangeAny,
 | |
| 	},
 | |
| 	{
 | |
| 		IP:    "192.169.0.0-255.255.255.255",
 | |
| 		Ports: tailcfg.PortRangeAny,
 | |
| 	},
 | |
| 	{
 | |
| 		IP:    "2000::-3fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
 | |
| 		Ports: tailcfg.PortRangeAny,
 | |
| 	},
 | |
| }
 | |
| 
 | |
| // hsExitNodeDest is the list of destination IP ranges that are allowed when
 | |
| // we use headscale "autogroup:internet".
 | |
| var hsExitNodeDest = []tailcfg.NetPortRange{
 | |
| 	{IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "11.0.0.0/8", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "12.0.0.0/6", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "16.0.0.0/4", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "32.0.0.0/3", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "64.0.0.0/3", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "96.0.0.0/6", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "100.0.0.0/10", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "100.128.0.0/9", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "101.0.0.0/8", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "102.0.0.0/7", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "104.0.0.0/5", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "112.0.0.0/4", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "128.0.0.0/3", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "160.0.0.0/5", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "168.0.0.0/8", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "169.0.0.0/9", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "169.128.0.0/10", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "169.192.0.0/11", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "169.224.0.0/12", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "169.240.0.0/13", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "169.248.0.0/14", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "169.252.0.0/15", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "169.255.0.0/16", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "170.0.0.0/7", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "172.0.0.0/12", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "172.32.0.0/11", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "172.64.0.0/10", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "172.128.0.0/9", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "173.0.0.0/8", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "174.0.0.0/7", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "176.0.0.0/4", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "192.0.0.0/9", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "192.128.0.0/11", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "192.160.0.0/13", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "192.169.0.0/16", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "192.170.0.0/15", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "192.172.0.0/14", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "192.176.0.0/12", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "192.192.0.0/10", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "193.0.0.0/8", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "194.0.0.0/7", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "196.0.0.0/6", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "224.0.0.0/3", Ports: tailcfg.PortRangeAny},
 | |
| 	{IP: "2000::/3", Ports: tailcfg.PortRangeAny},
 | |
| }
 | |
| 
 | |
| func TestTheInternet(t *testing.T) {
 | |
| 	internetSet := theInternet()
 | |
| 
 | |
| 	internetPrefs := internetSet.Prefixes()
 | |
| 
 | |
| 	for i := range internetPrefs {
 | |
| 		if internetPrefs[i].String() != hsExitNodeDest[i].IP {
 | |
| 			t.Errorf(
 | |
| 				"prefix from internet set %q != hsExit list %q",
 | |
| 				internetPrefs[i].String(),
 | |
| 				hsExitNodeDest[i].IP,
 | |
| 			)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(internetPrefs) != len(hsExitNodeDest) {
 | |
| 		t.Fatalf(
 | |
| 			"expected same length of prefixes, internet: %d, hsExit: %d",
 | |
| 			len(internetPrefs),
 | |
| 			len(hsExitNodeDest),
 | |
| 		)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestReduceFilterRules(t *testing.T) {
 | |
| 	users := []types.User{
 | |
| 		{Model: gorm.Model{ID: 1}, Name: "mickael"},
 | |
| 		{Model: gorm.Model{ID: 2}, Name: "user1"},
 | |
| 		{Model: gorm.Model{ID: 3}, Name: "user2"},
 | |
| 		{Model: gorm.Model{ID: 4}, Name: "user100"},
 | |
| 	}
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name  string
 | |
| 		node  *types.Node
 | |
| 		peers types.Nodes
 | |
| 		pol   ACLPolicy
 | |
| 		want  []tailcfg.FilterRule
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "host1-can-reach-host2-no-rules",
 | |
| 			pol: ACLPolicy{
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"100.64.0.1"},
 | |
| 						Destinations: []string{"100.64.0.2:*"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			node: &types.Node{
 | |
| 				IPv4: iap("100.64.0.1"),
 | |
| 				IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"),
 | |
| 				User: users[0],
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
 | |
| 					User: users[0],
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "1604-subnet-routers-are-preserved",
 | |
| 			pol: ACLPolicy{
 | |
| 				Groups: Groups{
 | |
| 					"group:admins": {"user1"},
 | |
| 				},
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"group:admins"},
 | |
| 						Destinations: []string{"group:admins:*"},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"group:admins"},
 | |
| 						Destinations: []string{"10.33.0.0/16:*"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			node: &types.Node{
 | |
| 				IPv4: iap("100.64.0.1"),
 | |
| 				IPv6: iap("fd7a:115c:a1e0::1"),
 | |
| 				User: users[1],
 | |
| 				Hostinfo: &tailcfg.Hostinfo{
 | |
| 					RoutableIPs: []netip.Prefix{
 | |
| 						netip.MustParsePrefix("10.33.0.0/16"),
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::2"),
 | |
| 					User: users[1],
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "100.64.0.1/32",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 						{
 | |
| 							IP:    "fd7a:115c:a1e0::1/128",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "10.33.0.0/16",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "1786-reducing-breaks-exit-nodes-the-client",
 | |
| 			pol: ACLPolicy{
 | |
| 				Hosts: Hosts{
 | |
| 					// Exit node
 | |
| 					"internal": netip.MustParsePrefix("100.64.0.100/32"),
 | |
| 				},
 | |
| 				Groups: Groups{
 | |
| 					"group:team": {"user3", "user2", "user1"},
 | |
| 				},
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"internal:*",
 | |
| 						},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"autogroup:internet:*",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			node: &types.Node{
 | |
| 				IPv4: iap("100.64.0.1"),
 | |
| 				IPv6: iap("fd7a:115c:a1e0::1"),
 | |
| 				User: users[1],
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::2"),
 | |
| 					User: users[2],
 | |
| 				},
 | |
| 				// "internal" exit node
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.100"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::100"),
 | |
| 					User: users[3],
 | |
| 					Hostinfo: &tailcfg.Hostinfo{
 | |
| 						RoutableIPs: tsaddr.ExitRoutes(),
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "1786-reducing-breaks-exit-nodes-the-exit",
 | |
| 			pol: ACLPolicy{
 | |
| 				Hosts: Hosts{
 | |
| 					// Exit node
 | |
| 					"internal": netip.MustParsePrefix("100.64.0.100/32"),
 | |
| 				},
 | |
| 				Groups: Groups{
 | |
| 					"group:team": {"user3", "user2", "user1"},
 | |
| 				},
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"internal:*",
 | |
| 						},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"autogroup:internet:*",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			node: &types.Node{
 | |
| 				IPv4: iap("100.64.0.100"),
 | |
| 				IPv6: iap("fd7a:115c:a1e0::100"),
 | |
| 				User: types.User{Name: "user100"},
 | |
| 				Hostinfo: &tailcfg.Hostinfo{
 | |
| 					RoutableIPs: tsaddr.ExitRoutes(),
 | |
| 				},
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::2"),
 | |
| 					User: users[2],
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::1"),
 | |
| 					User: users[1],
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "100.64.0.100/32",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 						{
 | |
| 							IP:    "fd7a:115c:a1e0::100/128",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: hsExitNodeDest,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "1786-reducing-breaks-exit-nodes-the-example-from-issue",
 | |
| 			pol: ACLPolicy{
 | |
| 				Hosts: Hosts{
 | |
| 					// Exit node
 | |
| 					"internal": netip.MustParsePrefix("100.64.0.100/32"),
 | |
| 				},
 | |
| 				Groups: Groups{
 | |
| 					"group:team": {"user3", "user2", "user1"},
 | |
| 				},
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"internal:*",
 | |
| 						},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"0.0.0.0/5:*",
 | |
| 							"8.0.0.0/7:*",
 | |
| 							"11.0.0.0/8:*",
 | |
| 							"12.0.0.0/6:*",
 | |
| 							"16.0.0.0/4:*",
 | |
| 							"32.0.0.0/3:*",
 | |
| 							"64.0.0.0/2:*",
 | |
| 							"128.0.0.0/3:*",
 | |
| 							"160.0.0.0/5:*",
 | |
| 							"168.0.0.0/6:*",
 | |
| 							"172.0.0.0/12:*",
 | |
| 							"172.32.0.0/11:*",
 | |
| 							"172.64.0.0/10:*",
 | |
| 							"172.128.0.0/9:*",
 | |
| 							"173.0.0.0/8:*",
 | |
| 							"174.0.0.0/7:*",
 | |
| 							"176.0.0.0/4:*",
 | |
| 							"192.0.0.0/9:*",
 | |
| 							"192.128.0.0/11:*",
 | |
| 							"192.160.0.0/13:*",
 | |
| 							"192.169.0.0/16:*",
 | |
| 							"192.170.0.0/15:*",
 | |
| 							"192.172.0.0/14:*",
 | |
| 							"192.176.0.0/12:*",
 | |
| 							"192.192.0.0/10:*",
 | |
| 							"193.0.0.0/8:*",
 | |
| 							"194.0.0.0/7:*",
 | |
| 							"196.0.0.0/6:*",
 | |
| 							"200.0.0.0/5:*",
 | |
| 							"208.0.0.0/4:*",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			node: &types.Node{
 | |
| 				IPv4: iap("100.64.0.100"),
 | |
| 				IPv6: iap("fd7a:115c:a1e0::100"),
 | |
| 				User: users[3],
 | |
| 				Hostinfo: &tailcfg.Hostinfo{
 | |
| 					RoutableIPs: tsaddr.ExitRoutes(),
 | |
| 				},
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::2"),
 | |
| 					User: users[2],
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::1"),
 | |
| 					User: users[1],
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "100.64.0.100/32",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 						{
 | |
| 							IP:    "fd7a:115c:a1e0::100/128",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "11.0.0.0/8", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "12.0.0.0/6", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "16.0.0.0/4", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "32.0.0.0/3", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "64.0.0.0/2", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "128.0.0.0/3", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "160.0.0.0/5", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "168.0.0.0/6", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "172.0.0.0/12", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "172.32.0.0/11", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "172.64.0.0/10", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "172.128.0.0/9", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "173.0.0.0/8", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "174.0.0.0/7", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "176.0.0.0/4", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "192.0.0.0/9", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "192.128.0.0/11", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "192.160.0.0/13", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "192.169.0.0/16", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "192.170.0.0/15", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "192.172.0.0/14", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "192.176.0.0/12", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "192.192.0.0/10", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "193.0.0.0/8", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "194.0.0.0/7", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "196.0.0.0/6", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny},
 | |
| 						{IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "1786-reducing-breaks-exit-nodes-app-connector-like",
 | |
| 			pol: ACLPolicy{
 | |
| 				Hosts: Hosts{
 | |
| 					// Exit node
 | |
| 					"internal": netip.MustParsePrefix("100.64.0.100/32"),
 | |
| 				},
 | |
| 				Groups: Groups{
 | |
| 					"group:team": {"user3", "user2", "user1"},
 | |
| 				},
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"internal:*",
 | |
| 						},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"8.0.0.0/8:*",
 | |
| 							"16.0.0.0/8:*",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			node: &types.Node{
 | |
| 				IPv4: iap("100.64.0.100"),
 | |
| 				IPv6: iap("fd7a:115c:a1e0::100"),
 | |
| 				User: users[3],
 | |
| 				Hostinfo: &tailcfg.Hostinfo{
 | |
| 					RoutableIPs: []netip.Prefix{
 | |
| 						netip.MustParsePrefix("8.0.0.0/16"),
 | |
| 						netip.MustParsePrefix("16.0.0.0/16"),
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::2"),
 | |
| 					User: users[2],
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::1"),
 | |
| 					User: users[1],
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "100.64.0.100/32",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 						{
 | |
| 							IP:    "fd7a:115c:a1e0::100/128",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "8.0.0.0/8",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 						{
 | |
| 							IP:    "16.0.0.0/8",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "1786-reducing-breaks-exit-nodes-app-connector-like2",
 | |
| 			pol: ACLPolicy{
 | |
| 				Hosts: Hosts{
 | |
| 					// Exit node
 | |
| 					"internal": netip.MustParsePrefix("100.64.0.100/32"),
 | |
| 				},
 | |
| 				Groups: Groups{
 | |
| 					"group:team": {"user3", "user2", "user1"},
 | |
| 				},
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"internal:*",
 | |
| 						},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:team"},
 | |
| 						Destinations: []string{
 | |
| 							"8.0.0.0/16:*",
 | |
| 							"16.0.0.0/16:*",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			node: &types.Node{
 | |
| 				IPv4: iap("100.64.0.100"),
 | |
| 				IPv6: iap("fd7a:115c:a1e0::100"),
 | |
| 				User: users[3],
 | |
| 				Hostinfo: &tailcfg.Hostinfo{
 | |
| 					RoutableIPs: []netip.Prefix{
 | |
| 						netip.MustParsePrefix("8.0.0.0/8"),
 | |
| 						netip.MustParsePrefix("16.0.0.0/8"),
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::2"),
 | |
| 					User: users[2],
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::1"),
 | |
| 					User: users[1],
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "100.64.0.100/32",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 						{
 | |
| 							IP:    "fd7a:115c:a1e0::100/128",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					SrcIPs: []string{
 | |
| 						"100.64.0.1/32",
 | |
| 						"100.64.0.2/32",
 | |
| 						"fd7a:115c:a1e0::1/128",
 | |
| 						"fd7a:115c:a1e0::2/128",
 | |
| 					},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "8.0.0.0/16",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 						{
 | |
| 							IP:    "16.0.0.0/16",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "1817-reduce-breaks-32-mask",
 | |
| 			pol: ACLPolicy{
 | |
| 				Hosts: Hosts{
 | |
| 					"vlan1": netip.MustParsePrefix("172.16.0.0/24"),
 | |
| 					"dns1":  netip.MustParsePrefix("172.16.0.21/32"),
 | |
| 				},
 | |
| 				Groups: Groups{
 | |
| 					"group:access": {"user1"},
 | |
| 				},
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:  "accept",
 | |
| 						Sources: []string{"group:access"},
 | |
| 						Destinations: []string{
 | |
| 							"tag:access-servers:*",
 | |
| 							"dns1:*",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			node: &types.Node{
 | |
| 				IPv4: iap("100.64.0.100"),
 | |
| 				IPv6: iap("fd7a:115c:a1e0::100"),
 | |
| 				User: users[3],
 | |
| 				Hostinfo: &tailcfg.Hostinfo{
 | |
| 					RoutableIPs: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")},
 | |
| 				},
 | |
| 				ForcedTags: []string{"tag:access-servers"},
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					IPv6: iap("fd7a:115c:a1e0::1"),
 | |
| 					User: users[1],
 | |
| 				},
 | |
| 			},
 | |
| 			want: []tailcfg.FilterRule{
 | |
| 				{
 | |
| 					SrcIPs: []string{"100.64.0.1/32", "fd7a:115c:a1e0::1/128"},
 | |
| 					DstPorts: []tailcfg.NetPortRange{
 | |
| 						{
 | |
| 							IP:    "100.64.0.100/32",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 						{
 | |
| 							IP:    "fd7a:115c:a1e0::100/128",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 						{
 | |
| 							IP:    "172.16.0.21/32",
 | |
| 							Ports: tailcfg.PortRangeAny,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			got, _ := tt.pol.CompileFilterRules(
 | |
| 				users,
 | |
| 				append(tt.peers, tt.node),
 | |
| 			)
 | |
| 
 | |
| 			got = ReduceFilterRules(tt.node, got)
 | |
| 
 | |
| 			if diff := cmp.Diff(tt.want, got); diff != "" {
 | |
| 				log.Trace().Interface("got", got).Msg("result")
 | |
| 				t.Errorf("TestReduceFilterRules() unexpected result (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_getTags(t *testing.T) {
 | |
| 	users := []types.User{
 | |
| 		{
 | |
| 			Model: gorm.Model{ID: 1},
 | |
| 			Name:  "joe",
 | |
| 		},
 | |
| 	}
 | |
| 	type args struct {
 | |
| 		aclPolicy *ACLPolicy
 | |
| 		node      *types.Node
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name        string
 | |
| 		args        args
 | |
| 		wantInvalid []string
 | |
| 		wantValid   []string
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "valid tag one nodes",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					TagOwners: TagOwners{
 | |
| 						"tag:valid": []string{"joe"},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{
 | |
| 					User: users[0],
 | |
| 					Hostinfo: &tailcfg.Hostinfo{
 | |
| 						RequestTags: []string{"tag:valid"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantValid:   []string{"tag:valid"},
 | |
| 			wantInvalid: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "invalid tag and valid tag one nodes",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					TagOwners: TagOwners{
 | |
| 						"tag:valid": []string{"joe"},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{
 | |
| 					User: users[0],
 | |
| 					Hostinfo: &tailcfg.Hostinfo{
 | |
| 						RequestTags: []string{"tag:valid", "tag:invalid"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantValid:   []string{"tag:valid"},
 | |
| 			wantInvalid: []string{"tag:invalid"},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multiple invalid and identical tags, should return only one invalid tag",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					TagOwners: TagOwners{
 | |
| 						"tag:valid": []string{"joe"},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{
 | |
| 					User: users[0],
 | |
| 					Hostinfo: &tailcfg.Hostinfo{
 | |
| 						RequestTags: []string{
 | |
| 							"tag:invalid",
 | |
| 							"tag:valid",
 | |
| 							"tag:invalid",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantValid:   []string{"tag:valid"},
 | |
| 			wantInvalid: []string{"tag:invalid"},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "only invalid tags",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{
 | |
| 					TagOwners: TagOwners{
 | |
| 						"tag:valid": []string{"joe"},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{
 | |
| 					User: users[0],
 | |
| 					Hostinfo: &tailcfg.Hostinfo{
 | |
| 						RequestTags: []string{"tag:invalid", "very-invalid"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantValid:   nil,
 | |
| 			wantInvalid: []string{"tag:invalid", "very-invalid"},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "empty ACLPolicy should return empty tags and should not panic",
 | |
| 			args: args{
 | |
| 				aclPolicy: &ACLPolicy{},
 | |
| 				node: &types.Node{
 | |
| 					User: users[0],
 | |
| 					Hostinfo: &tailcfg.Hostinfo{
 | |
| 						RequestTags: []string{"tag:invalid", "very-invalid"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			wantValid:   nil,
 | |
| 			wantInvalid: []string{"tag:invalid", "very-invalid"},
 | |
| 		},
 | |
| 	}
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			gotValid, gotInvalid := test.args.aclPolicy.TagsOfNode(
 | |
| 				users,
 | |
| 				test.args.node,
 | |
| 			)
 | |
| 			for _, valid := range gotValid {
 | |
| 				if !slices.Contains(test.wantValid, valid) {
 | |
| 					t.Errorf(
 | |
| 						"valids: getTags() = %v, want %v",
 | |
| 						gotValid,
 | |
| 						test.wantValid,
 | |
| 					)
 | |
| 
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 			for _, invalid := range gotInvalid {
 | |
| 				if !slices.Contains(test.wantInvalid, invalid) {
 | |
| 					t.Errorf(
 | |
| 						"invalids: getTags() = %v, want %v",
 | |
| 						gotInvalid,
 | |
| 						test.wantInvalid,
 | |
| 					)
 | |
| 
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_getFilteredByACLPeers(t *testing.T) {
 | |
| 	type args struct {
 | |
| 		nodes types.Nodes
 | |
| 		rules []tailcfg.FilterRule
 | |
| 		node  *types.Node
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name string
 | |
| 		args args
 | |
| 		want types.Nodes
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "all hosts can talk to each other",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{ // list of all nodes in the database
 | |
| 					&types.Node{
 | |
| 						ID:   1,
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   2,
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "marc"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   3,
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | |
| 					{
 | |
| 						SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "*"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{ // current nodes
 | |
| 					ID:   1,
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					User: types.User{Name: "joe"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					ID:   2,
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					User: types.User{Name: "marc"},
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					ID:   3,
 | |
| 					IPv4: iap("100.64.0.3"),
 | |
| 					User: types.User{Name: "mickael"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "One host can talk to another, but not all hosts",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{ // list of all nodes in the database
 | |
| 					&types.Node{
 | |
| 						ID:   1,
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   2,
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "marc"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   3,
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | |
| 					{
 | |
| 						SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "100.64.0.2"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{ // current nodes
 | |
| 					ID:   1,
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					User: types.User{Name: "joe"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					ID:   2,
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					User: types.User{Name: "marc"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "host cannot directly talk to destination, but return path is authorized",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{ // list of all nodes in the database
 | |
| 					&types.Node{
 | |
| 						ID:   1,
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   2,
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "marc"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   3,
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | |
| 					{
 | |
| 						SrcIPs: []string{"100.64.0.3"},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "100.64.0.2"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{ // current nodes
 | |
| 					ID:   2,
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					User: types.User{Name: "marc"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					ID:   3,
 | |
| 					IPv4: iap("100.64.0.3"),
 | |
| 					User: types.User{Name: "mickael"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "rules allows all hosts to reach one destination",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{ // list of all nodes in the database
 | |
| 					&types.Node{
 | |
| 						ID:   1,
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   2,
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "marc"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   3,
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | |
| 					{
 | |
| 						SrcIPs: []string{"*"},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "100.64.0.2"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{ // current nodes
 | |
| 					ID:   1,
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					User: types.User{Name: "joe"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					ID:   2,
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					User: types.User{Name: "marc"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "rules allows all hosts to reach one destination, destination can reach all hosts",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{ // list of all nodes in the database
 | |
| 					&types.Node{
 | |
| 						ID:   1,
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   2,
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "marc"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   3,
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | |
| 					{
 | |
| 						SrcIPs: []string{"*"},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "100.64.0.2"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{ // current nodes
 | |
| 					ID:   2,
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					User: types.User{Name: "marc"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					ID:   1,
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					User: types.User{Name: "joe"},
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					ID:   3,
 | |
| 					IPv4: iap("100.64.0.3"),
 | |
| 					User: types.User{Name: "mickael"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "rule allows all hosts to reach all destinations",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{ // list of all nodes in the database
 | |
| 					&types.Node{
 | |
| 						ID:   1,
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   2,
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "marc"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   3,
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | |
| 					{
 | |
| 						SrcIPs: []string{"*"},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "*"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{ // current nodes
 | |
| 					ID:   2,
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					User: types.User{Name: "marc"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					ID:   1,
 | |
| 					IPv4: iap("100.64.0.1"),
 | |
| 					User: types.User{Name: "joe"},
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					ID:   3,
 | |
| 					IPv4: iap("100.64.0.3"),
 | |
| 					User: types.User{Name: "mickael"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "without rule all communications are forbidden",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{ // list of all nodes in the database
 | |
| 					&types.Node{
 | |
| 						ID:   1,
 | |
| 						IPv4: iap("100.64.0.1"),
 | |
| 						User: types.User{Name: "joe"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   2,
 | |
| 						IPv4: iap("100.64.0.2"),
 | |
| 						User: types.User{Name: "marc"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:   3,
 | |
| 						IPv4: iap("100.64.0.3"),
 | |
| 						User: types.User{Name: "mickael"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | |
| 				},
 | |
| 				node: &types.Node{ // current nodes
 | |
| 					ID:   2,
 | |
| 					IPv4: iap("100.64.0.2"),
 | |
| 					User: types.User{Name: "marc"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: nil,
 | |
| 		},
 | |
| 		{
 | |
| 			// Investigating 699
 | |
| 			// Found some nodes: [ts-head-8w6paa ts-unstable-lys2ib ts-head-upcrmb ts-unstable-rlwpvr] nodes=ts-head-8w6paa
 | |
| 			// ACL rules generated ACL=[{"DstPorts":[{"Bits":null,"IP":"*","Ports":{"First":0,"Last":65535}}],"SrcIPs":["fd7a:115c:a1e0::3","100.64.0.3","fd7a:115c:a1e0::4","100.64.0.4"]}]
 | |
| 			// ACL Cache Map={"100.64.0.3":{"*":{}},"100.64.0.4":{"*":{}},"fd7a:115c:a1e0::3":{"*":{}},"fd7a:115c:a1e0::4":{"*":{}}}
 | |
| 			name: "issue-699-broken-star",
 | |
| 			args: args{
 | |
| 				nodes: types.Nodes{ //
 | |
| 					&types.Node{
 | |
| 						ID:       1,
 | |
| 						Hostname: "ts-head-upcrmb",
 | |
| 						IPv4:     iap("100.64.0.3"),
 | |
| 						IPv6:     iap("fd7a:115c:a1e0::3"),
 | |
| 						User:     types.User{Name: "user1"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:       2,
 | |
| 						Hostname: "ts-unstable-rlwpvr",
 | |
| 						IPv4:     iap("100.64.0.4"),
 | |
| 						IPv6:     iap("fd7a:115c:a1e0::4"),
 | |
| 						User:     types.User{Name: "user1"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:       3,
 | |
| 						Hostname: "ts-head-8w6paa",
 | |
| 						IPv4:     iap("100.64.0.1"),
 | |
| 						IPv6:     iap("fd7a:115c:a1e0::1"),
 | |
| 						User:     types.User{Name: "user2"},
 | |
| 					},
 | |
| 					&types.Node{
 | |
| 						ID:       4,
 | |
| 						Hostname: "ts-unstable-lys2ib",
 | |
| 						IPv4:     iap("100.64.0.2"),
 | |
| 						IPv6:     iap("fd7a:115c:a1e0::2"),
 | |
| 						User:     types.User{Name: "user2"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{ // list of all ACLRules registered
 | |
| 					{
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{
 | |
| 								IP:    "*",
 | |
| 								Ports: tailcfg.PortRange{First: 0, Last: 65535},
 | |
| 							},
 | |
| 						},
 | |
| 						SrcIPs: []string{
 | |
| 							"fd7a:115c:a1e0::3", "100.64.0.3",
 | |
| 							"fd7a:115c:a1e0::4", "100.64.0.4",
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{ // current nodes
 | |
| 					ID:       3,
 | |
| 					Hostname: "ts-head-8w6paa",
 | |
| 					IPv4:     iap("100.64.0.1"),
 | |
| 					IPv6:     iap("fd7a:115c:a1e0::1"),
 | |
| 					User:     types.User{Name: "user2"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					ID:       1,
 | |
| 					Hostname: "ts-head-upcrmb",
 | |
| 					IPv4:     iap("100.64.0.3"),
 | |
| 					IPv6:     iap("fd7a:115c:a1e0::3"),
 | |
| 					User:     types.User{Name: "user1"},
 | |
| 				},
 | |
| 				&types.Node{
 | |
| 					ID:       2,
 | |
| 					Hostname: "ts-unstable-rlwpvr",
 | |
| 					IPv4:     iap("100.64.0.4"),
 | |
| 					IPv6:     iap("fd7a:115c:a1e0::4"),
 | |
| 					User:     types.User{Name: "user1"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "failing-edge-case-during-p3-refactor",
 | |
| 			args: args{
 | |
| 				nodes: []*types.Node{
 | |
| 					{
 | |
| 						ID:       1,
 | |
| 						IPv4:     iap("100.64.0.2"),
 | |
| 						Hostname: "peer1",
 | |
| 						User:     types.User{Name: "mini"},
 | |
| 					},
 | |
| 					{
 | |
| 						ID:       2,
 | |
| 						IPv4:     iap("100.64.0.3"),
 | |
| 						Hostname: "peer2",
 | |
| 						User:     types.User{Name: "peer2"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{
 | |
| 					{
 | |
| 						SrcIPs: []string{"100.64.0.1/32"},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "::/0", Ports: tailcfg.PortRangeAny},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{
 | |
| 					ID:       0,
 | |
| 					IPv4:     iap("100.64.0.1"),
 | |
| 					Hostname: "mini",
 | |
| 					User:     types.User{Name: "mini"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: []*types.Node{
 | |
| 				{
 | |
| 					ID:       2,
 | |
| 					IPv4:     iap("100.64.0.3"),
 | |
| 					Hostname: "peer2",
 | |
| 					User:     types.User{Name: "peer2"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "p4-host-in-netmap-user2-dest-bug",
 | |
| 			args: args{
 | |
| 				nodes: []*types.Node{
 | |
| 					{
 | |
| 						ID:       1,
 | |
| 						IPv4:     iap("100.64.0.2"),
 | |
| 						Hostname: "user1-2",
 | |
| 						User:     types.User{Name: "user1"},
 | |
| 					},
 | |
| 					{
 | |
| 						ID:       0,
 | |
| 						IPv4:     iap("100.64.0.1"),
 | |
| 						Hostname: "user1-1",
 | |
| 						User:     types.User{Name: "user1"},
 | |
| 					},
 | |
| 					{
 | |
| 						ID:       3,
 | |
| 						IPv4:     iap("100.64.0.4"),
 | |
| 						Hostname: "user2-2",
 | |
| 						User:     types.User{Name: "user2"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{
 | |
| 					{
 | |
| 						SrcIPs: []string{
 | |
| 							"100.64.0.3/32",
 | |
| 							"100.64.0.4/32",
 | |
| 							"fd7a:115c:a1e0::3/128",
 | |
| 							"fd7a:115c:a1e0::4/128",
 | |
| 						},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
 | |
| 						},
 | |
| 					},
 | |
| 					{
 | |
| 						SrcIPs: []string{
 | |
| 							"100.64.0.1/32",
 | |
| 							"100.64.0.2/32",
 | |
| 							"fd7a:115c:a1e0::1/128",
 | |
| 							"fd7a:115c:a1e0::2/128",
 | |
| 						},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{
 | |
| 					ID:       2,
 | |
| 					IPv4:     iap("100.64.0.3"),
 | |
| 					Hostname: "user-2-1",
 | |
| 					User:     types.User{Name: "user2"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: []*types.Node{
 | |
| 				{
 | |
| 					ID:       1,
 | |
| 					IPv4:     iap("100.64.0.2"),
 | |
| 					Hostname: "user1-2",
 | |
| 					User:     types.User{Name: "user1"},
 | |
| 				},
 | |
| 				{
 | |
| 					ID:       0,
 | |
| 					IPv4:     iap("100.64.0.1"),
 | |
| 					Hostname: "user1-1",
 | |
| 					User:     types.User{Name: "user1"},
 | |
| 				},
 | |
| 				{
 | |
| 					ID:       3,
 | |
| 					IPv4:     iap("100.64.0.4"),
 | |
| 					Hostname: "user2-2",
 | |
| 					User:     types.User{Name: "user2"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "p4-host-in-netmap-user1-dest-bug",
 | |
| 			args: args{
 | |
| 				nodes: []*types.Node{
 | |
| 					{
 | |
| 						ID:       1,
 | |
| 						IPv4:     iap("100.64.0.2"),
 | |
| 						Hostname: "user1-2",
 | |
| 						User:     types.User{Name: "user1"},
 | |
| 					},
 | |
| 					{
 | |
| 						ID:       2,
 | |
| 						IPv4:     iap("100.64.0.3"),
 | |
| 						Hostname: "user-2-1",
 | |
| 						User:     types.User{Name: "user2"},
 | |
| 					},
 | |
| 					{
 | |
| 						ID:       3,
 | |
| 						IPv4:     iap("100.64.0.4"),
 | |
| 						Hostname: "user2-2",
 | |
| 						User:     types.User{Name: "user2"},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{
 | |
| 					{
 | |
| 						SrcIPs: []string{
 | |
| 							"100.64.0.1/32",
 | |
| 							"100.64.0.2/32",
 | |
| 							"fd7a:115c:a1e0::1/128",
 | |
| 							"fd7a:115c:a1e0::2/128",
 | |
| 						},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "100.64.0.1/32", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "100.64.0.2/32", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny},
 | |
| 						},
 | |
| 					},
 | |
| 					{
 | |
| 						SrcIPs: []string{
 | |
| 							"100.64.0.1/32",
 | |
| 							"100.64.0.2/32",
 | |
| 							"fd7a:115c:a1e0::1/128",
 | |
| 							"fd7a:115c:a1e0::2/128",
 | |
| 						},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
 | |
| 							{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{
 | |
| 					ID:       0,
 | |
| 					IPv4:     iap("100.64.0.1"),
 | |
| 					Hostname: "user1-1",
 | |
| 					User:     types.User{Name: "user1"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: []*types.Node{
 | |
| 				{
 | |
| 					ID:       1,
 | |
| 					IPv4:     iap("100.64.0.2"),
 | |
| 					Hostname: "user1-2",
 | |
| 					User:     types.User{Name: "user1"},
 | |
| 				},
 | |
| 				{
 | |
| 					ID:       2,
 | |
| 					IPv4:     iap("100.64.0.3"),
 | |
| 					Hostname: "user-2-1",
 | |
| 					User:     types.User{Name: "user2"},
 | |
| 				},
 | |
| 				{
 | |
| 					ID:       3,
 | |
| 					IPv4:     iap("100.64.0.4"),
 | |
| 					Hostname: "user2-2",
 | |
| 					User:     types.User{Name: "user2"},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 
 | |
| 		{
 | |
| 			name: "subnet-router-with-only-route",
 | |
| 			args: args{
 | |
| 				nodes: []*types.Node{
 | |
| 					{
 | |
| 						ID:       1,
 | |
| 						IPv4:     iap("100.64.0.1"),
 | |
| 						Hostname: "user1",
 | |
| 						User:     types.User{Name: "user1"},
 | |
| 					},
 | |
| 					{
 | |
| 						ID:       2,
 | |
| 						IPv4:     iap("100.64.0.2"),
 | |
| 						Hostname: "router",
 | |
| 						User:     types.User{Name: "router"},
 | |
| 						Routes: types.Routes{
 | |
| 							types.Route{
 | |
| 								NodeID:    2,
 | |
| 								Prefix:    netip.MustParsePrefix("10.33.0.0/16"),
 | |
| 								IsPrimary: true,
 | |
| 								Enabled:   true,
 | |
| 							},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				rules: []tailcfg.FilterRule{
 | |
| 					{
 | |
| 						SrcIPs: []string{
 | |
| 							"100.64.0.1/32",
 | |
| 						},
 | |
| 						DstPorts: []tailcfg.NetPortRange{
 | |
| 							{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				node: &types.Node{
 | |
| 					ID:       1,
 | |
| 					IPv4:     iap("100.64.0.1"),
 | |
| 					Hostname: "user1",
 | |
| 					User:     types.User{Name: "user1"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: []*types.Node{
 | |
| 				{
 | |
| 					ID:       2,
 | |
| 					IPv4:     iap("100.64.0.2"),
 | |
| 					Hostname: "router",
 | |
| 					User:     types.User{Name: "router"},
 | |
| 					Routes: types.Routes{
 | |
| 						types.Route{
 | |
| 							NodeID:    2,
 | |
| 							Prefix:    netip.MustParsePrefix("10.33.0.0/16"),
 | |
| 							IsPrimary: true,
 | |
| 							Enabled:   true,
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			got := FilterNodesByACL(
 | |
| 				tt.args.node,
 | |
| 				tt.args.nodes,
 | |
| 				tt.args.rules,
 | |
| 			)
 | |
| 			if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
 | |
| 				t.Errorf("FilterNodesByACL() unexpected result (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestSSHRules(t *testing.T) {
 | |
| 	users := []types.User{
 | |
| 		{
 | |
| 			Name: "user1",
 | |
| 		},
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name  string
 | |
| 		node  types.Node
 | |
| 		peers types.Nodes
 | |
| 		pol   ACLPolicy
 | |
| 		want  *tailcfg.SSHPolicy
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "peers-can-connect",
 | |
| 			node: types.Node{
 | |
| 				Hostname: "testnodes",
 | |
| 				IPv4:     iap("100.64.99.42"),
 | |
| 				UserID:   0,
 | |
| 				User:     users[0],
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					Hostname: "testnodes2",
 | |
| 					IPv4:     iap("100.64.0.1"),
 | |
| 					UserID:   0,
 | |
| 					User:     users[0],
 | |
| 				},
 | |
| 			},
 | |
| 			pol: ACLPolicy{
 | |
| 				Groups: Groups{
 | |
| 					"group:test": []string{"user1"},
 | |
| 				},
 | |
| 				Hosts: Hosts{
 | |
| 					"client": netip.PrefixFrom(netip.MustParseAddr("100.64.99.42"), 32),
 | |
| 				},
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"*"},
 | |
| 						Destinations: []string{"*:*"},
 | |
| 					},
 | |
| 				},
 | |
| 				SSHs: []SSH{
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"group:test"},
 | |
| 						Destinations: []string{"client"},
 | |
| 						Users:        []string{"autogroup:nonroot"},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"*"},
 | |
| 						Destinations: []string{"client"},
 | |
| 						Users:        []string{"autogroup:nonroot"},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"group:test"},
 | |
| 						Destinations: []string{"100.64.99.42"},
 | |
| 						Users:        []string{"autogroup:nonroot"},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"*"},
 | |
| 						Destinations: []string{"100.64.99.42"},
 | |
| 						Users:        []string{"autogroup:nonroot"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{
 | |
| 				{
 | |
| 					Principals: []*tailcfg.SSHPrincipal{
 | |
| 						{
 | |
| 							UserLogin: "user1",
 | |
| 						},
 | |
| 					},
 | |
| 					SSHUsers: map[string]string{
 | |
| 						"autogroup:nonroot": "=",
 | |
| 					},
 | |
| 					Action: &tailcfg.SSHAction{
 | |
| 						Accept:                   true,
 | |
| 						AllowAgentForwarding:     true,
 | |
| 						AllowLocalPortForwarding: true,
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					SSHUsers: map[string]string{
 | |
| 						"autogroup:nonroot": "=",
 | |
| 					},
 | |
| 					Principals: []*tailcfg.SSHPrincipal{
 | |
| 						{
 | |
| 							Any: true,
 | |
| 						},
 | |
| 					},
 | |
| 					Action: &tailcfg.SSHAction{
 | |
| 						Accept:                   true,
 | |
| 						AllowAgentForwarding:     true,
 | |
| 						AllowLocalPortForwarding: true,
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Principals: []*tailcfg.SSHPrincipal{
 | |
| 						{
 | |
| 							UserLogin: "user1",
 | |
| 						},
 | |
| 					},
 | |
| 					SSHUsers: map[string]string{
 | |
| 						"autogroup:nonroot": "=",
 | |
| 					},
 | |
| 					Action: &tailcfg.SSHAction{
 | |
| 						Accept:                   true,
 | |
| 						AllowAgentForwarding:     true,
 | |
| 						AllowLocalPortForwarding: true,
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					SSHUsers: map[string]string{
 | |
| 						"autogroup:nonroot": "=",
 | |
| 					},
 | |
| 					Principals: []*tailcfg.SSHPrincipal{
 | |
| 						{
 | |
| 							Any: true,
 | |
| 						},
 | |
| 					},
 | |
| 					Action: &tailcfg.SSHAction{
 | |
| 						Accept:                   true,
 | |
| 						AllowAgentForwarding:     true,
 | |
| 						AllowLocalPortForwarding: true,
 | |
| 					},
 | |
| 				},
 | |
| 			}},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "peers-cannot-connect",
 | |
| 			node: types.Node{
 | |
| 				Hostname: "testnodes",
 | |
| 				IPv4:     iap("100.64.0.1"),
 | |
| 				UserID:   0,
 | |
| 				User:     users[0],
 | |
| 			},
 | |
| 			peers: types.Nodes{
 | |
| 				&types.Node{
 | |
| 					Hostname: "testnodes2",
 | |
| 					IPv4:     iap("100.64.99.42"),
 | |
| 					UserID:   0,
 | |
| 					User:     users[0],
 | |
| 				},
 | |
| 			},
 | |
| 			pol: ACLPolicy{
 | |
| 				Groups: Groups{
 | |
| 					"group:test": []string{"user1"},
 | |
| 				},
 | |
| 				Hosts: Hosts{
 | |
| 					"client": netip.PrefixFrom(netip.MustParseAddr("100.64.99.42"), 32),
 | |
| 				},
 | |
| 				ACLs: []ACL{
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"*"},
 | |
| 						Destinations: []string{"*:*"},
 | |
| 					},
 | |
| 				},
 | |
| 				SSHs: []SSH{
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"group:test"},
 | |
| 						Destinations: []string{"100.64.99.42"},
 | |
| 						Users:        []string{"autogroup:nonroot"},
 | |
| 					},
 | |
| 					{
 | |
| 						Action:       "accept",
 | |
| 						Sources:      []string{"*"},
 | |
| 						Destinations: []string{"100.64.99.42"},
 | |
| 						Users:        []string{"autogroup:nonroot"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			want: &tailcfg.SSHPolicy{Rules: nil},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			got, err := tt.pol.CompileSSHPolicy(&tt.node, users, tt.peers)
 | |
| 			require.NoError(t, err)
 | |
| 
 | |
| 			if diff := cmp.Diff(tt.want, got); diff != "" {
 | |
| 				t.Errorf("TestSSHRules() unexpected result (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestParseDestination(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		dest      string
 | |
| 		wantAlias string
 | |
| 		wantPort  string
 | |
| 	}{
 | |
| 		{
 | |
| 			dest:      "git-server:*",
 | |
| 			wantAlias: "git-server",
 | |
| 			wantPort:  "*",
 | |
| 		},
 | |
| 		{
 | |
| 			dest:      "192.168.1.0/24:22",
 | |
| 			wantAlias: "192.168.1.0/24",
 | |
| 			wantPort:  "22",
 | |
| 		},
 | |
| 		{
 | |
| 			dest:      "192.168.1.1:22",
 | |
| 			wantAlias: "192.168.1.1",
 | |
| 			wantPort:  "22",
 | |
| 		},
 | |
| 		{
 | |
| 			dest:      "fd7a:115c:a1e0::2:22",
 | |
| 			wantAlias: "fd7a:115c:a1e0::2",
 | |
| 			wantPort:  "22",
 | |
| 		},
 | |
| 		{
 | |
| 			dest:      "fd7a:115c:a1e0::2/128:22",
 | |
| 			wantAlias: "fd7a:115c:a1e0::2/128",
 | |
| 			wantPort:  "22",
 | |
| 		},
 | |
| 		{
 | |
| 			dest:      "tag:montreal-webserver:80,443",
 | |
| 			wantAlias: "tag:montreal-webserver",
 | |
| 			wantPort:  "80,443",
 | |
| 		},
 | |
| 		{
 | |
| 			dest:      "tag:api-server:443",
 | |
| 			wantAlias: "tag:api-server",
 | |
| 			wantPort:  "443",
 | |
| 		},
 | |
| 		{
 | |
| 			dest:      "example-host-1:*",
 | |
| 			wantAlias: "example-host-1",
 | |
| 			wantPort:  "*",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.dest, func(t *testing.T) {
 | |
| 			alias, port, _ := parseDestination(tt.dest)
 | |
| 
 | |
| 			if alias != tt.wantAlias {
 | |
| 				t.Errorf("unexpected alias: want(%s) != got(%s)", tt.wantAlias, alias)
 | |
| 			}
 | |
| 
 | |
| 			if port != tt.wantPort {
 | |
| 				t.Errorf("unexpected port: want(%s) != got(%s)", tt.wantPort, port)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // this test should validate that we can expand a group in a TagOWner section and
 | |
| // match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
 | |
| // the tag is matched in the Sources section.
 | |
| func TestValidExpandTagOwnersInSources(t *testing.T) {
 | |
| 	hostInfo := tailcfg.Hostinfo{
 | |
| 		OS:          "centos",
 | |
| 		Hostname:    "testnodes",
 | |
| 		RequestTags: []string{"tag:test"},
 | |
| 	}
 | |
| 
 | |
| 	user := types.User{
 | |
| 		Model: gorm.Model{ID: 1},
 | |
| 		Name:  "user1",
 | |
| 	}
 | |
| 
 | |
| 	node := &types.Node{
 | |
| 		ID:             0,
 | |
| 		Hostname:       "testnodes",
 | |
| 		IPv4:           iap("100.64.0.1"),
 | |
| 		UserID:         0,
 | |
| 		User:           user,
 | |
| 		RegisterMethod: util.RegisterMethodAuthKey,
 | |
| 		Hostinfo:       &hostInfo,
 | |
| 	}
 | |
| 
 | |
| 	pol := &ACLPolicy{
 | |
| 		Groups:    Groups{"group:test": []string{"user1", "user2"}},
 | |
| 		TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
 | |
| 		ACLs: []ACL{
 | |
| 			{
 | |
| 				Action:       "accept",
 | |
| 				Sources:      []string{"tag:test"},
 | |
| 				Destinations: []string{"*:*"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{}, []types.User{user})
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	want := []tailcfg.FilterRule{
 | |
| 		{
 | |
| 			SrcIPs: []string{"100.64.0.1/32"},
 | |
| 			DstPorts: []tailcfg.NetPortRange{
 | |
| 				{IP: "0.0.0.0/0", Ports: tailcfg.PortRange{Last: 65535}},
 | |
| 				{IP: "::/0", Ports: tailcfg.PortRange{Last: 65535}},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	if diff := cmp.Diff(want, got); diff != "" {
 | |
| 		t.Errorf("TestValidExpandTagOwnersInSources() unexpected result (-want +got):\n%s", diff)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // need a test with:
 | |
| // tag on a host that isn't owned by a tag owners. So the user
 | |
| // of the host should be valid.
 | |
| func TestInvalidTagValidUser(t *testing.T) {
 | |
| 	hostInfo := tailcfg.Hostinfo{
 | |
| 		OS:          "centos",
 | |
| 		Hostname:    "testnodes",
 | |
| 		RequestTags: []string{"tag:foo"},
 | |
| 	}
 | |
| 
 | |
| 	node := &types.Node{
 | |
| 		ID:       1,
 | |
| 		Hostname: "testnodes",
 | |
| 		IPv4:     iap("100.64.0.1"),
 | |
| 		UserID:   1,
 | |
| 		User: types.User{
 | |
| 			Model: gorm.Model{ID: 1},
 | |
| 			Name:  "user1",
 | |
| 		},
 | |
| 		RegisterMethod: util.RegisterMethodAuthKey,
 | |
| 		Hostinfo:       &hostInfo,
 | |
| 	}
 | |
| 
 | |
| 	pol := &ACLPolicy{
 | |
| 		TagOwners: TagOwners{"tag:test": []string{"user1"}},
 | |
| 		ACLs: []ACL{
 | |
| 			{
 | |
| 				Action:       "accept",
 | |
| 				Sources:      []string{"user1"},
 | |
| 				Destinations: []string{"*:*"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	got, _, err := GenerateFilterAndSSHRulesForTests(
 | |
| 		pol,
 | |
| 		node,
 | |
| 		types.Nodes{},
 | |
| 		[]types.User{node.User},
 | |
| 	)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	want := []tailcfg.FilterRule{
 | |
| 		{
 | |
| 			SrcIPs: []string{"100.64.0.1/32"},
 | |
| 			DstPorts: []tailcfg.NetPortRange{
 | |
| 				{IP: "0.0.0.0/0", Ports: tailcfg.PortRange{Last: 65535}},
 | |
| 				{IP: "::/0", Ports: tailcfg.PortRange{Last: 65535}},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	if diff := cmp.Diff(want, got); diff != "" {
 | |
| 		t.Errorf("TestInvalidTagValidUser() unexpected result (-want +got):\n%s", diff)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // this test should validate that we can expand a group in a TagOWner section and
 | |
| // match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
 | |
| // the tag is matched in the Destinations section.
 | |
| func TestValidExpandTagOwnersInDestinations(t *testing.T) {
 | |
| 	hostInfo := tailcfg.Hostinfo{
 | |
| 		OS:          "centos",
 | |
| 		Hostname:    "testnodes",
 | |
| 		RequestTags: []string{"tag:test"},
 | |
| 	}
 | |
| 
 | |
| 	node := &types.Node{
 | |
| 		ID:       1,
 | |
| 		Hostname: "testnodes",
 | |
| 		IPv4:     iap("100.64.0.1"),
 | |
| 		UserID:   1,
 | |
| 		User: types.User{
 | |
| 			Model: gorm.Model{ID: 1},
 | |
| 			Name:  "user1",
 | |
| 		},
 | |
| 		RegisterMethod: util.RegisterMethodAuthKey,
 | |
| 		Hostinfo:       &hostInfo,
 | |
| 	}
 | |
| 
 | |
| 	pol := &ACLPolicy{
 | |
| 		Groups:    Groups{"group:test": []string{"user1", "user2"}},
 | |
| 		TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
 | |
| 		ACLs: []ACL{
 | |
| 			{
 | |
| 				Action:       "accept",
 | |
| 				Sources:      []string{"*"},
 | |
| 				Destinations: []string{"tag:test:*"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	// rules, _, err := GenerateFilterRules(pol, &node, peers, false)
 | |
| 	// c.Assert(err, check.IsNil)
 | |
| 	//
 | |
| 	// c.Assert(rules, check.HasLen, 1)
 | |
| 	// c.Assert(rules[0].DstPorts, check.HasLen, 1)
 | |
| 	// c.Assert(rules[0].DstPorts[0].IP, check.Equals, "100.64.0.1/32")
 | |
| 
 | |
| 	got, _, err := GenerateFilterAndSSHRulesForTests(
 | |
| 		pol,
 | |
| 		node,
 | |
| 		types.Nodes{},
 | |
| 		[]types.User{node.User},
 | |
| 	)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	want := []tailcfg.FilterRule{
 | |
| 		{
 | |
| 			SrcIPs: []string{"0.0.0.0/0", "::/0"},
 | |
| 			DstPorts: []tailcfg.NetPortRange{
 | |
| 				{IP: "100.64.0.1/32", Ports: tailcfg.PortRange{Last: 65535}},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	if diff := cmp.Diff(want, got); diff != "" {
 | |
| 		t.Errorf(
 | |
| 			"TestValidExpandTagOwnersInDestinations() unexpected result (-want +got):\n%s",
 | |
| 			diff,
 | |
| 		)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // tag on a host is owned by a tag owner, the tag is valid.
 | |
| // an ACL rule is matching the tag to a user. It should not be valid since the
 | |
| // host should be tied to the tag now.
 | |
| func TestValidTagInvalidUser(t *testing.T) {
 | |
| 	hostInfo := tailcfg.Hostinfo{
 | |
| 		OS:          "centos",
 | |
| 		Hostname:    "webserver",
 | |
| 		RequestTags: []string{"tag:webapp"},
 | |
| 	}
 | |
| 	user := types.User{
 | |
| 		Model: gorm.Model{ID: 1},
 | |
| 		Name:  "user1",
 | |
| 	}
 | |
| 
 | |
| 	node := &types.Node{
 | |
| 		ID:             1,
 | |
| 		Hostname:       "webserver",
 | |
| 		IPv4:           iap("100.64.0.1"),
 | |
| 		UserID:         1,
 | |
| 		User:           user,
 | |
| 		RegisterMethod: util.RegisterMethodAuthKey,
 | |
| 		Hostinfo:       &hostInfo,
 | |
| 	}
 | |
| 
 | |
| 	hostInfo2 := tailcfg.Hostinfo{
 | |
| 		OS:       "debian",
 | |
| 		Hostname: "Hostname",
 | |
| 	}
 | |
| 
 | |
| 	nodes2 := &types.Node{
 | |
| 		ID:             2,
 | |
| 		Hostname:       "user",
 | |
| 		IPv4:           iap("100.64.0.2"),
 | |
| 		UserID:         1,
 | |
| 		User:           user,
 | |
| 		RegisterMethod: util.RegisterMethodAuthKey,
 | |
| 		Hostinfo:       &hostInfo2,
 | |
| 	}
 | |
| 
 | |
| 	pol := &ACLPolicy{
 | |
| 		TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
 | |
| 		ACLs: []ACL{
 | |
| 			{
 | |
| 				Action:       "accept",
 | |
| 				Sources:      []string{"user1"},
 | |
| 				Destinations: []string{"tag:webapp:80,443"},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	got, _, err := GenerateFilterAndSSHRulesForTests(
 | |
| 		pol,
 | |
| 		node,
 | |
| 		types.Nodes{nodes2},
 | |
| 		[]types.User{user},
 | |
| 	)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	want := []tailcfg.FilterRule{
 | |
| 		{
 | |
| 			SrcIPs: []string{"100.64.0.2/32"},
 | |
| 			DstPorts: []tailcfg.NetPortRange{
 | |
| 				{IP: "100.64.0.1/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
 | |
| 				{IP: "100.64.0.1/32", Ports: tailcfg.PortRange{First: 443, Last: 443}},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	if diff := cmp.Diff(want, got); diff != "" {
 | |
| 		t.Errorf("TestValidTagInvalidUser() unexpected result (-want +got):\n%s", diff)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestFindUserByToken(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		users   []types.User
 | |
| 		token   string
 | |
| 		want    types.User
 | |
| 		wantErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "exact match by ProviderIdentifier",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "token1"}},
 | |
| 				{Email: "user2@example.com"},
 | |
| 			},
 | |
| 			token:   "token1",
 | |
| 			want:    types.User{ProviderIdentifier: sql.NullString{Valid: true, String: "token1"}},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "no matches found",
 | |
| 			users: []types.User{
 | |
| 				{Email: "user1@example.com"},
 | |
| 				{Name: "username"},
 | |
| 			},
 | |
| 			token:   "nonexistent-token",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multiple matches by email and name",
 | |
| 			users: []types.User{
 | |
| 				{Email: "token2", Name: "notoken"},
 | |
| 				{Name: "token2", Email: "notoken@example.com"},
 | |
| 			},
 | |
| 			token:   "token2",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "match by email",
 | |
| 			users: []types.User{
 | |
| 				{Email: "token3@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "othertoken"}},
 | |
| 			},
 | |
| 			token:   "token3@example.com",
 | |
| 			want:    types.User{Email: "token3@example.com"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "match by name",
 | |
| 			users: []types.User{
 | |
| 				{Name: "token4"},
 | |
| 				{Email: "user5@example.com"},
 | |
| 			},
 | |
| 			token:   "token4",
 | |
| 			want:    types.User{Name: "token4"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "provider identifier takes precedence over email and name matches",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "token5"}},
 | |
| 				{Email: "token5@example.com", Name: "token5"},
 | |
| 			},
 | |
| 			token:   "token5",
 | |
| 			want:    types.User{ProviderIdentifier: sql.NullString{Valid: true, String: "token5"}},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "empty token finds no users",
 | |
| 			users: []types.User{
 | |
| 				{Email: "user6@example.com"},
 | |
| 				{Name: "username6"},
 | |
| 			},
 | |
| 			token:   "",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		// Test case 1: Duplicate Emails with Unique ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "duplicate emails with unique provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid1"}, Email: "user@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid2"}, Email: "user@example.com"},
 | |
| 			},
 | |
| 			token:   "user@example.com",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 2: Duplicate Names with Unique ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "duplicate names with unique provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid3"}, Name: "John Doe"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid4"}, Name: "John Doe"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 3: Duplicate Emails and Names with Unique ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "duplicate emails and names with unique provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid5"}, Email: "user@example.com", Name: "John Doe"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid6"}, Email: "user@example.com", Name: "John Doe"},
 | |
| 			},
 | |
| 			token:   "user@example.com",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 4: Unique Names without ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "unique names without provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "janesmith@example.com"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 5: Duplicate Emails without ProviderIdentifiers but Unique Names
 | |
| 		{
 | |
| 			name: "duplicate emails without provider identifiers but unique names",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "user@example.com"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 6: Duplicate Names and Emails without ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "duplicate names and emails without provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 7: Multiple Users with the Same Email but Different Names and Unique ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "multiple users with same email, different names, unique provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid7"}, Email: "user@example.com", Name: "John Doe"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid8"}, Email: "user@example.com", Name: "Jane Smith"},
 | |
| 			},
 | |
| 			token:   "user@example.com",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 8: Multiple Users with the Same Name but Different Emails and Unique ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "multiple users with same name, different emails, unique provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid9"}, Email: "johndoe@example.com", Name: "John Doe"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid10"}, Email: "janedoe@example.com", Name: "John Doe"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 9: Multiple Users with Same Email and Name but Unique ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "multiple users with same email and name, unique provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid11"}, Email: "user@example.com", Name: "John Doe"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid12"}, Email: "user@example.com", Name: "John Doe"},
 | |
| 			},
 | |
| 			token:   "user@example.com",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 10: Multiple Users without ProviderIdentifiers but with Unique Names and Emails
 | |
| 		{
 | |
| 			name: "multiple users without provider identifiers, unique names and emails",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "janesmith@example.com"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 11: Multiple Users without ProviderIdentifiers and Duplicate Emails but Unique Names
 | |
| 		{
 | |
| 			name: "multiple users without provider identifiers, duplicate emails but unique names",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "user@example.com"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 12: Multiple Users without ProviderIdentifiers and Duplicate Names but Unique Emails
 | |
| 		{
 | |
| 			name: "multiple users without provider identifiers, duplicate names but unique emails",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "janedoe@example.com"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 13: Multiple Users without ProviderIdentifiers and Duplicate Both Names and Emails
 | |
| 		{
 | |
| 			name: "multiple users without provider identifiers, duplicate names and emails",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 14: Multiple Users with Same Email Without ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "multiple users with same email without provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "user@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "Jane Smith", Email: "user@example.com"},
 | |
| 			},
 | |
| 			token:   "user@example.com",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 
 | |
| 		// Test case 15: Multiple Users with Same Name Without ProviderIdentifiers
 | |
| 		{
 | |
| 			name: "multiple users with same name without provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "johndoe@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "John Doe", Email: "janedoe@example.com"},
 | |
| 			},
 | |
| 			token:   "John Doe",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Name field used as email address match",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid3"}, Name: "user@example.com", Email: "another@example.com"},
 | |
| 			},
 | |
| 			token:   "user@example.com",
 | |
| 			want:    types.User{ProviderIdentifier: sql.NullString{Valid: true, String: "pid3"}, Name: "user@example.com", Email: "another@example.com"},
 | |
| 			wantErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multiple users with same name as email and unique provider identifiers",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid4"}, Name: "user@example.com", Email: "user1@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: true, String: "pid5"}, Name: "user@example.com", Email: "user2@example.com"},
 | |
| 			},
 | |
| 			token:   "user@example.com",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "no provider identifier and duplicate names as emails",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "user@example.com", Email: "another1@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "user@example.com", Email: "another2@example.com"},
 | |
| 			},
 | |
| 			token:   "user@example.com",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "name as email with multiple matches when provider identifier is not set",
 | |
| 			users: []types.User{
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "user@example.com", Email: "another1@example.com"},
 | |
| 				{ProviderIdentifier: sql.NullString{Valid: false, String: ""}, Name: "user@example.com", Email: "another2@example.com"},
 | |
| 			},
 | |
| 			token:   "user@example.com",
 | |
| 			want:    types.User{},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			gotUser, err := findUserFromToken(tt.users, tt.token)
 | |
| 			if (err != nil) != tt.wantErr {
 | |
| 				t.Errorf("findUserFromToken() error = %v, wantErr %v", err, tt.wantErr)
 | |
| 				return
 | |
| 			}
 | |
| 			if diff := cmp.Diff(tt.want, gotUser, util.Comparers...); diff != "" {
 | |
| 				t.Errorf("findUserFromToken() unexpected result (-want +got):\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |