mirror of
https://github.com/juanfont/headscale.git
synced 2026-01-23 02:24:10 +00:00
integration: add test for reauth tag removal
Some checks failed
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Some checks failed
Build / build-nix (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Has been cancelled
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Has been cancelled
Check Generated Files / check-generated (push) Has been cancelled
NixOS Module Tests / nix-module-check (push) Has been cancelled
Tests / test (push) Has been cancelled
Add TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags to validate that nodes can be untagged via `tailscale up --advertise-tags= --force-reauth`. The test verifies: - Node starts with tags and is owned by tagged-devices - After reauth with empty tags, all tags are removed - Node ownership returns to the authenticating user Updates #2979
This commit is contained in:
parent
0451dd4718
commit
2e180d2587
2 changed files with 171 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -2463,3 +2464,172 @@ func TestTagsAdminAPICannotSetInvalidFormat(t *testing.T) {
|
|||
}
|
||||
}, 10*time.Second, 500*time.Millisecond, "verifying original tags preserved")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test for Issue #2979: Reauth to untag a device
|
||||
// =============================================================================
|
||||
|
||||
// TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags tests that reauthenticating
|
||||
// with an empty tag list (--advertise-tags= --force-reauth) removes all tags
|
||||
// and returns ownership to the user.
|
||||
//
|
||||
// Bug #2979: Reauth to untag a device keeps it tagged
|
||||
// Setup: Register a node with tags via user login, then reauth with --advertise-tags= --force-reauth
|
||||
// Expected: Node should have no tags and ownership should return to the user.
|
||||
//
|
||||
// Note: This only works with --force-reauth because without it, the Tailscale
|
||||
// client doesn't trigger a full reauth to the server - it only updates local state.
|
||||
func TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
t.Run("with force-reauth", func(t *testing.T) {
|
||||
tc := struct {
|
||||
name string
|
||||
testName string
|
||||
forceReauth bool
|
||||
}{
|
||||
name: "with force-reauth",
|
||||
testName: "with-force-reauth",
|
||||
forceReauth: true,
|
||||
}
|
||||
policy := tagsTestPolicy()
|
||||
|
||||
spec := ScenarioSpec{
|
||||
NodesPerUser: 0,
|
||||
Users: []string{tagTestUser},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnvWithLoginURL(
|
||||
[]tsic.Option{},
|
||||
hsic.WithACLPolicy(policy),
|
||||
hsic.WithTestName("tags-reauth-untag-2979-"+tc.testName),
|
||||
hsic.WithTLS(),
|
||||
)
|
||||
requireNoErrHeadscaleEnv(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
requireNoErrGetHeadscale(t, err)
|
||||
|
||||
// Step 1: Create and register a node with tags
|
||||
t.Logf("Step 1: Registering node with tags")
|
||||
|
||||
client, err := scenario.CreateTailscaleNode(
|
||||
"head",
|
||||
tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]),
|
||||
tsic.WithExtraLoginArgs([]string{"--advertise-tags=tag:valid-owned,tag:second"}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
loginURL, err := client.LoginWithURL(headscale.GetEndpoint())
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := doLoginURL(client.Hostname(), loginURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = scenario.runHeadscaleRegister(tagTestUser, body)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.WaitForRunning(120 * time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify initial tags
|
||||
var initialNodeID uint64
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes(tagTestUser)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, nodes, 1, "Expected exactly one node")
|
||||
|
||||
if len(nodes) == 1 {
|
||||
node := nodes[0]
|
||||
initialNodeID = node.GetId()
|
||||
t.Logf("Initial state - Node ID: %d, Tags: %v, User: %s",
|
||||
node.GetId(), node.GetTags(), node.GetUser().GetName())
|
||||
|
||||
// Verify node has the expected tags
|
||||
assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned", "tag:second"})
|
||||
}
|
||||
}, 30*time.Second, 500*time.Millisecond, "checking initial tags")
|
||||
|
||||
// Step 2: Reauth with empty tags to remove all tags
|
||||
t.Logf("Step 2: Reauthenticating with empty tag list to untag device (%s)", tc.name)
|
||||
|
||||
if tc.forceReauth {
|
||||
// Manually run tailscale up with --force-reauth and empty tags
|
||||
// This will output a login URL that we need to complete
|
||||
// Include --hostname to match the initial login command
|
||||
command := []string{
|
||||
"tailscale", "up",
|
||||
"--login-server=" + headscale.GetEndpoint(),
|
||||
"--hostname=" + client.Hostname(),
|
||||
"--advertise-tags=",
|
||||
"--force-reauth",
|
||||
}
|
||||
|
||||
stdout, stderr, _ := client.Execute(command)
|
||||
t.Logf("Reauth command stderr: %s", stderr)
|
||||
|
||||
// Parse the login URL from the command output
|
||||
loginURL, err := util.ParseLoginURLFromCLILogin(stdout + stderr)
|
||||
require.NoError(t, err, "Failed to parse login URL from reauth command")
|
||||
t.Logf("Reauth login URL: %s", loginURL)
|
||||
|
||||
body, err := doLoginURL(client.Hostname(), loginURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = scenario.runHeadscaleRegister(tagTestUser, body)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.WaitForRunning(120 * time.Second)
|
||||
require.NoError(t, err)
|
||||
t.Logf("Completed reauth with empty tags")
|
||||
} else {
|
||||
// Without force-reauth, just try tailscale up
|
||||
// Include --hostname to match the initial login command
|
||||
command := []string{
|
||||
"tailscale", "up",
|
||||
"--login-server=" + headscale.GetEndpoint(),
|
||||
"--hostname=" + client.Hostname(),
|
||||
"--advertise-tags=",
|
||||
}
|
||||
stdout, stderr, err := client.Execute(command)
|
||||
t.Logf("CLI reauth result: err=%v, stdout=%s, stderr=%s", err, stdout, stderr)
|
||||
}
|
||||
|
||||
// Step 3: Verify tags are removed and ownership is returned to user
|
||||
// This is the key assertion for bug #2979
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes(tagTestUser)
|
||||
assert.NoError(c, err)
|
||||
|
||||
if len(nodes) >= 1 {
|
||||
node := nodes[0]
|
||||
t.Logf("After reauth - Node ID: %d, Tags: %v, User: %s",
|
||||
node.GetId(), node.GetTags(), node.GetUser().GetName())
|
||||
|
||||
// Assert: Node should have NO tags
|
||||
assertNodeHasNoTagsWithCollect(c, node)
|
||||
|
||||
// Assert: Node should be owned by the user (not tagged-devices)
|
||||
assert.Equal(c, tagTestUser, node.GetUser().GetName(),
|
||||
"Node ownership should return to user %s after untagging", tagTestUser)
|
||||
|
||||
// Verify the node ID is still the same (not a new registration)
|
||||
assert.Equal(c, initialNodeID, node.GetId(),
|
||||
"Node ID should remain the same after reauth")
|
||||
|
||||
if len(node.GetTags()) == 0 && node.GetUser().GetName() == tagTestUser {
|
||||
t.Logf("Test #2979 (%s) PASS: Node successfully untagged and ownership returned to user", tc.name)
|
||||
} else {
|
||||
t.Logf("Test #2979 (%s) FAIL: Expected no tags and user=%s, got tags=%v user=%s",
|
||||
tc.name, tagTestUser, node.GetTags(), node.GetUser().GetName())
|
||||
}
|
||||
}
|
||||
}, 60*time.Second, 1*time.Second, "verifying tags removed and ownership returned")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue