headscale/hscontrol/policy/v1/acls_test.go
Kristoffer Dalby 87326f5c4f
Experimental implementation of Policy v2 (#2214)
* utility iterator for ipset

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* split policy -> policy and v1

This commit split out the common policy logic and policy implementation
into separate packages.

policy contains functions that are independent of the policy implementation,
this typically means logic that works on tailcfg types and generic formats.
In addition, it defines the PolicyManager interface which the v1 implements.

v1 is a subpackage which implements the PolicyManager using the "original"
policy implementation.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use polivyv1 definitions in integration tests

These can be marshalled back into JSON, which the
new format might not be able to.

Also, just dont change it all to JSON strings for now.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* formatter: breaks lines

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove compareprefix, use tsaddr version

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove getacl test, add back autoapprover

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use policy manager tag handling

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* rename display helper for user

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* introduce policy v2 package

policy v2 is built from the ground up to be stricter
and follow the same pattern for all types of resolvers.

TODO introduce
aliass
resolver

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* wire up policyv2 in integration testing

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* split policy v2 tests into seperate workflow to work around github limit

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add policy manager output to /debug

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2025-03-10 16:20:29 +01:00

2982 lines
69 KiB
Go

package v1
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/stretchr/testify/require"
"go4.org/netipx"
"gopkg.in/check.v1"
"gorm.io/gorm"
"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) {
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,
},
}
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 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)
}
})
}
}