compat tests: switch to anonymized norse-god + pokemon names

tscap now anonymizes captures at write time (see kradalby/tscap
package anonymize): SaaS user identifiers become odin/thor/freya and
node hostnames become original-151 pokemon names. Update the four
policy v2 compat test setups to construct types.User and types.Node
objects with the new names so the in-test topology matches what the
captured testdata files contain.

The convertPolicyUserEmails / convertSaaSEmail helpers become
passthroughs because tscap already produces the final email form.

Updates #3157
Updates #3169
This commit is contained in:
Kristoffer Dalby
2026-04-08 08:21:31 +00:00
parent e30de58627
commit 76c7cb3342
4 changed files with 110 additions and 140 deletions
@@ -39,55 +39,56 @@ func ptrAddr(s string) *netip.Addr {
} }
// setupACLCompatUsers returns the 3 test users for ACL compatibility tests. // setupACLCompatUsers returns the 3 test users for ACL compatibility tests.
// Email addresses use @example.com domain, matching the converted Tailscale // Names and emails match the anonymized identifiers tscap writes into the
// policy format (Tailscale uses @passkey and @dalby.cc). // capture files (see github.com/kradalby/tscap/anonymize): users get
// norse-god names and nodes get original-151 pokémon names.
func setupACLCompatUsers() types.Users { func setupACLCompatUsers() types.Users {
return types.Users{ return types.Users{
{Model: gorm.Model{ID: 1}, Name: "kratail2tid", Email: "kratail2tid@example.com"}, {Model: gorm.Model{ID: 1}, Name: "odin", Email: "odin@example.com"},
{Model: gorm.Model{ID: 2}, Name: "kristoffer", Email: "kristoffer@example.com"}, {Model: gorm.Model{ID: 2}, Name: "thor", Email: "thor@example.com"},
{Model: gorm.Model{ID: 3}, Name: "monitorpasskeykradalby", Email: "monitorpasskeykradalby@example.com"}, {Model: gorm.Model{ID: 3}, Name: "freya", Email: "freya@example.com"},
} }
} }
// setupACLCompatNodes returns the 8 test nodes for ACL compatibility tests. // setupACLCompatNodes returns the 8 test nodes for ACL compatibility tests.
// Uses the same topology as the grants compat tests. // Node GivenNames match tscap's anonymized pokémon naming.
func setupACLCompatNodes(users types.Users) types.Nodes { func setupACLCompatNodes(users types.Users) types.Nodes {
return types.Nodes{ return types.Nodes{
{ {
ID: 1, GivenName: "user1", ID: 1, GivenName: "bulbasaur",
User: &users[0], UserID: &users[0].ID, User: &users[0], UserID: &users[0].ID,
IPv4: ptrAddr("100.90.199.68"), IPv6: ptrAddr("fd7a:115c:a1e0::2d01:c747"), IPv4: ptrAddr("100.90.199.68"), IPv6: ptrAddr("fd7a:115c:a1e0::2d01:c747"),
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
}, },
{ {
ID: 2, GivenName: "user-kris", ID: 2, GivenName: "ivysaur",
User: &users[1], UserID: &users[1].ID, User: &users[1], UserID: &users[1].ID,
IPv4: ptrAddr("100.110.121.96"), IPv6: ptrAddr("fd7a:115c:a1e0::1737:7960"), IPv4: ptrAddr("100.110.121.96"), IPv6: ptrAddr("fd7a:115c:a1e0::1737:7960"),
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
}, },
{ {
ID: 3, GivenName: "user-mon", ID: 3, GivenName: "venusaur",
User: &users[2], UserID: &users[2].ID, User: &users[2], UserID: &users[2].ID,
IPv4: ptrAddr("100.103.90.82"), IPv6: ptrAddr("fd7a:115c:a1e0::9e37:5a52"), IPv4: ptrAddr("100.103.90.82"), IPv6: ptrAddr("fd7a:115c:a1e0::9e37:5a52"),
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
}, },
{ {
ID: 4, GivenName: "tagged-server", ID: 4, GivenName: "beedrill",
IPv4: ptrAddr("100.108.74.26"), IPv6: ptrAddr("fd7a:115c:a1e0::b901:4a87"), IPv4: ptrAddr("100.108.74.26"), IPv6: ptrAddr("fd7a:115c:a1e0::b901:4a87"),
Tags: []string{"tag:server"}, Hostinfo: &tailcfg.Hostinfo{}, Tags: []string{"tag:server"}, Hostinfo: &tailcfg.Hostinfo{},
}, },
{ {
ID: 5, GivenName: "tagged-prod", ID: 5, GivenName: "kakuna",
IPv4: ptrAddr("100.103.8.15"), IPv6: ptrAddr("fd7a:115c:a1e0::5b37:80f"), IPv4: ptrAddr("100.103.8.15"), IPv6: ptrAddr("fd7a:115c:a1e0::5b37:80f"),
Tags: []string{"tag:prod"}, Hostinfo: &tailcfg.Hostinfo{}, Tags: []string{"tag:prod"}, Hostinfo: &tailcfg.Hostinfo{},
}, },
{ {
ID: 6, GivenName: "tagged-client", ID: 6, GivenName: "weedle",
IPv4: ptrAddr("100.83.200.69"), IPv6: ptrAddr("fd7a:115c:a1e0::c537:c845"), IPv4: ptrAddr("100.83.200.69"), IPv6: ptrAddr("fd7a:115c:a1e0::c537:c845"),
Tags: []string{"tag:client"}, Hostinfo: &tailcfg.Hostinfo{}, Tags: []string{"tag:client"}, Hostinfo: &tailcfg.Hostinfo{},
}, },
{ {
ID: 7, GivenName: "subnet-router", ID: 7, GivenName: "squirtle",
IPv4: ptrAddr("100.92.142.61"), IPv6: ptrAddr("fd7a:115c:a1e0::3e37:8e3d"), IPv4: ptrAddr("100.92.142.61"), IPv6: ptrAddr("fd7a:115c:a1e0::3e37:8e3d"),
Tags: []string{"tag:router"}, Tags: []string{"tag:router"},
Hostinfo: &tailcfg.Hostinfo{ Hostinfo: &tailcfg.Hostinfo{
@@ -96,7 +97,7 @@ func setupACLCompatNodes(users types.Users) types.Nodes {
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
}, },
{ {
ID: 8, GivenName: "exit-node", ID: 8, GivenName: "charmander",
IPv4: ptrAddr("100.85.66.106"), IPv6: ptrAddr("fd7a:115c:a1e0::7c37:426a"), IPv4: ptrAddr("100.85.66.106"), IPv6: ptrAddr("fd7a:115c:a1e0::7c37:426a"),
Tags: []string{"tag:exit"}, Hostinfo: &tailcfg.Hostinfo{}, Tags: []string{"tag:exit"}, Hostinfo: &tailcfg.Hostinfo{},
}, },
@@ -36,23 +36,26 @@ import (
// setupGrantsCompatUsers returns the 3 test users for grants compatibility tests. // setupGrantsCompatUsers returns the 3 test users for grants compatibility tests.
// Email addresses use @example.com domain, matching the converted Tailscale policy format. // Users get norse-god names; nodes get original-151 pokémon names — matching
// the anonymized identifiers tscap writes into the capture files
// (see github.com/kradalby/tscap/anonymize).
func setupGrantsCompatUsers() types.Users { func setupGrantsCompatUsers() types.Users {
return types.Users{ return types.Users{
{Model: gorm.Model{ID: 1}, Name: "kratail2tid", Email: "kratail2tid@example.com"}, {Model: gorm.Model{ID: 1}, Name: "odin", Email: "odin@example.com"},
{Model: gorm.Model{ID: 2}, Name: "kristoffer", Email: "kristoffer@example.com"}, {Model: gorm.Model{ID: 2}, Name: "thor", Email: "thor@example.com"},
{Model: gorm.Model{ID: 3}, Name: "monitorpasskeykradalby", Email: "monitorpasskeykradalby@example.com"}, {Model: gorm.Model{ID: 3}, Name: "freya", Email: "freya@example.com"},
} }
} }
// setupGrantsCompatNodes returns the 8 test nodes for grants compatibility tests. // setupGrantsCompatNodes returns the 15 test nodes for grants compatibility tests.
// The node configuration matches the Tailscale test environment: // The node configuration matches the Tailscale test environment:
// - 3 user-owned nodes (user1, user-kris, user-mon) // - 3 user-owned nodes (bulbasaur, ivysaur, venusaur)
// - 5 tagged nodes (tagged-server, tagged-prod, tagged-client, subnet-router, exit-node) // - 12 tagged nodes (beedrill, kakuna, weedle, squirtle, charmander,
// pidgey, pidgeotto, rattata, raticate, spearow, fearow, blastoise)
func setupGrantsCompatNodes(users types.Users) types.Nodes { func setupGrantsCompatNodes(users types.Users) types.Nodes {
nodeUser1 := &types.Node{ nodeBulbasaur := &types.Node{
ID: 1, ID: 1,
GivenName: "user1", GivenName: "bulbasaur",
User: &users[0], User: &users[0],
UserID: &users[0].ID, UserID: &users[0].ID,
IPv4: ptrAddr("100.90.199.68"), IPv4: ptrAddr("100.90.199.68"),
@@ -60,9 +63,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
} }
nodeUserKris := &types.Node{ nodeIvysaur := &types.Node{
ID: 2, ID: 2,
GivenName: "user-kris", GivenName: "ivysaur",
User: &users[1], User: &users[1],
UserID: &users[1].ID, UserID: &users[1].ID,
IPv4: ptrAddr("100.110.121.96"), IPv4: ptrAddr("100.110.121.96"),
@@ -70,9 +73,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
} }
nodeUserMon := &types.Node{ nodeVenusaur := &types.Node{
ID: 3, ID: 3,
GivenName: "user-mon", GivenName: "venusaur",
User: &users[2], User: &users[2],
UserID: &users[2].ID, UserID: &users[2].ID,
IPv4: ptrAddr("100.103.90.82"), IPv4: ptrAddr("100.103.90.82"),
@@ -80,36 +83,36 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
} }
nodeTaggedServer := &types.Node{ nodeBeedrill := &types.Node{
ID: 4, ID: 4,
GivenName: "tagged-server", GivenName: "beedrill",
IPv4: ptrAddr("100.108.74.26"), IPv4: ptrAddr("100.108.74.26"),
IPv6: ptrAddr("fd7a:115c:a1e0::b901:4a87"), IPv6: ptrAddr("fd7a:115c:a1e0::b901:4a87"),
Tags: []string{"tag:server"}, Tags: []string{"tag:server"},
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
} }
nodeTaggedProd := &types.Node{ nodeKakuna := &types.Node{
ID: 5, ID: 5,
GivenName: "tagged-prod", GivenName: "kakuna",
IPv4: ptrAddr("100.103.8.15"), IPv4: ptrAddr("100.103.8.15"),
IPv6: ptrAddr("fd7a:115c:a1e0::5b37:80f"), IPv6: ptrAddr("fd7a:115c:a1e0::5b37:80f"),
Tags: []string{"tag:prod"}, Tags: []string{"tag:prod"},
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
} }
nodeTaggedClient := &types.Node{ nodeWeedle := &types.Node{
ID: 6, ID: 6,
GivenName: "tagged-client", GivenName: "weedle",
IPv4: ptrAddr("100.83.200.69"), IPv4: ptrAddr("100.83.200.69"),
IPv6: ptrAddr("fd7a:115c:a1e0::c537:c845"), IPv6: ptrAddr("fd7a:115c:a1e0::c537:c845"),
Tags: []string{"tag:client"}, Tags: []string{"tag:client"},
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
} }
nodeSubnetRouter := &types.Node{ nodeSquirtle := &types.Node{
ID: 7, ID: 7,
GivenName: "subnet-router", GivenName: "squirtle",
IPv4: ptrAddr("100.92.142.61"), IPv4: ptrAddr("100.92.142.61"),
IPv6: ptrAddr("fd7a:115c:a1e0::3e37:8e3d"), IPv6: ptrAddr("fd7a:115c:a1e0::3e37:8e3d"),
Tags: []string{"tag:router"}, Tags: []string{"tag:router"},
@@ -119,9 +122,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")}, ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")},
} }
nodeExitNode := &types.Node{ nodeCharmander := &types.Node{
ID: 8, ID: 8,
GivenName: "exit-node", GivenName: "charmander",
IPv4: ptrAddr("100.85.66.106"), IPv4: ptrAddr("100.85.66.106"),
IPv6: ptrAddr("fd7a:115c:a1e0::7c37:426a"), IPv6: ptrAddr("fd7a:115c:a1e0::7c37:426a"),
Tags: []string{"tag:exit"}, Tags: []string{"tag:exit"},
@@ -139,9 +142,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
// --- New nodes for expanded via grant topology --- // --- New nodes for expanded via grant topology ---
nodeExitA := &types.Node{ nodePidgey := &types.Node{
ID: 9, ID: 9,
GivenName: "exit-a", GivenName: "pidgey",
IPv4: ptrAddr("100.124.195.93"), IPv4: ptrAddr("100.124.195.93"),
IPv6: ptrAddr("fd7a:115c:a1e0::7837:c35d"), IPv6: ptrAddr("fd7a:115c:a1e0::7837:c35d"),
Tags: []string{"tag:exit-a"}, Tags: []string{"tag:exit-a"},
@@ -157,9 +160,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
}, },
} }
nodeExitB := &types.Node{ nodePidgeotto := &types.Node{
ID: 10, ID: 10,
GivenName: "exit-b", GivenName: "pidgeotto",
IPv4: ptrAddr("100.116.18.24"), IPv4: ptrAddr("100.116.18.24"),
IPv6: ptrAddr("fd7a:115c:a1e0::ff37:1218"), IPv6: ptrAddr("fd7a:115c:a1e0::ff37:1218"),
Tags: []string{"tag:exit-b"}, Tags: []string{"tag:exit-b"},
@@ -175,27 +178,27 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
}, },
} }
nodeGroupA := &types.Node{ nodeRattata := &types.Node{
ID: 11, ID: 11,
GivenName: "group-a-client", GivenName: "rattata",
IPv4: ptrAddr("100.107.162.14"), IPv4: ptrAddr("100.107.162.14"),
IPv6: ptrAddr("fd7a:115c:a1e0::a237:a20e"), IPv6: ptrAddr("fd7a:115c:a1e0::a237:a20e"),
Tags: []string{"tag:group-a"}, Tags: []string{"tag:group-a"},
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
} }
nodeGroupB := &types.Node{ nodeRaticate := &types.Node{
ID: 12, ID: 12,
GivenName: "group-b-client", GivenName: "raticate",
IPv4: ptrAddr("100.77.135.18"), IPv4: ptrAddr("100.77.135.18"),
IPv6: ptrAddr("fd7a:115c:a1e0::4b37:8712"), IPv6: ptrAddr("fd7a:115c:a1e0::4b37:8712"),
Tags: []string{"tag:group-b"}, Tags: []string{"tag:group-b"},
Hostinfo: &tailcfg.Hostinfo{}, Hostinfo: &tailcfg.Hostinfo{},
} }
nodeRouterA := &types.Node{ nodeSpearow := &types.Node{
ID: 13, ID: 13,
GivenName: "router-a", GivenName: "spearow",
IPv4: ptrAddr("100.109.43.124"), IPv4: ptrAddr("100.109.43.124"),
IPv6: ptrAddr("fd7a:115c:a1e0::a537:2b7c"), IPv6: ptrAddr("fd7a:115c:a1e0::a537:2b7c"),
Tags: []string{"tag:router-a"}, Tags: []string{"tag:router-a"},
@@ -205,9 +208,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.44.0.0/16")}, ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.44.0.0/16")},
} }
nodeRouterB := &types.Node{ nodeFearow := &types.Node{
ID: 14, ID: 14,
GivenName: "router-b", GivenName: "fearow",
IPv4: ptrAddr("100.65.172.123"), IPv4: ptrAddr("100.65.172.123"),
IPv6: ptrAddr("fd7a:115c:a1e0::5a37:ac7c"), IPv6: ptrAddr("fd7a:115c:a1e0::5a37:ac7c"),
Tags: []string{"tag:router-b"}, Tags: []string{"tag:router-b"},
@@ -217,9 +220,9 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.55.0.0/16")}, ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("10.55.0.0/16")},
} }
nodeMultiExitRouter := &types.Node{ nodeBlastoise := &types.Node{
ID: 15, ID: 15,
GivenName: "multi-exit-router", GivenName: "blastoise",
IPv4: ptrAddr("100.105.127.107"), IPv4: ptrAddr("100.105.127.107"),
IPv6: ptrAddr("fd7a:115c:a1e0::9537:7f6b"), IPv6: ptrAddr("fd7a:115c:a1e0::9537:7f6b"),
Tags: []string{"tag:exit", "tag:router"}, Tags: []string{"tag:exit", "tag:router"},
@@ -238,21 +241,21 @@ func setupGrantsCompatNodes(users types.Users) types.Nodes {
} }
return types.Nodes{ return types.Nodes{
nodeUser1, nodeBulbasaur,
nodeUserKris, nodeIvysaur,
nodeUserMon, nodeVenusaur,
nodeTaggedServer, nodeBeedrill,
nodeTaggedProd, nodeKakuna,
nodeTaggedClient, nodeWeedle,
nodeSubnetRouter, nodeSquirtle,
nodeExitNode, nodeCharmander,
nodeExitA, nodePidgey,
nodeExitB, nodePidgeotto,
nodeGroupA, nodeRattata,
nodeGroupB, nodeRaticate,
nodeRouterA, nodeSpearow,
nodeRouterB, nodeFearow,
nodeMultiExitRouter, nodeBlastoise,
} }
} }
@@ -267,22 +270,12 @@ func findGrantsNode(nodes types.Nodes, name string) *types.Node {
return nil return nil
} }
// convertPolicyUserEmails converts Tailscale SaaS user email formats to // convertPolicyUserEmails used to map SaaS-side emails to @example.com.
// headscale-compatible @example.com format in the raw policy JSON. // tscap now anonymizes the policy JSON at write time (kratail2tid -> odin,
// // kristoffer -> thor, monitorpasskeykradalby -> freya), so the captured
// Tailscale uses provider-specific email formats: // FullPolicy is already in its final form and this is a passthrough.
// - kratail2tid@passkey (passkey auth)
// - kristoffer@dalby.cc (email auth)
// - monitorpasskeykradalby@passkey (passkey auth)
//
// Headscale resolves users by Email field, so we convert all to @example.com.
func convertPolicyUserEmails(policyJSON []byte) []byte { func convertPolicyUserEmails(policyJSON []byte) []byte {
s := string(policyJSON) return policyJSON
s = strings.ReplaceAll(s, "kratail2tid@passkey", "kratail2tid@example.com")
s = strings.ReplaceAll(s, "kristoffer@dalby.cc", "kristoffer@example.com")
s = strings.ReplaceAll(s, "monitorpasskeykradalby@passkey", "monitorpasskeykradalby@example.com")
return []byte(s)
} }
// loadGrantTestFile loads and parses a single grant capture HuJSON file. // loadGrantTestFile loads and parses a single grant capture HuJSON file.
@@ -351,11 +344,12 @@ func TestGrantsCompat(t *testing.T) {
// Determine which node set to use based on the test's topology. // Determine which node set to use based on the test's topology.
// Tests captured with the expanded 15-node topology (V26+) have // Tests captured with the expanded 15-node topology (V26+) have
// nodes like exit-a, group-a-client, etc. Tests from the original // nodes like pidgey (exit-a), rattata (group-a-client), etc.
// 8-node topology should only use the first 8 nodes to avoid // Tests from the original 8-node topology should only use the
// resolving extra IPs from nodes that weren't present during capture. // first 8 nodes to avoid resolving extra IPs from nodes that
// weren't present during capture.
nodes := allNodes nodes := allNodes
if _, hasNewNodes := tf.Captures["exit-a"]; !hasNewNodes { if _, hasNewNodes := tf.Captures["pidgey"]; !hasNewNodes {
nodes = allNodes[:8] nodes = allNodes[:8]
} }
@@ -504,7 +498,7 @@ func testGrantSuccess(
for nodeName, capture := range tf.Captures { for nodeName, capture := range tf.Captures {
t.Run(nodeName, func(t *testing.T) { t.Run(nodeName, func(t *testing.T) {
// Check if this node was offline during capture. // Check if this node was offline during capture.
// tagged-prod was frequently offline (132 of 188 success tests). // kakuna (tag:prod) was frequently offline (132 of 188 success tests).
// When offline, packet_filter_rules is null and topology shows // When offline, packet_filter_rules is null and topology shows
// hostname="unknown" with empty tags. // hostname="unknown" with empty tags.
captureIsNull := len(capture.PacketFilterRules) == 0 || captureIsNull := len(capture.PacketFilterRules) == 0 ||
@@ -57,21 +57,11 @@ func loadRoutesTestFile(t *testing.T, path string) *testcapture.Capture {
return c return c
} }
// convertSaaSEmail rewrites a Tailscale SaaS user email to its // convertSaaSEmail used to map SaaS-side emails to @example.com placeholders.
// headscale-equivalent @example.com form, matching the convention used by // tscap now anonymizes captures at write time (norse-god names + pokémon
// convertPolicyUserEmails. The kristoffer@dalby.cc and *@passkey domains // hostnames), so the captured topology emails are already in their final
// are normalized to @example.com so that policy resolution finds the user // form and this is a passthrough.
// objects we set up in the test.
func convertSaaSEmail(email string) string { func convertSaaSEmail(email string) string {
switch email {
case "kratail2tid@passkey":
return "kratail2tid@example.com"
case "kristoffer@dalby.cc":
return "kristoffer@example.com"
case "monitorpasskeykradalby@passkey":
return "monitorpasskeykradalby@example.com"
}
return email return email
} }
@@ -19,7 +19,6 @@ package v2
import ( import (
"encoding/json" "encoding/json"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@@ -33,43 +32,41 @@ import (
// setupSSHDataCompatUsers returns the 3 test users for SSH data-driven // setupSSHDataCompatUsers returns the 3 test users for SSH data-driven
// compatibility tests. The user configuration matches the Tailscale test // compatibility tests. Users get norse-god names; nodes get original-151
// environment with email domains preserved for localpart matching: // pokémon names — matching the anonymized identifiers tscap writes into
// - kratail2tid@example.com (converted from @passkey) // the capture files (see github.com/kradalby/tscap/anonymize).
// - kristoffer@dalby.cc (kept as-is — different domain for localpart exclusion)
// - monitorpasskeykradalby@example.com (converted from @passkey)
func setupSSHDataCompatUsers() types.Users { func setupSSHDataCompatUsers() types.Users {
return types.Users{ return types.Users{
{ {
Model: gorm.Model{ID: 1}, Model: gorm.Model{ID: 1},
Name: "kratail2tid", Name: "odin",
Email: "kratail2tid@example.com", Email: "odin@example.com",
}, },
{ {
Model: gorm.Model{ID: 2}, Model: gorm.Model{ID: 2},
Name: "kristoffer", Name: "thor",
Email: "kristoffer@dalby.cc", Email: "thor@example.com",
}, },
{ {
Model: gorm.Model{ID: 3}, Model: gorm.Model{ID: 3},
Name: "monitorpasskeykradalby", Name: "freya",
Email: "monitorpasskeykradalby@example.com", Email: "freya@example.com",
}, },
} }
} }
// setupSSHDataCompatNodes returns the 5 test nodes for SSH data-driven // setupSSHDataCompatNodes returns the test nodes for SSH data-driven
// compatibility tests. Node GivenNames match the keys in the JSON files: // compatibility tests. Node GivenNames match the anonymized pokémon names:
// - user1 (owned by kratail2tid) // - bulbasaur (owned by odin)
// - user-kris (owned by kristoffer) // - ivysaur (owned by thor)
// - user-mon (owned by monitorpasskeykradalby) // - venusaur (owned by freya)
// - tagged-server (tag:server) // - beedrill (tag:server)
// - tagged-prod (tag:prod) // - kakuna (tag:prod)
func setupSSHDataCompatNodes(users types.Users) types.Nodes { func setupSSHDataCompatNodes(users types.Users) types.Nodes {
return types.Nodes{ return types.Nodes{
&types.Node{ &types.Node{
ID: 1, ID: 1,
GivenName: "user1", GivenName: "bulbasaur",
User: &users[0], User: &users[0],
UserID: &users[0].ID, UserID: &users[0].ID,
IPv4: ptrAddr("100.90.199.68"), IPv4: ptrAddr("100.90.199.68"),
@@ -78,7 +75,7 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes {
}, },
&types.Node{ &types.Node{
ID: 2, ID: 2,
GivenName: "user-kris", GivenName: "ivysaur",
User: &users[1], User: &users[1],
UserID: &users[1].ID, UserID: &users[1].ID,
IPv4: ptrAddr("100.110.121.96"), IPv4: ptrAddr("100.110.121.96"),
@@ -87,7 +84,7 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes {
}, },
&types.Node{ &types.Node{
ID: 3, ID: 3,
GivenName: "user-mon", GivenName: "venusaur",
User: &users[2], User: &users[2],
UserID: &users[2].ID, UserID: &users[2].ID,
IPv4: ptrAddr("100.103.90.82"), IPv4: ptrAddr("100.103.90.82"),
@@ -96,7 +93,7 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes {
}, },
&types.Node{ &types.Node{
ID: 4, ID: 4,
GivenName: "tagged-server", GivenName: "beedrill",
IPv4: ptrAddr("100.108.74.26"), IPv4: ptrAddr("100.108.74.26"),
IPv6: ptrAddr("fd7a:115c:a1e0::b901:4a87"), IPv6: ptrAddr("fd7a:115c:a1e0::b901:4a87"),
Tags: []string{"tag:server"}, Tags: []string{"tag:server"},
@@ -104,7 +101,7 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes {
}, },
&types.Node{ &types.Node{
ID: 5, ID: 5,
GivenName: "tagged-prod", GivenName: "kakuna",
IPv4: ptrAddr("100.103.8.15"), IPv4: ptrAddr("100.103.8.15"),
IPv6: ptrAddr("fd7a:115c:a1e0::5b37:80f"), IPv6: ptrAddr("fd7a:115c:a1e0::5b37:80f"),
Tags: []string{"tag:prod"}, Tags: []string{"tag:prod"},
@@ -113,19 +110,6 @@ func setupSSHDataCompatNodes(users types.Users) types.Nodes {
} }
} }
// convertSSHPolicyEmails converts Tailscale SaaS email domains to
// headscale-compatible format in the raw policy JSON.
//
// The @passkey domain is converted to @example.com. The @dalby.cc domain
// is kept as-is to preserve localpart matching semantics (kristoffer should
// NOT match localpart:*@example.com, just as it doesn't match
// localpart:*@passkey in Tailscale SaaS).
func convertSSHPolicyEmails(s string) string {
s = strings.ReplaceAll(s, "@passkey", "@example.com")
return s
}
// loadSSHTestFile loads and parses a single SSH capture HuJSON file. // loadSSHTestFile loads and parses a single SSH capture HuJSON file.
func loadSSHTestFile(t *testing.T, path string) *testcapture.Capture { func loadSSHTestFile(t *testing.T, path string) *testcapture.Capture {
t.Helper() t.Helper()
@@ -196,10 +180,11 @@ func TestSSHDataCompat(t *testing.T) {
return return
} }
// Use the captured full policy verbatim (with email-domain conversion). // Use the captured full policy verbatim. Anonymization in
policyJSON := convertSSHPolicyEmails(string(tf.Input.FullPolicy)) // tscap already rewrites SaaS emails to @example.com.
policyJSON := tf.Input.FullPolicy
pol, err := unmarshalPolicy([]byte(policyJSON)) pol, err := unmarshalPolicy(policyJSON)
require.NoError( require.NoError(
t, t,
err, err,