make tags first class node owner (#2885)

This PR changes tags to be something that exists on nodes in addition to users, to being its own thing. It is part of moving our tags support towards the correct tailscale compatible implementation.

There are probably rough edges in this PR, but the intention is to get it in, and then start fixing bugs from 0.28.0 milestone (long standing tags issue) to discover what works and what doesnt.

Updates #2417
Closes #2619
This commit is contained in:
Kristoffer Dalby
2025-12-02 12:01:25 +01:00
committed by GitHub
parent 705b239677
commit eb788cd007
49 changed files with 3102 additions and 757 deletions

View File

@@ -32,11 +32,11 @@ func TestApproveRoutesWithPolicy_NeverRemovesApprovedRoutes(t *testing.T) {
MachineKey: key.NewMachine().Public(),
NodeKey: key.NewNode().Public(),
Hostname: "test-node",
UserID: user1.ID,
User: user1,
UserID: ptr.To(user1.ID),
User: ptr.To(user1),
RegisterMethod: util.RegisterMethodAuthKey,
IPv4: ptr.To(netip.MustParseAddr("100.64.0.1")),
ForcedTags: []string{"tag:test"},
Tags: []string{"tag:test"},
}
node2 := &types.Node{
@@ -44,8 +44,8 @@ func TestApproveRoutesWithPolicy_NeverRemovesApprovedRoutes(t *testing.T) {
MachineKey: key.NewMachine().Public(),
NodeKey: key.NewNode().Public(),
Hostname: "other-node",
UserID: user2.ID,
User: user2,
UserID: ptr.To(user2.ID),
User: ptr.To(user2),
RegisterMethod: util.RegisterMethodAuthKey,
IPv4: ptr.To(netip.MustParseAddr("100.64.0.2")),
}
@@ -304,8 +304,8 @@ func TestApproveRoutesWithPolicy_NilAndEmptyCases(t *testing.T) {
MachineKey: key.NewMachine().Public(),
NodeKey: key.NewNode().Public(),
Hostname: "testnode",
UserID: user.ID,
User: user,
UserID: ptr.To(user.ID),
User: ptr.To(user),
RegisterMethod: util.RegisterMethodAuthKey,
IPv4: ptr.To(netip.MustParseAddr("100.64.0.1")),
ApprovedRoutes: tt.currentApproved,

View File

@@ -168,15 +168,15 @@ func TestApproveRoutesWithPolicy_NeverRemovesRoutes(t *testing.T) {
MachineKey: key.NewMachine().Public(),
NodeKey: key.NewNode().Public(),
Hostname: tt.nodeHostname,
UserID: user.ID,
User: user,
UserID: ptr.To(user.ID),
User: ptr.To(user),
RegisterMethod: util.RegisterMethodAuthKey,
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: tt.announcedRoutes,
},
IPv4: ptr.To(netip.MustParseAddr("100.64.0.1")),
ApprovedRoutes: tt.currentApproved,
ForcedTags: tt.nodeTags,
Tags: tt.nodeTags,
}
nodes := types.Nodes{&node}
@@ -294,8 +294,8 @@ func TestApproveRoutesWithPolicy_EdgeCases(t *testing.T) {
MachineKey: key.NewMachine().Public(),
NodeKey: key.NewNode().Public(),
Hostname: "testnode",
UserID: user.ID,
User: user,
UserID: ptr.To(user.ID),
User: ptr.To(user),
RegisterMethod: util.RegisterMethodAuthKey,
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: tt.announcedRoutes,
@@ -343,8 +343,8 @@ func TestApproveRoutesWithPolicy_NilPolicyManagerCase(t *testing.T) {
MachineKey: key.NewMachine().Public(),
NodeKey: key.NewNode().Public(),
Hostname: "testnode",
UserID: user.ID,
User: user,
UserID: ptr.To(user.ID),
User: ptr.To(user),
RegisterMethod: util.RegisterMethodAuthKey,
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: announcedRoutes,

View File

@@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
)
var ap = func(ipStr string) *netip.Addr {
@@ -44,17 +45,17 @@ func TestReduceNodes(t *testing.T) {
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{
@@ -68,19 +69,19 @@ func TestReduceNodes(t *testing.T) {
node: &types.Node{ // current nodes
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
},
want: types.Nodes{
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
},
@@ -91,17 +92,17 @@ func TestReduceNodes(t *testing.T) {
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
@@ -115,14 +116,14 @@ func TestReduceNodes(t *testing.T) {
node: &types.Node{ // current nodes
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
},
want: types.Nodes{
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
},
},
@@ -133,17 +134,17 @@ func TestReduceNodes(t *testing.T) {
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
@@ -157,14 +158,14 @@ func TestReduceNodes(t *testing.T) {
node: &types.Node{ // current nodes
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
},
want: types.Nodes{
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
},
@@ -175,17 +176,17 @@ func TestReduceNodes(t *testing.T) {
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
@@ -199,14 +200,14 @@ func TestReduceNodes(t *testing.T) {
node: &types.Node{ // current nodes
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
},
want: types.Nodes{
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
},
},
@@ -217,17 +218,17 @@ func TestReduceNodes(t *testing.T) {
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
@@ -241,19 +242,19 @@ func TestReduceNodes(t *testing.T) {
node: &types.Node{ // current nodes
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
},
want: types.Nodes{
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
},
@@ -264,17 +265,17 @@ func TestReduceNodes(t *testing.T) {
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
@@ -288,19 +289,19 @@ func TestReduceNodes(t *testing.T) {
node: &types.Node{ // current nodes
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
},
want: types.Nodes{
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
},
@@ -311,17 +312,17 @@ func TestReduceNodes(t *testing.T) {
&types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "joe"},
User: &types.User{Name: "joe"},
},
&types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
&types.Node{
ID: 3,
IPv4: ap("100.64.0.3"),
User: types.User{Name: "mickael"},
User: &types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
@@ -329,7 +330,7 @@ func TestReduceNodes(t *testing.T) {
node: &types.Node{ // current nodes
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "marc"},
User: &types.User{Name: "marc"},
},
},
want: nil,
@@ -347,28 +348,28 @@ func TestReduceNodes(t *testing.T) {
Hostname: "ts-head-upcrmb",
IPv4: ap("100.64.0.3"),
IPv6: ap("fd7a:115c:a1e0::3"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
&types.Node{
ID: 2,
Hostname: "ts-unstable-rlwpvr",
IPv4: ap("100.64.0.4"),
IPv6: ap("fd7a:115c:a1e0::4"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
&types.Node{
ID: 3,
Hostname: "ts-head-8w6paa",
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
&types.Node{
ID: 4,
Hostname: "ts-unstable-lys2ib",
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
@@ -390,7 +391,7 @@ func TestReduceNodes(t *testing.T) {
Hostname: "ts-head-8w6paa",
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
},
want: types.Nodes{
@@ -399,14 +400,14 @@ func TestReduceNodes(t *testing.T) {
Hostname: "ts-head-upcrmb",
IPv4: ap("100.64.0.3"),
IPv6: ap("fd7a:115c:a1e0::3"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
&types.Node{
ID: 2,
Hostname: "ts-unstable-rlwpvr",
IPv4: ap("100.64.0.4"),
IPv6: ap("fd7a:115c:a1e0::4"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
},
},
@@ -418,13 +419,13 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "peer1",
User: types.User{Name: "mini"},
User: &types.User{Name: "mini"},
},
{
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "peer2",
User: types.User{Name: "peer2"},
User: &types.User{Name: "peer2"},
},
},
rules: []tailcfg.FilterRule{
@@ -440,7 +441,7 @@ func TestReduceNodes(t *testing.T) {
ID: 0,
IPv4: ap("100.64.0.1"),
Hostname: "mini",
User: types.User{Name: "mini"},
User: &types.User{Name: "mini"},
},
},
want: []*types.Node{
@@ -448,7 +449,7 @@ func TestReduceNodes(t *testing.T) {
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "peer2",
User: types.User{Name: "peer2"},
User: &types.User{Name: "peer2"},
},
},
},
@@ -460,19 +461,19 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "user1-2",
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
{
ID: 0,
IPv4: ap("100.64.0.1"),
Hostname: "user1-1",
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
{
ID: 3,
IPv4: ap("100.64.0.4"),
Hostname: "user2-2",
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
},
rules: []tailcfg.FilterRule{
@@ -509,7 +510,7 @@ func TestReduceNodes(t *testing.T) {
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "user-2-1",
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
},
want: []*types.Node{
@@ -517,19 +518,19 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "user1-2",
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
{
ID: 0,
IPv4: ap("100.64.0.1"),
Hostname: "user1-1",
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
{
ID: 3,
IPv4: ap("100.64.0.4"),
Hostname: "user2-2",
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
},
},
@@ -541,19 +542,19 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "user1-2",
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
{
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "user-2-1",
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
{
ID: 3,
IPv4: ap("100.64.0.4"),
Hostname: "user2-2",
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
},
rules: []tailcfg.FilterRule{
@@ -590,7 +591,7 @@ func TestReduceNodes(t *testing.T) {
ID: 0,
IPv4: ap("100.64.0.1"),
Hostname: "user1-1",
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
},
want: []*types.Node{
@@ -598,19 +599,19 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.2"),
Hostname: "user1-2",
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
{
ID: 2,
IPv4: ap("100.64.0.3"),
Hostname: "user-2-1",
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
{
ID: 3,
IPv4: ap("100.64.0.4"),
Hostname: "user2-2",
User: types.User{Name: "user2"},
User: &types.User{Name: "user2"},
},
},
},
@@ -622,13 +623,13 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "user1",
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
{
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "router",
User: types.User{Name: "router"},
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
},
@@ -649,7 +650,7 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "user1",
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
},
want: []*types.Node{
@@ -657,7 +658,7 @@ func TestReduceNodes(t *testing.T) {
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "router",
User: types.User{Name: "router"},
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
},
@@ -673,7 +674,7 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router",
User: types.User{Name: "router"},
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
@@ -683,7 +684,7 @@ func TestReduceNodes(t *testing.T) {
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "node",
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
},
rules: []tailcfg.FilterRule{
@@ -700,7 +701,7 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router",
User: types.User{Name: "router"},
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
@@ -712,7 +713,7 @@ func TestReduceNodes(t *testing.T) {
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "node",
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
},
},
@@ -724,7 +725,7 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router",
User: types.User{Name: "router"},
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
@@ -734,7 +735,7 @@ func TestReduceNodes(t *testing.T) {
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "node",
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
},
rules: []tailcfg.FilterRule{
@@ -751,7 +752,7 @@ func TestReduceNodes(t *testing.T) {
ID: 2,
IPv4: ap("100.64.0.2"),
Hostname: "node",
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
},
want: []*types.Node{
@@ -759,7 +760,7 @@ func TestReduceNodes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.1"),
Hostname: "router",
User: types.User{Name: "router"},
User: &types.User{Name: "router"},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.99.0.0/16")},
},
@@ -804,7 +805,7 @@ func TestReduceNodesFromPolicy(t *testing.T) {
ID: id,
IPv4: ap(ip),
Hostname: hostname,
User: types.User{Name: username},
User: &types.User{Name: username},
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: routes,
},
@@ -812,8 +813,6 @@ func TestReduceNodesFromPolicy(t *testing.T) {
}
}
type args struct {
}
tests := []struct {
name string
nodes types.Nodes
@@ -1075,22 +1074,22 @@ func TestSSHPolicyRules(t *testing.T) {
nodeUser1 := types.Node{
Hostname: "user1-device",
IPv4: ap("100.64.0.1"),
UserID: 1,
User: users[0],
UserID: ptr.To(uint(1)),
User: ptr.To(users[0]),
}
nodeUser2 := types.Node{
Hostname: "user2-device",
IPv4: ap("100.64.0.2"),
UserID: 2,
User: users[1],
UserID: ptr.To(uint(2)),
User: ptr.To(users[1]),
}
taggedClient := types.Node{
Hostname: "tagged-client",
IPv4: ap("100.64.0.4"),
UserID: 2,
User: users[1],
ForcedTags: []string{"tag:client"},
Hostname: "tagged-client",
IPv4: ap("100.64.0.4"),
UserID: ptr.To(uint(2)),
User: ptr.To(users[1]),
Tags: []string{"tag:client"},
}
tests := []struct {
@@ -1447,7 +1446,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/24"),
@@ -1475,7 +1474,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/24"),
@@ -1501,7 +1500,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/24"),
@@ -1529,7 +1528,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/24"),
@@ -1556,7 +1555,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/24"),
@@ -1581,7 +1580,7 @@ func TestReduceRoutes(t *testing.T) {
ID: 1,
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: types.User{Name: "user1"},
User: &types.User{Name: "user1"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/24"),
@@ -1614,7 +1613,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.2"), // Node IP
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.10.10.0/24"),
@@ -1646,7 +1645,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.10.10.0/24"),
@@ -1673,7 +1672,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.10.10.0/24"),
@@ -1701,7 +1700,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.2"),
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.10.10.0/24"),
@@ -1739,7 +1738,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.2"), // node with IP 100.64.0.2
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.10.10.0/24"),
@@ -1774,7 +1773,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 1,
IPv4: ap("100.64.0.1"), // router with IP 100.64.0.1
User: types.User{Name: "router"},
User: &types.User{Name: "router"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.10.10.0/24"),
@@ -1816,7 +1815,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.2"), // node
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.10.10.0/24"),
@@ -1850,7 +1849,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.64.0.2"), // node
User: types.User{Name: "node"},
User: &types.User{Name: "node"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("10.10.10.0/24"),
@@ -1887,7 +1886,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.123.45.89"), // Node B - regular node
User: types.User{Name: "node-b"},
User: &types.User{Name: "node-b"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("192.168.1.0/24"), // Subnet connected to Node A
@@ -1917,7 +1916,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 1,
IPv4: ap("100.123.45.67"), // Node A - router node
User: types.User{Name: "router"},
User: &types.User{Name: "router"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("192.168.1.0/24"), // Subnet connected to this router
@@ -1946,7 +1945,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.123.45.89"), // Node B - regular node that should be reachable
User: types.User{Name: "node-b"},
User: &types.User{Name: "node-b"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("192.168.1.0/24"), // Subnet behind router
@@ -1984,7 +1983,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 3,
IPv4: ap("100.123.45.99"), // Node C - isolated node
User: types.User{Name: "isolated-node"},
User: &types.User{Name: "isolated-node"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("192.168.1.0/24"), // Subnet behind router
@@ -2027,7 +2026,7 @@ func TestReduceRoutes(t *testing.T) {
node: &types.Node{
ID: 2,
IPv4: ap("100.123.45.89"), // Node B - regular node
User: types.User{Name: "node-b"},
User: &types.User{Name: "node-b"},
},
routes: []netip.Prefix{
netip.MustParsePrefix("192.168.1.0/14"), // Network 192.168.1.0/14 as mentioned in original issue

View File

@@ -16,6 +16,7 @@ import (
"gorm.io/gorm"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
"tailscale.com/util/must"
)
@@ -143,13 +144,13 @@ func TestReduceFilterRules(t *testing.T) {
node: &types.Node{
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"),
User: users[0],
User: ptr.To(users[0]),
},
peers: types.Nodes{
&types.Node{
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
User: users[0],
User: ptr.To(users[0]),
},
},
want: []tailcfg.FilterRule{},
@@ -190,7 +191,7 @@ func TestReduceFilterRules(t *testing.T) {
node: &types.Node{
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: users[1],
User: ptr.To(users[1]),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{
netip.MustParsePrefix("10.33.0.0/16"),
@@ -201,7 +202,7 @@ func TestReduceFilterRules(t *testing.T) {
&types.Node{
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: users[1],
User: ptr.To(users[1]),
},
},
want: []tailcfg.FilterRule{
@@ -282,19 +283,19 @@ func TestReduceFilterRules(t *testing.T) {
node: &types.Node{
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: users[1],
User: ptr.To(users[1]),
},
peers: types.Nodes{
&types.Node{
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: users[2],
User: ptr.To(users[2]),
},
// "internal" exit node
&types.Node{
IPv4: ap("100.64.0.100"),
IPv6: ap("fd7a:115c:a1e0::100"),
User: users[3],
User: ptr.To(users[3]),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: tsaddr.ExitRoutes(),
},
@@ -343,7 +344,7 @@ func TestReduceFilterRules(t *testing.T) {
node: &types.Node{
IPv4: ap("100.64.0.100"),
IPv6: ap("fd7a:115c:a1e0::100"),
User: users[3],
User: ptr.To(users[3]),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: tsaddr.ExitRoutes(),
},
@@ -352,12 +353,12 @@ func TestReduceFilterRules(t *testing.T) {
&types.Node{
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: users[2],
User: ptr.To(users[2]),
},
&types.Node{
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: users[1],
User: ptr.To(users[1]),
},
},
want: []tailcfg.FilterRule{
@@ -452,7 +453,7 @@ func TestReduceFilterRules(t *testing.T) {
node: &types.Node{
IPv4: ap("100.64.0.100"),
IPv6: ap("fd7a:115c:a1e0::100"),
User: users[3],
User: ptr.To(users[3]),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: tsaddr.ExitRoutes(),
},
@@ -461,12 +462,12 @@ func TestReduceFilterRules(t *testing.T) {
&types.Node{
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: users[2],
User: ptr.To(users[2]),
},
&types.Node{
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: users[1],
User: ptr.To(users[1]),
},
},
want: []tailcfg.FilterRule{
@@ -564,7 +565,7 @@ func TestReduceFilterRules(t *testing.T) {
node: &types.Node{
IPv4: ap("100.64.0.100"),
IPv6: ap("fd7a:115c:a1e0::100"),
User: users[3],
User: ptr.To(users[3]),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/16"), netip.MustParsePrefix("16.0.0.0/16")},
},
@@ -573,12 +574,12 @@ func TestReduceFilterRules(t *testing.T) {
&types.Node{
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: users[2],
User: ptr.To(users[2]),
},
&types.Node{
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: users[1],
User: ptr.To(users[1]),
},
},
want: []tailcfg.FilterRule{
@@ -654,7 +655,7 @@ func TestReduceFilterRules(t *testing.T) {
node: &types.Node{
IPv4: ap("100.64.0.100"),
IPv6: ap("fd7a:115c:a1e0::100"),
User: users[3],
User: ptr.To(users[3]),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/8"), netip.MustParsePrefix("16.0.0.0/8")},
},
@@ -663,12 +664,12 @@ func TestReduceFilterRules(t *testing.T) {
&types.Node{
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: users[2],
User: ptr.To(users[2]),
},
&types.Node{
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: users[1],
User: ptr.To(users[1]),
},
},
want: []tailcfg.FilterRule{
@@ -736,17 +737,17 @@ func TestReduceFilterRules(t *testing.T) {
node: &types.Node{
IPv4: ap("100.64.0.100"),
IPv6: ap("fd7a:115c:a1e0::100"),
User: users[3],
User: ptr.To(users[3]),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")},
},
ForcedTags: []string{"tag:access-servers"},
Tags: []string{"tag:access-servers"},
},
peers: types.Nodes{
&types.Node{
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: users[1],
User: ptr.To(users[1]),
},
},
want: []tailcfg.FilterRule{
@@ -803,13 +804,13 @@ func TestReduceFilterRules(t *testing.T) {
node: &types.Node{
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: users[3],
User: ptr.To(users[3]),
},
peers: types.Nodes{
&types.Node{
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: users[1],
User: ptr.To(users[1]),
Hostinfo: &tailcfg.Hostinfo{
RoutableIPs: []netip.Prefix{p("172.16.0.0/24"), p("10.10.11.0/24"), p("10.10.12.0/24")},
},

View File

@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"tailscale.com/types/ptr"
)
func TestNodeCanApproveRoute(t *testing.T) {
@@ -24,34 +25,34 @@ func TestNodeCanApproveRoute(t *testing.T) {
ID: 1,
Hostname: "user1-device",
IPv4: ap("100.64.0.1"),
UserID: 1,
User: users[0],
UserID: ptr.To(uint(1)),
User: ptr.To(users[0]),
}
exitNode := types.Node{
ID: 2,
Hostname: "user2-device",
IPv4: ap("100.64.0.2"),
UserID: 2,
User: users[1],
UserID: ptr.To(uint(2)),
User: ptr.To(users[1]),
}
taggedNode := types.Node{
ID: 3,
Hostname: "tagged-server",
IPv4: ap("100.64.0.3"),
UserID: 3,
User: users[2],
ForcedTags: []string{"tag:router"},
ID: 3,
Hostname: "tagged-server",
IPv4: ap("100.64.0.3"),
UserID: ptr.To(uint(3)),
User: ptr.To(users[2]),
Tags: []string{"tag:router"},
}
multiTagNode := types.Node{
ID: 4,
Hostname: "multi-tag-node",
IPv4: ap("100.64.0.4"),
UserID: 2,
User: users[1],
ForcedTags: []string{"tag:router", "tag:server"},
ID: 4,
Hostname: "multi-tag-node",
IPv4: ap("100.64.0.4"),
UserID: ptr.To(uint(2)),
User: ptr.To(users[1]),
Tags: []string{"tag:router", "tag:server"},
}
tests := []struct {

View File

@@ -168,7 +168,7 @@ func (pol *Policy) compileACLWithAutogroupSelf(
// Pre-filter to same-user untagged devices once - reuse for both sources and destinations
sameUserNodes := make([]types.NodeView, 0)
for _, n := range nodes.All() {
if n.User().ID == node.User().ID && !n.IsTagged() {
if n.User().ID() == node.User().ID() && !n.IsTagged() {
sameUserNodes = append(sameUserNodes, n)
}
}
@@ -349,7 +349,7 @@ func (pol *Policy) compileSSHPolicy(
// Build destination set for autogroup:self (same-user untagged devices only)
var dest netipx.IPSetBuilder
for _, n := range nodes.All() {
if n.User().ID == node.User().ID && !n.IsTagged() {
if n.User().ID() == node.User().ID() && !n.IsTagged() {
n.AppendToIPSet(&dest)
}
}
@@ -365,7 +365,7 @@ func (pol *Policy) compileSSHPolicy(
// Pre-filter to same-user untagged devices for efficiency
sameUserNodes := make([]types.NodeView, 0)
for _, n := range nodes.All() {
if n.User().ID == node.User().ID && !n.IsTagged() {
if n.User().ID() == node.User().ID() && !n.IsTagged() {
sameUserNodes = append(sameUserNodes, n)
}
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
)
// aliasWithPorts creates an AliasWithPorts structure from an alias and ports.
@@ -381,7 +382,7 @@ func TestParsing(t *testing.T) {
},
&types.Node{
IPv4: ap("200.200.200.200"),
User: users[0],
User: &users[0],
Hostinfo: &tailcfg.Hostinfo{},
},
}.ViewSlice())
@@ -409,14 +410,14 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
nodeUser1 := types.Node{
Hostname: "user1-device",
IPv4: createAddr("100.64.0.1"),
UserID: 1,
User: users[0],
UserID: ptr.To(users[0].ID),
User: ptr.To(users[0]),
}
nodeUser2 := types.Node{
Hostname: "user2-device",
IPv4: createAddr("100.64.0.2"),
UserID: 2,
User: users[1],
UserID: ptr.To(users[1].ID),
User: ptr.To(users[1]),
}
nodes := types.Nodes{&nodeUser1, &nodeUser2}
@@ -621,14 +622,14 @@ func TestCompileSSHPolicy_CheckAction(t *testing.T) {
nodeUser1 := types.Node{
Hostname: "user1-device",
IPv4: createAddr("100.64.0.1"),
UserID: 1,
User: users[0],
UserID: ptr.To(users[0].ID),
User: ptr.To(users[0]),
}
nodeUser2 := types.Node{
Hostname: "user2-device",
IPv4: createAddr("100.64.0.2"),
UserID: 2,
User: users[1],
UserID: ptr.To(users[1].ID),
User: ptr.To(users[1]),
}
nodes := types.Nodes{&nodeUser1, &nodeUser2}
@@ -682,15 +683,15 @@ func TestSSHIntegrationReproduction(t *testing.T) {
node1 := &types.Node{
Hostname: "user1-node",
IPv4: createAddr("100.64.0.1"),
UserID: 1,
User: users[0],
UserID: ptr.To(users[0].ID),
User: ptr.To(users[0]),
}
node2 := &types.Node{
Hostname: "user2-node",
IPv4: createAddr("100.64.0.2"),
UserID: 2,
User: users[1],
UserID: ptr.To(users[1].ID),
User: ptr.To(users[1]),
}
nodes := types.Nodes{node1, node2}
@@ -741,11 +742,12 @@ func TestSSHJSONSerialization(t *testing.T) {
{Name: "user1", Model: gorm.Model{ID: 1}},
}
uid := uint(1)
node := &types.Node{
Hostname: "test-node",
IPv4: createAddr("100.64.0.1"),
UserID: 1,
User: users[0],
UserID: &uid,
User: &users[0],
}
nodes := types.Nodes{node}
@@ -804,32 +806,32 @@ func TestCompileFilterRulesForNodeWithAutogroupSelf(t *testing.T) {
nodes := types.Nodes{
{
User: users[0],
User: ptr.To(users[0]),
IPv4: ap("100.64.0.1"),
},
{
User: users[0],
User: ptr.To(users[0]),
IPv4: ap("100.64.0.2"),
},
{
User: users[1],
User: ptr.To(users[1]),
IPv4: ap("100.64.0.3"),
},
{
User: users[1],
User: ptr.To(users[1]),
IPv4: ap("100.64.0.4"),
},
// Tagged device for user1
{
User: users[0],
IPv4: ap("100.64.0.5"),
ForcedTags: []string{"tag:test"},
User: &users[0],
IPv4: ap("100.64.0.5"),
Tags: []string{"tag:test"},
},
// Tagged device for user2
{
User: users[1],
IPv4: ap("100.64.0.6"),
ForcedTags: []string{"tag:test"},
User: &users[1],
IPv4: ap("100.64.0.6"),
Tags: []string{"tag:test"},
},
}
@@ -925,6 +927,251 @@ func TestCompileFilterRulesForNodeWithAutogroupSelf(t *testing.T) {
}
}
// TestTagUserMutualExclusivity tests that user-owned nodes and tagged nodes
// are treated as separate identity classes and cannot inadvertently access each other.
func TestTagUserMutualExclusivity(t *testing.T) {
users := types.Users{
{Model: gorm.Model{ID: 1}, Name: "user1"},
{Model: gorm.Model{ID: 2}, Name: "user2"},
}
nodes := types.Nodes{
// User-owned nodes
{
User: ptr.To(users[0]),
IPv4: ap("100.64.0.1"),
},
{
User: ptr.To(users[1]),
IPv4: ap("100.64.0.2"),
},
// Tagged nodes
{
User: &users[0], // "created by" tracking
IPv4: ap("100.64.0.10"),
Tags: []string{"tag:server"},
},
{
User: &users[1], // "created by" tracking
IPv4: ap("100.64.0.11"),
Tags: []string{"tag:database"},
},
}
policy := &Policy{
TagOwners: TagOwners{
Tag("tag:server"): Owners{ptr.To(Username("user1@"))},
Tag("tag:database"): Owners{ptr.To(Username("user2@"))},
},
ACLs: []ACL{
// Rule 1: user1 (user-owned) should NOT be able to reach tagged nodes
{
Action: "accept",
Sources: []Alias{up("user1@")},
Destinations: []AliasWithPorts{
aliasWithPorts(tp("tag:server"), tailcfg.PortRangeAny),
},
},
// Rule 2: tag:server should be able to reach tag:database
{
Action: "accept",
Sources: []Alias{tp("tag:server")},
Destinations: []AliasWithPorts{
aliasWithPorts(tp("tag:database"), tailcfg.PortRangeAny),
},
},
},
}
err := policy.validate()
if err != nil {
t.Fatalf("policy validation failed: %v", err)
}
// Test user1's user-owned node (100.64.0.1)
userNode := nodes[0].View()
userRules, err := policy.compileFilterRulesForNode(users, userNode, nodes.ViewSlice())
if err != nil {
t.Fatalf("unexpected error for user node: %v", err)
}
// User1's user-owned node should NOT reach tag:server (100.64.0.10)
// because user1@ as a source only matches user1's user-owned devices, NOT tagged devices
for _, rule := range userRules {
for _, dst := range rule.DstPorts {
if dst.IP == "100.64.0.10" {
t.Errorf("SECURITY: user-owned node should NOT reach tagged node (got dest %s in rule)", dst.IP)
}
}
}
// Test tag:server node (100.64.0.10)
// compileFilterRulesForNode returns rules for what the node can ACCESS (as source)
taggedNode := nodes[2].View()
taggedRules, err := policy.compileFilterRulesForNode(users, taggedNode, nodes.ViewSlice())
if err != nil {
t.Fatalf("unexpected error for tagged node: %v", err)
}
// Tag:server (as source) should be able to reach tag:database (100.64.0.11)
// Check destinations in the rules for this node
foundDatabaseDest := false
for _, rule := range taggedRules {
// Check if this rule applies to tag:server as source
if !slices.Contains(rule.SrcIPs, "100.64.0.10/32") {
continue
}
// Check if tag:database is in destinations
for _, dst := range rule.DstPorts {
if dst.IP == "100.64.0.11/32" {
foundDatabaseDest = true
break
}
}
if foundDatabaseDest {
break
}
}
if !foundDatabaseDest {
t.Errorf("tag:server should reach tag:database but didn't find 100.64.0.11 in destinations")
}
}
// TestAutogroupTagged tests that autogroup:tagged correctly selects all devices
// with tag-based identity (IsTagged() == true or has requested tags in tagOwners).
func TestAutogroupTagged(t *testing.T) {
t.Parallel()
users := types.Users{
{Model: gorm.Model{ID: 1}, Name: "user1"},
{Model: gorm.Model{ID: 2}, Name: "user2"},
}
nodes := types.Nodes{
// User-owned nodes (not tagged)
{
User: ptr.To(users[0]),
IPv4: ap("100.64.0.1"),
},
{
User: ptr.To(users[1]),
IPv4: ap("100.64.0.2"),
},
// Tagged nodes
{
User: &users[0], // "created by" tracking
IPv4: ap("100.64.0.10"),
Tags: []string{"tag:server"},
},
{
User: &users[1], // "created by" tracking
IPv4: ap("100.64.0.11"),
Tags: []string{"tag:database"},
},
{
User: &users[0],
IPv4: ap("100.64.0.12"),
Tags: []string{"tag:web", "tag:prod"},
},
}
policy := &Policy{
TagOwners: TagOwners{
Tag("tag:server"): Owners{ptr.To(Username("user1@"))},
Tag("tag:database"): Owners{ptr.To(Username("user2@"))},
Tag("tag:web"): Owners{ptr.To(Username("user1@"))},
Tag("tag:prod"): Owners{ptr.To(Username("user1@"))},
},
ACLs: []ACL{
// Rule: autogroup:tagged can reach user-owned nodes
{
Action: "accept",
Sources: []Alias{agp("autogroup:tagged")},
Destinations: []AliasWithPorts{
aliasWithPorts(up("user1@"), tailcfg.PortRangeAny),
aliasWithPorts(up("user2@"), tailcfg.PortRangeAny),
},
},
},
}
err := policy.validate()
require.NoError(t, err)
// Verify autogroup:tagged includes all tagged nodes
taggedIPs, err := AutoGroupTagged.Resolve(policy, users, nodes.ViewSlice())
require.NoError(t, err)
require.NotNil(t, taggedIPs)
// Should contain all tagged nodes
assert.True(t, taggedIPs.Contains(*ap("100.64.0.10")), "should include tag:server")
assert.True(t, taggedIPs.Contains(*ap("100.64.0.11")), "should include tag:database")
assert.True(t, taggedIPs.Contains(*ap("100.64.0.12")), "should include tag:web,tag:prod")
// Should NOT contain user-owned nodes
assert.False(t, taggedIPs.Contains(*ap("100.64.0.1")), "should not include user1 node")
assert.False(t, taggedIPs.Contains(*ap("100.64.0.2")), "should not include user2 node")
// Test ACL filtering: all tagged nodes should be able to reach user nodes
tests := []struct {
name string
sourceNode types.NodeView
shouldReach []string // IP strings for comparison
}{
{
name: "tag:server can reach user-owned nodes",
sourceNode: nodes[2].View(),
shouldReach: []string{"100.64.0.1", "100.64.0.2"},
},
{
name: "tag:database can reach user-owned nodes",
sourceNode: nodes[3].View(),
shouldReach: []string{"100.64.0.1", "100.64.0.2"},
},
{
name: "tag:web,tag:prod can reach user-owned nodes",
sourceNode: nodes[4].View(),
shouldReach: []string{"100.64.0.1", "100.64.0.2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
rules, err := policy.compileFilterRulesForNode(users, tt.sourceNode, nodes.ViewSlice())
require.NoError(t, err)
// Verify all expected destinations are reachable
for _, expectedDest := range tt.shouldReach {
found := false
for _, rule := range rules {
for _, dstPort := range rule.DstPorts {
// DstPort.IP is CIDR notation like "100.64.0.1/32"
if strings.HasPrefix(dstPort.IP, expectedDest+"/") || dstPort.IP == expectedDest {
found = true
break
}
}
if found {
break
}
}
assert.True(t, found, "Expected to find destination %s in rules", expectedDest)
}
})
}
}
func TestAutogroupSelfInSourceIsRejected(t *testing.T) {
// Test that autogroup:self cannot be used in sources (per Tailscale spec)
policy := &Policy{
@@ -959,10 +1206,10 @@ func TestAutogroupSelfWithSpecificUserSource(t *testing.T) {
}
nodes := types.Nodes{
{User: users[0], IPv4: ap("100.64.0.1")},
{User: users[0], IPv4: ap("100.64.0.2")},
{User: users[1], IPv4: ap("100.64.0.3")},
{User: users[1], IPv4: ap("100.64.0.4")},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.1")},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.2")},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.3")},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.4")},
}
policy := &Policy{
@@ -1026,11 +1273,11 @@ func TestAutogroupSelfWithGroupSource(t *testing.T) {
}
nodes := types.Nodes{
{User: users[0], IPv4: ap("100.64.0.1")},
{User: users[0], IPv4: ap("100.64.0.2")},
{User: users[1], IPv4: ap("100.64.0.3")},
{User: users[1], IPv4: ap("100.64.0.4")},
{User: users[2], IPv4: ap("100.64.0.5")},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.1")},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.2")},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.3")},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.4")},
{User: ptr.To(users[2]), IPv4: ap("100.64.0.5")},
}
policy := &Policy{
@@ -1095,13 +1342,13 @@ func TestSSHWithAutogroupSelfInDestination(t *testing.T) {
nodes := types.Nodes{
// User1's nodes
{User: users[0], IPv4: ap("100.64.0.1"), Hostname: "user1-node1"},
{User: users[0], IPv4: ap("100.64.0.2"), Hostname: "user1-node2"},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.1"), Hostname: "user1-node1"},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.2"), Hostname: "user1-node2"},
// User2's nodes
{User: users[1], IPv4: ap("100.64.0.3"), Hostname: "user2-node1"},
{User: users[1], IPv4: ap("100.64.0.4"), Hostname: "user2-node2"},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.3"), Hostname: "user2-node1"},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.4"), Hostname: "user2-node2"},
// Tagged node for user1 (should be excluded)
{User: users[0], IPv4: ap("100.64.0.5"), Hostname: "user1-tagged", ForcedTags: []string{"tag:server"}},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.5"), Hostname: "user1-tagged", Tags: []string{"tag:server"}},
}
policy := &Policy{
@@ -1173,10 +1420,10 @@ func TestSSHWithAutogroupSelfAndSpecificUser(t *testing.T) {
}
nodes := types.Nodes{
{User: users[0], IPv4: ap("100.64.0.1")},
{User: users[0], IPv4: ap("100.64.0.2")},
{User: users[1], IPv4: ap("100.64.0.3")},
{User: users[1], IPv4: ap("100.64.0.4")},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.1")},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.2")},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.3")},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.4")},
}
policy := &Policy{
@@ -1227,11 +1474,11 @@ func TestSSHWithAutogroupSelfAndGroup(t *testing.T) {
}
nodes := types.Nodes{
{User: users[0], IPv4: ap("100.64.0.1")},
{User: users[0], IPv4: ap("100.64.0.2")},
{User: users[1], IPv4: ap("100.64.0.3")},
{User: users[1], IPv4: ap("100.64.0.4")},
{User: users[2], IPv4: ap("100.64.0.5")},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.1")},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.2")},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.3")},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.4")},
{User: ptr.To(users[2]), IPv4: ap("100.64.0.5")},
}
policy := &Policy{
@@ -1284,10 +1531,10 @@ func TestSSHWithAutogroupSelfExcludesTaggedDevices(t *testing.T) {
}
nodes := types.Nodes{
{User: users[0], IPv4: ap("100.64.0.1"), Hostname: "untagged1"},
{User: users[0], IPv4: ap("100.64.0.2"), Hostname: "untagged2"},
{User: users[0], IPv4: ap("100.64.0.3"), Hostname: "tagged1", ForcedTags: []string{"tag:server"}},
{User: users[0], IPv4: ap("100.64.0.4"), Hostname: "tagged2", ForcedTags: []string{"tag:web"}},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.1"), Hostname: "untagged1"},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.2"), Hostname: "untagged2"},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.3"), Hostname: "tagged1", Tags: []string{"tag:server"}},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.4"), Hostname: "tagged2", Tags: []string{"tag:web"}},
}
policy := &Policy{
@@ -1344,10 +1591,10 @@ func TestSSHWithAutogroupSelfAndMixedDestinations(t *testing.T) {
}
nodes := types.Nodes{
{User: users[0], IPv4: ap("100.64.0.1"), Hostname: "user1-device"},
{User: users[0], IPv4: ap("100.64.0.2"), Hostname: "user1-device2"},
{User: users[1], IPv4: ap("100.64.0.3"), Hostname: "user2-device"},
{User: users[1], IPv4: ap("100.64.0.4"), Hostname: "user2-router", ForcedTags: []string{"tag:router"}},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.1"), Hostname: "user1-device"},
{User: ptr.To(users[0]), IPv4: ap("100.64.0.2"), Hostname: "user1-device2"},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.3"), Hostname: "user2-device"},
{User: ptr.To(users[1]), IPv4: ap("100.64.0.4"), Hostname: "user2-router", Tags: []string{"tag:router"}},
}
policy := &Policy{

View File

@@ -697,14 +697,14 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S
// Check for removed nodes
for nodeID, oldNode := range oldNodeMap {
if _, exists := newNodeMap[nodeID]; !exists {
affectedUsers[oldNode.User().ID] = struct{}{}
affectedUsers[oldNode.User().ID()] = struct{}{}
}
}
// Check for added nodes
for nodeID, newNode := range newNodeMap {
if _, exists := oldNodeMap[nodeID]; !exists {
affectedUsers[newNode.User().ID] = struct{}{}
affectedUsers[newNode.User().ID()] = struct{}{}
}
}
@@ -712,26 +712,26 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S
for nodeID, newNode := range newNodeMap {
if oldNode, exists := oldNodeMap[nodeID]; exists {
// Check if user changed
if oldNode.User().ID != newNode.User().ID {
affectedUsers[oldNode.User().ID] = struct{}{}
affectedUsers[newNode.User().ID] = struct{}{}
if oldNode.User().ID() != newNode.User().ID() {
affectedUsers[oldNode.User().ID()] = struct{}{}
affectedUsers[newNode.User().ID()] = struct{}{}
}
// Check if tag status changed
if oldNode.IsTagged() != newNode.IsTagged() {
affectedUsers[newNode.User().ID] = struct{}{}
affectedUsers[newNode.User().ID()] = struct{}{}
}
// Check if IPs changed (simple check - could be more sophisticated)
oldIPs := oldNode.IPs()
newIPs := newNode.IPs()
if len(oldIPs) != len(newIPs) {
affectedUsers[newNode.User().ID] = struct{}{}
affectedUsers[newNode.User().ID()] = struct{}{}
} else {
// Check if any IPs are different
for i, oldIP := range oldIPs {
if i >= len(newIPs) || oldIP != newIPs[i] {
affectedUsers[newNode.User().ID] = struct{}{}
affectedUsers[newNode.User().ID()] = struct{}{}
break
}
}
@@ -750,7 +750,7 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S
// Check in new nodes first
for _, node := range newNodes.All() {
if node.ID() == nodeID {
nodeUserID = node.User().ID
nodeUserID = node.User().ID()
found = true
break
}
@@ -760,7 +760,7 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S
if !found {
for _, node := range oldNodes.All() {
if node.ID() == nodeID {
nodeUserID = node.User().ID
nodeUserID = node.User().ID()
found = true
break
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
)
func node(name, ipv4, ipv6 string, user types.User, hostinfo *tailcfg.Hostinfo) *types.Node {
@@ -19,8 +20,8 @@ func node(name, ipv4, ipv6 string, user types.User, hostinfo *tailcfg.Hostinfo)
Hostname: name,
IPv4: ap(ipv4),
IPv6: ap(ipv6),
User: user,
UserID: user.ID,
User: ptr.To(user),
UserID: ptr.To(user.ID),
Hostinfo: hostinfo,
}
}
@@ -456,8 +457,8 @@ func TestAutogroupSelfWithOtherRules(t *testing.T) {
Hostname: "test-1-device",
IPv4: ap("100.64.0.1"),
IPv6: ap("fd7a:115c:a1e0::1"),
User: users[0],
UserID: users[0].ID,
User: ptr.To(users[0]),
UserID: ptr.To(users[0].ID),
Hostinfo: &tailcfg.Hostinfo{},
}
@@ -467,9 +468,9 @@ func TestAutogroupSelfWithOtherRules(t *testing.T) {
Hostname: "test-2-router",
IPv4: ap("100.64.0.2"),
IPv6: ap("fd7a:115c:a1e0::2"),
User: users[1],
UserID: users[1].ID,
ForcedTags: []string{"tag:node-router"},
User: ptr.To(users[1]),
UserID: ptr.To(users[1].ID),
Tags: []string{"tag:node-router"},
Hostinfo: &tailcfg.Hostinfo{},
}

View File

@@ -206,7 +206,12 @@ func (u Username) Resolve(_ *Policy, users types.Users, nodes views.Slice[types.
continue
}
if node.User().ID == user.ID {
// Skip nodes without a user (defensive check for tests)
if !node.User().Valid() {
continue
}
if node.User().ID() == user.ID {
node.AppendToIPSet(&ips)
}
}
@@ -311,8 +316,8 @@ func (t Tag) Resolve(p *Policy, users types.Users, nodes views.Slice[types.NodeV
}
for _, node := range nodes.All() {
// Check if node has this tag in all tags (ForcedTags + AuthKey.Tags)
if slices.Contains(node.Tags(), string(t)) {
// Check if node has this tag
if node.HasTag(string(t)) {
node.AppendToIPSet(&ips)
}

View File

@@ -1549,7 +1549,17 @@ func TestResolvePolicy(t *testing.T) {
"groupuser1": {Model: gorm.Model{ID: 3}, Name: "groupuser1"},
"groupuser2": {Model: gorm.Model{ID: 4}, Name: "groupuser2"},
"notme": {Model: gorm.Model{ID: 5}, Name: "notme"},
"testuser2": {Model: gorm.Model{ID: 6}, Name: "testuser2"},
}
// Extract users to variables so we can take their addresses
testuser := users["testuser"]
groupuser := users["groupuser"]
groupuser1 := users["groupuser1"]
groupuser2 := users["groupuser2"]
notme := users["notme"]
testuser2 := users["testuser2"]
tests := []struct {
name string
nodes types.Nodes
@@ -1579,29 +1589,27 @@ func TestResolvePolicy(t *testing.T) {
nodes: types.Nodes{
// Not matching other user
{
User: users["notme"],
User: ptr.To(notme),
IPv4: ap("100.100.101.1"),
},
// Not matching forced tags
{
User: users["testuser"],
ForcedTags: []string{"tag:anything"},
User: ptr.To(testuser),
Tags: []string{"tag:anything"},
IPv4: ap("100.100.101.2"),
},
// not matching pak tag
// not matching because it's tagged (tags copied from AuthKey)
{
User: users["testuser"],
AuthKey: &types.PreAuthKey{
Tags: []string{"alsotagged"},
},
User: ptr.To(testuser),
Tags: []string{"alsotagged"},
IPv4: ap("100.100.101.3"),
},
{
User: users["testuser"],
User: ptr.To(testuser),
IPv4: ap("100.100.101.103"),
},
{
User: users["testuser"],
User: ptr.To(testuser),
IPv4: ap("100.100.101.104"),
},
},
@@ -1613,29 +1621,27 @@ func TestResolvePolicy(t *testing.T) {
nodes: types.Nodes{
// Not matching other user
{
User: users["notme"],
User: ptr.To(notme),
IPv4: ap("100.100.101.4"),
},
// Not matching forced tags
{
User: users["groupuser"],
ForcedTags: []string{"tag:anything"},
User: ptr.To(groupuser),
Tags: []string{"tag:anything"},
IPv4: ap("100.100.101.5"),
},
// not matching pak tag
// not matching because it's tagged (tags copied from AuthKey)
{
User: users["groupuser"],
AuthKey: &types.PreAuthKey{
Tags: []string{"tag:alsotagged"},
},
User: ptr.To(groupuser),
Tags: []string{"tag:alsotagged"},
IPv4: ap("100.100.101.6"),
},
{
User: users["groupuser"],
User: ptr.To(groupuser),
IPv4: ap("100.100.101.203"),
},
{
User: users["groupuser"],
User: ptr.To(groupuser),
IPv4: ap("100.100.101.204"),
},
},
@@ -1653,12 +1659,12 @@ func TestResolvePolicy(t *testing.T) {
nodes: types.Nodes{
// Not matching other user
{
User: users["notme"],
User: ptr.To(notme),
IPv4: ap("100.100.101.9"),
},
// Not matching forced tags
{
ForcedTags: []string{"tag:anything"},
Tags: []string{"tag:anything"},
IPv4: ap("100.100.101.10"),
},
// not matching pak tag
@@ -1670,14 +1676,12 @@ func TestResolvePolicy(t *testing.T) {
},
// Not matching forced tags
{
ForcedTags: []string{"tag:test"},
Tags: []string{"tag:test"},
IPv4: ap("100.100.101.234"),
},
// not matching pak tag
// matching tag (tags copied from AuthKey during registration)
{
AuthKey: &types.PreAuthKey{
Tags: []string{"tag:test"},
},
Tags: []string{"tag:test"},
IPv4: ap("100.100.101.239"),
},
},
@@ -1706,11 +1710,11 @@ func TestResolvePolicy(t *testing.T) {
toResolve: ptr.To(Group("group:testgroup")),
nodes: types.Nodes{
{
User: users["groupuser1"],
User: ptr.To(groupuser1),
IPv4: ap("100.100.101.203"),
},
{
User: users["groupuser2"],
User: ptr.To(groupuser2),
IPv4: ap("100.100.101.204"),
},
},
@@ -1731,7 +1735,7 @@ func TestResolvePolicy(t *testing.T) {
toResolve: ptr.To(Username("invaliduser@")),
nodes: types.Nodes{
{
User: users["testuser"],
User: ptr.To(testuser),
IPv4: ap("100.100.101.103"),
},
},
@@ -1742,7 +1746,7 @@ func TestResolvePolicy(t *testing.T) {
toResolve: tp("tag:invalid"),
nodes: types.Nodes{
{
ForcedTags: []string{"tag:test"},
Tags: []string{"tag:test"},
IPv4: ap("100.100.101.234"),
},
},
@@ -1763,18 +1767,18 @@ func TestResolvePolicy(t *testing.T) {
nodes: types.Nodes{
// Node with no tags (should be included)
{
User: users["testuser"],
User: ptr.To(testuser),
IPv4: ap("100.100.101.1"),
},
// Node with forced tags (should be excluded)
{
User: users["testuser"],
ForcedTags: []string{"tag:test"},
User: ptr.To(testuser),
Tags: []string{"tag:test"},
IPv4: ap("100.100.101.2"),
},
// Node with allowed requested tag (should be excluded)
{
User: users["testuser"],
User: ptr.To(testuser),
Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:test"},
},
@@ -1782,7 +1786,7 @@ func TestResolvePolicy(t *testing.T) {
},
// Node with non-allowed requested tag (should be included)
{
User: users["testuser"],
User: ptr.To(testuser),
Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:notallowed"},
},
@@ -1790,7 +1794,7 @@ func TestResolvePolicy(t *testing.T) {
},
// Node with multiple requested tags, one allowed (should be excluded)
{
User: users["testuser"],
User: ptr.To(testuser),
Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:test", "tag:notallowed"},
},
@@ -1798,7 +1802,7 @@ func TestResolvePolicy(t *testing.T) {
},
// Node with multiple requested tags, none allowed (should be included)
{
User: users["testuser"],
User: ptr.To(testuser),
Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:notallowed1", "tag:notallowed2"},
},
@@ -1822,18 +1826,18 @@ func TestResolvePolicy(t *testing.T) {
nodes: types.Nodes{
// Node with no tags (should be excluded)
{
User: users["testuser"],
User: ptr.To(testuser),
IPv4: ap("100.100.101.1"),
},
// Node with forced tag (should be included)
{
User: users["testuser"],
ForcedTags: []string{"tag:test"},
User: ptr.To(testuser),
Tags: []string{"tag:test"},
IPv4: ap("100.100.101.2"),
},
// Node with allowed requested tag (should be included)
{
User: users["testuser"],
User: ptr.To(testuser),
Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:test"},
},
@@ -1841,7 +1845,7 @@ func TestResolvePolicy(t *testing.T) {
},
// Node with non-allowed requested tag (should be excluded)
{
User: users["testuser"],
User: ptr.To(testuser),
Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:notallowed"},
},
@@ -1849,7 +1853,7 @@ func TestResolvePolicy(t *testing.T) {
},
// Node with multiple requested tags, one allowed (should be included)
{
User: users["testuser"],
User: ptr.To(testuser),
Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:test", "tag:notallowed"},
},
@@ -1857,7 +1861,7 @@ func TestResolvePolicy(t *testing.T) {
},
// Node with multiple requested tags, none allowed (should be excluded)
{
User: users["testuser"],
User: ptr.To(testuser),
Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:notallowed1", "tag:notallowed2"},
},
@@ -1865,8 +1869,8 @@ func TestResolvePolicy(t *testing.T) {
},
// Node with multiple forced tags (should be included)
{
User: users["testuser"],
ForcedTags: []string{"tag:test", "tag:other"},
User: ptr.To(testuser),
Tags: []string{"tag:test", "tag:other"},
IPv4: ap("100.100.101.7"),
},
},
@@ -1886,20 +1890,20 @@ func TestResolvePolicy(t *testing.T) {
toResolve: ptr.To(AutoGroupSelf),
nodes: types.Nodes{
{
User: users["testuser"],
User: ptr.To(testuser),
IPv4: ap("100.100.101.1"),
},
{
User: users["testuser2"],
User: ptr.To(testuser2),
IPv4: ap("100.100.101.2"),
},
{
User: users["testuser"],
ForcedTags: []string{"tag:test"},
User: ptr.To(testuser),
Tags: []string{"tag:test"},
IPv4: ap("100.100.101.3"),
},
{
User: users["testuser2"],
User: ptr.To(testuser2),
Hostinfo: &tailcfg.Hostinfo{
RequestTags: []string{"tag:test"},
},
@@ -1961,23 +1965,23 @@ func TestResolveAutoApprovers(t *testing.T) {
nodes := types.Nodes{
{
IPv4: ap("100.64.0.1"),
User: users[0],
User: &users[0],
},
{
IPv4: ap("100.64.0.2"),
User: users[1],
User: &users[1],
},
{
IPv4: ap("100.64.0.3"),
User: users[2],
User: &users[2],
},
{
IPv4: ap("100.64.0.4"),
ForcedTags: []string{"tag:testtag"},
Tags: []string{"tag:testtag"},
},
{
IPv4: ap("100.64.0.5"),
ForcedTags: []string{"tag:exittest"},
Tags: []string{"tag:exittest"},
},
}
@@ -2280,15 +2284,15 @@ func TestNodeCanApproveRoute(t *testing.T) {
nodes := types.Nodes{
{
IPv4: ap("100.64.0.1"),
User: users[0],
User: &users[0],
},
{
IPv4: ap("100.64.0.2"),
User: users[1],
User: &users[1],
},
{
IPv4: ap("100.64.0.3"),
User: users[2],
User: &users[2],
},
}
@@ -2413,15 +2417,15 @@ func TestResolveTagOwners(t *testing.T) {
nodes := types.Nodes{
{
IPv4: ap("100.64.0.1"),
User: users[0],
User: &users[0],
},
{
IPv4: ap("100.64.0.2"),
User: users[1],
User: &users[1],
},
{
IPv4: ap("100.64.0.3"),
User: users[2],
User: &users[2],
},
}
@@ -2498,15 +2502,15 @@ func TestNodeCanHaveTag(t *testing.T) {
nodes := types.Nodes{
{
IPv4: ap("100.64.0.1"),
User: users[0],
User: &users[0],
},
{
IPv4: ap("100.64.0.2"),
User: users[1],
User: &users[1],
},
{
IPv4: ap("100.64.0.3"),
User: users[2],
User: &users[2],
},
}
@@ -2580,6 +2584,49 @@ func TestNodeCanHaveTag(t *testing.T) {
tag: "tag:test",
want: false,
},
{
name: "node-with-unauthorized-tag-different-user",
policy: &Policy{
TagOwners: TagOwners{
Tag("tag:prod"): Owners{ptr.To(Username("user1@"))},
},
},
node: nodes[2], // user3's node
tag: "tag:prod",
want: false,
},
{
name: "node-with-multiple-tags-one-unauthorized",
policy: &Policy{
TagOwners: TagOwners{
Tag("tag:web"): Owners{ptr.To(Username("user1@"))},
Tag("tag:database"): Owners{ptr.To(Username("user2@"))},
},
},
node: nodes[0], // user1's node
tag: "tag:database",
want: false, // user1 cannot have tag:database (owned by user2)
},
{
name: "empty-tagowners-map",
policy: &Policy{
TagOwners: TagOwners{},
},
node: nodes[0],
tag: "tag:test",
want: false, // No one can have tags if tagOwners is empty
},
{
name: "tag-not-in-tagowners",
policy: &Policy{
TagOwners: TagOwners{
Tag("tag:prod"): Owners{ptr.To(Username("user1@"))},
},
},
node: nodes[0],
tag: "tag:dev", // This tag is not defined in tagOwners
want: false,
},
}
for _, tt := range tests {