From b12df053c21cdf6e21ef4ae715879ae8413ad750 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 26 Jan 2025 17:36:35 +0100 Subject: [PATCH] update list node helper, add listuser Signed-off-by: Kristoffer Dalby --- integration/auth_key_test.go | 26 +---- integration/auth_oidc_test.go | 154 +++++------------------------- integration/auth_web_flow_test.go | 15 +-- integration/control.go | 3 +- integration/general_test.go | 18 +--- integration/hsic/hsic.go | 64 +++++++++++-- 6 files changed, 94 insertions(+), 186 deletions(-) diff --git a/integration/auth_key_test.go b/integration/auth_key_test.go index 83f15d492c..66c02e8e75 100644 --- a/integration/auth_key_test.go +++ b/integration/auth_key_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/samber/lo" @@ -59,12 +58,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) { headscale, err := scenario.Headscale() assertNoErrGetHeadscale(t, err) - var listNodes []*v1.Node - for username := range spec { - nodes, err := headscale.ListNodesInUser(username) - assertNoErr(t, err) - listNodes = append(listNodes, nodes...) - } + listNodes, err := headscale.ListNodes() assert.Equal(t, len(listNodes), len(allClients)) nodeCountBeforeLogout := len(listNodes) t.Logf("node count before logout: %d", nodeCountBeforeLogout) @@ -120,12 +114,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) { success := pingAllHelper(t, allClients, allAddrs) t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) - listNodes = nil - for username := range spec { - nodes, err := headscale.ListNodesInUser(username) - assertNoErr(t, err) - listNodes = append(listNodes, nodes...) - } + listNodes, err = headscale.ListNodes() require.Equal(t, nodeCountBeforeLogout, len(listNodes)) t.Logf("node count first login: %d, after relogin: %d", nodeCountBeforeLogout, len(listNodes)) @@ -198,12 +187,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) { headscale, err := scenario.Headscale() assertNoErrGetHeadscale(t, err) - var listNodes []*v1.Node - for username := range spec { - nodes, err := headscale.ListNodesInUser(username) - assertNoErr(t, err) - listNodes = append(listNodes, nodes...) - } + listNodes, err := headscale.ListNodes() assert.Equal(t, len(listNodes), len(allClients)) nodeCountBeforeLogout := len(listNodes) t.Logf("node count before logout: %d", nodeCountBeforeLogout) @@ -235,12 +219,12 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) { } } - user1Nodes, err := headscale.ListNodesInUser("user1") + user1Nodes, err := headscale.ListNodes("user1") assertNoErr(t, err) assert.Len(t, user1Nodes, len(allClients)) // Validate that all the old nodes are still present with user2 - user2Nodes, err := headscale.ListNodesInUser("user2") + user2Nodes, err := headscale.ListNodes("user2") assertNoErr(t, err) assert.Len(t, user2Nodes, len(allClients)/2) diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index 024d8c2c4c..847f4ef71d 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -116,17 +116,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) { headscale, err := scenario.Headscale() assertNoErr(t, err) - var listUsers []v1.User - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "users", - "list", - "--output", - "json", - }, - &listUsers, - ) + listUsers, err := headscale.ListUsers() assertNoErr(t, err) want := []v1.User{ @@ -249,7 +239,7 @@ func TestOIDC024UserCreation(t *testing.T) { emailVerified bool cliUsers []string oidcUsers []string - want func(iss string) []v1.User + want func(iss string) []*v1.User }{ { name: "no-migration-verified-email", @@ -259,8 +249,8 @@ func TestOIDC024UserCreation(t *testing.T) { emailVerified: true, cliUsers: []string{"user1", "user2"}, oidcUsers: []string{"user1", "user2"}, - want: func(iss string) []v1.User { - return []v1.User{ + want: func(iss string) []*v1.User { + return []*v1.User{ { Id: 1, Name: "user1", @@ -296,8 +286,8 @@ func TestOIDC024UserCreation(t *testing.T) { emailVerified: false, cliUsers: []string{"user1", "user2"}, oidcUsers: []string{"user1", "user2"}, - want: func(iss string) []v1.User { - return []v1.User{ + want: func(iss string) []*v1.User { + return []*v1.User{ { Id: 1, Name: "user1", @@ -332,8 +322,8 @@ func TestOIDC024UserCreation(t *testing.T) { emailVerified: true, cliUsers: []string{"user1", "user2"}, oidcUsers: []string{"user1", "user2"}, - want: func(iss string) []v1.User { - return []v1.User{ + want: func(iss string) []*v1.User { + return []*v1.User{ { Id: 1, Name: "user1", @@ -360,8 +350,8 @@ func TestOIDC024UserCreation(t *testing.T) { emailVerified: false, cliUsers: []string{"user1", "user2"}, oidcUsers: []string{"user1", "user2"}, - want: func(iss string) []v1.User { - return []v1.User{ + want: func(iss string) []*v1.User { + return []*v1.User{ { Id: 1, Name: "user1", @@ -396,8 +386,8 @@ func TestOIDC024UserCreation(t *testing.T) { emailVerified: true, cliUsers: []string{"user1.headscale.net", "user2.headscale.net"}, oidcUsers: []string{"user1", "user2"}, - want: func(iss string) []v1.User { - return []v1.User{ + want: func(iss string) []*v1.User { + return []*v1.User{ // Hmm I think we will have to overwrite the initial name here // createuser with "user1.headscale.net", but oidc with "user1" { @@ -426,8 +416,8 @@ func TestOIDC024UserCreation(t *testing.T) { emailVerified: false, cliUsers: []string{"user1.headscale.net", "user2.headscale.net"}, oidcUsers: []string{"user1", "user2"}, - want: func(iss string) []v1.User { - return []v1.User{ + want: func(iss string) []*v1.User { + return []*v1.User{ { Id: 1, Name: "user1.headscale.net", @@ -509,17 +499,7 @@ func TestOIDC024UserCreation(t *testing.T) { want := tt.want(oidcConfig.Issuer) - var listUsers []v1.User - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "users", - "list", - "--output", - "json", - }, - &listUsers, - ) + listUsers, err := headscale.ListUsers() assertNoErr(t, err) sort.Slice(listUsers, func(i, j int) bool { @@ -587,23 +567,6 @@ func TestOIDCAuthenticationWithPKCE(t *testing.T) { err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) - // Verify PKCE was used in authentication - headscale, err := scenario.Headscale() - assertNoErr(t, err) - - var listUsers []v1.User - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "users", - "list", - "--output", - "json", - }, - &listUsers, - ) - assertNoErr(t, err) - allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { return x.String() }) @@ -664,17 +627,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { headscale, err := scenario.Headscale() assertNoErr(t, err) - var listUsers []v1.User - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "users", - "list", - "--output", - "json", - }, - &listUsers, - ) + listUsers, err := headscale.ListUsers() assertNoErr(t, err) assert.Len(t, listUsers, 0) @@ -687,19 +640,10 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { _, err = doLoginURL(ts.Hostname(), u) assertNoErr(t, err) - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "users", - "list", - "--output", - "json", - }, - &listUsers, - ) + listUsers, err = headscale.ListUsers() assertNoErr(t, err) assert.Len(t, listUsers, 1) - wantUsers := []v1.User{ + wantUsers := []*v1.User{ { Id: 1, Name: "user1", @@ -717,17 +661,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { t.Fatalf("unexpected users: %s", diff) } - var listNodes []v1.Node - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - &listNodes, - ) + listNodes, err := headscale.ListNodes() assertNoErr(t, err) assert.Len(t, listNodes, 1) @@ -751,19 +685,10 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { _, err = doLoginURL(ts.Hostname(), u) assertNoErr(t, err) - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "users", - "list", - "--output", - "json", - }, - &listUsers, - ) + listUsers, err = headscale.ListUsers() assertNoErr(t, err) assert.Len(t, listUsers, 2) - wantUsers = []v1.User{ + wantUsers = []*v1.User{ { Id: 1, Name: "user1", @@ -788,17 +713,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { t.Fatalf("unexpected users: %s", diff) } - var listNodesAfterNewUserLogin []v1.Node - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - &listNodesAfterNewUserLogin, - ) + listNodesAfterNewUserLogin, err := headscale.ListNodes() assertNoErr(t, err) assert.Len(t, listNodesAfterNewUserLogin, 2) @@ -827,19 +742,10 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { _, err = doLoginURL(ts.Hostname(), u) assertNoErr(t, err) - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "users", - "list", - "--output", - "json", - }, - &listUsers, - ) + listUsers, err = headscale.ListUsers() assertNoErr(t, err) assert.Len(t, listUsers, 2) - wantUsers = []v1.User{ + wantUsers = []*v1.User{ { Id: 1, Name: "user1", @@ -864,17 +770,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { t.Fatalf("unexpected users: %s", diff) } - var listNodesAfterLoggingBackIn []v1.Node - err = executeAndUnmarshal(headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - &listNodesAfterLoggingBackIn, - ) + listNodesAfterLoggingBackIn, err := headscale.ListNodes() assertNoErr(t, err) assert.Len(t, listNodesAfterLoggingBackIn, 2) diff --git a/integration/auth_web_flow_test.go b/integration/auth_web_flow_test.go index a6c30fd183..f87bab1376 100644 --- a/integration/auth_web_flow_test.go +++ b/integration/auth_web_flow_test.go @@ -9,7 +9,6 @@ import ( "strings" "testing" - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/integration/hsic" "github.com/samber/lo" "github.com/stretchr/testify/assert" @@ -112,12 +111,7 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) { headscale, err := scenario.Headscale() assertNoErrGetHeadscale(t, err) - var listNodes []*v1.Node - for username := range spec { - nodes, err := headscale.ListNodesInUser(username) - assertNoErr(t, err) - listNodes = append(listNodes, nodes...) - } + listNodes, err := headscale.ListNodes() assert.Equal(t, len(listNodes), len(allClients)) nodeCountBeforeLogout := len(listNodes) t.Logf("node count before logout: %d", nodeCountBeforeLogout) @@ -165,12 +159,7 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) { success = pingAllHelper(t, allClients, allAddrs) t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) - listNodes = nil - for username := range spec { - nodes, err := headscale.ListNodesInUser(username) - assertNoErr(t, err) - listNodes = append(listNodes, nodes...) - } + listNodes, err = headscale.ListNodes() require.Equal(t, nodeCountBeforeLogout, len(listNodes)) t.Logf("node count first login: %d, after relogin: %d", nodeCountBeforeLogout, len(listNodes)) diff --git a/integration/control.go b/integration/control.go index b5699577f5..8ec6bad6e4 100644 --- a/integration/control.go +++ b/integration/control.go @@ -17,7 +17,8 @@ type ControlServer interface { WaitForRunning() error CreateUser(user string) error CreateAuthKey(user string, reusable bool, ephemeral bool) (*v1.PreAuthKey, error) - ListNodesInUser(user string) ([]*v1.Node, error) + ListNodes(users ...string) ([]*v1.Node, error) + ListUsers() ([]*v1.User, error) GetCert() []byte GetHostname() string GetIP() string diff --git a/integration/general_test.go b/integration/general_test.go index 4201c5e5b5..3bdce469db 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -183,19 +183,9 @@ func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) { t.Logf("all clients logged out") - for userName := range spec { - nodes, err := headscale.ListNodesInUser(userName) - if err != nil { - log.Error(). - Err(err). - Str("user", userName). - Msg("Error listing nodes in user") - - return - } - - require.Len(t, nodes, 0) - } + nodes, err := headscale.ListNodes() + assertNoErr(t, err) + require.Len(t, nodes, 0) } // TestEphemeral2006DeletedTooQuickly verifies that ephemeral nodes are not @@ -298,7 +288,7 @@ func TestEphemeral2006DeletedTooQuickly(t *testing.T) { time.Sleep(3 * time.Minute) for userName := range spec { - nodes, err := headscale.ListNodesInUser(userName) + nodes, err := headscale.ListNodes(userName) if err != nil { log.Error(). Err(err). diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index e38abd1ce3..cff703ac63 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -1,6 +1,7 @@ package hsic import ( + "cmp" "crypto/tls" "encoding/json" "errors" @@ -10,6 +11,7 @@ import ( "net/http" "os" "path" + "sort" "strconv" "strings" "time" @@ -744,12 +746,58 @@ func (t *HeadscaleInContainer) CreateAuthKey( return &preAuthKey, nil } -// ListNodesInUser list the TailscaleClients (Node, Headscale internal representation) -// associated with a user. -func (t *HeadscaleInContainer) ListNodesInUser( - user string, +// ListNodes lists the currently registered Nodes in headscale. +// Optionally a list of usernames can be passed to get users for +// specific users. +func (t *HeadscaleInContainer) ListNodes( + users ...string, ) ([]*v1.Node, error) { - command := []string{"headscale", "--user", user, "nodes", "list", "--output", "json"} + var ret []*v1.Node + execUnmarshal := func(command []string) error { + result, _, err := dockertestutil.ExecuteCommand( + t.container, + command, + []string{}, + ) + if err != nil { + return fmt.Errorf("failed to execute list node command: %w", err) + } + + var nodes []*v1.Node + err = json.Unmarshal([]byte(result), &nodes) + if err != nil { + return fmt.Errorf("failed to unmarshal nodes: %w", err) + } + + ret = append(ret, nodes...) + return nil + } + + if len(users) == 0 { + err := execUnmarshal([]string{"headscale", "nodes", "list", "--output", "json"}) + if err != nil { + return nil, err + } + } else { + for _, user := range users { + command := []string{"headscale", "--user", user, "nodes", "list", "--output", "json"} + + err := execUnmarshal(command) + if err != nil { + return nil, err + } + } + } + + sort.Slice(ret, func(i, j int) bool { + return cmp.Compare(ret[i].GetId(), ret[j].GetId()) == -1 + }) + return ret, nil +} + +// ListUsers returns a list of users from Headscale. +func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) { + command := []string{"headscale", "users", "list", "--output", "json"} result, _, err := dockertestutil.ExecuteCommand( t.container, @@ -760,13 +808,13 @@ func (t *HeadscaleInContainer) ListNodesInUser( return nil, fmt.Errorf("failed to execute list node command: %w", err) } - var nodes []*v1.Node - err = json.Unmarshal([]byte(result), &nodes) + var users []*v1.User + err = json.Unmarshal([]byte(result), &users) if err != nil { return nil, fmt.Errorf("failed to unmarshal nodes: %w", err) } - return nodes, nil + return users, nil } // WriteFile save file inside the Headscale container.