From 8540926393a3ef4d774689b64e4c828bcb6bddd3 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 21 Oct 2026 09:47:54 +0000 Subject: [PATCH] hscontrol: compare a waiting login when key expiry is turned off SaaS answers a waiting login once an admin turns the device's key expiry off, with the client's newest node key and no login. Headscale keeps the login waiting. --- hscontrol/apiv2_compat_test.go | 7 ------- hscontrol/auth_compat_test.go | 20 ++++++++------------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/hscontrol/apiv2_compat_test.go b/hscontrol/apiv2_compat_test.go index f2aed7143..89142d24a 100644 --- a/hscontrol/apiv2_compat_test.go +++ b/hscontrol/apiv2_compat_test.go @@ -100,13 +100,6 @@ var apiv2CompatNotReplayed = map[string][]string{ "oauth/E9b": {"{key:K-E9b}"}, // uses E7's client } -// notCompared reports whether key falls under one of prefixes. -func notCompared(prefixes []string, key string) bool { - return slices.ContainsFunc(prefixes, func(p string) bool { - return strings.HasPrefix(key, p) - }) -} - // Differences no planned change removes yet. const ( mintScopeRefusalStatus = "TOKEN_SCOPE_REFUSAL_STATUS: SaaS refuses a token scope the client lacks with 403 and a message; headscale with 400 invalid_scope" diff --git a/hscontrol/auth_compat_test.go b/hscontrol/auth_compat_test.go index 0eff5fdfc..43e414239 100644 --- a/hscontrol/auth_compat_test.go +++ b/hscontrol/auth_compat_test.go @@ -30,22 +30,22 @@ import ( // pendingLoginSkipReasons lists checks where headscale differs from SaaS, // keyed ///. A listed check that matches fails, // so a fix must delete its entry. -var pendingLoginSkipReasons = map[string]string{} - -// pendingLoginNotCompared lists, by key prefix, checks left out: SaaS's -// answer there is in doubt, so it is no reference. -var pendingLoginNotCompared = []string{ - "pending-login/PL-E4-restore/pl-laptop/exchange-0", - "pending-login/PL-E4-restore/pl-laptop/devices", +var pendingLoginSkipReasons = map[string]string{ + "pending-login/PL-E4-restore/pl-laptop/exchange-0": loginCompletedByRestore, + "pending-login/PL-E4-restore/pl-laptop/devices": loginCompletedByRestore, } +// SaaS behaviour recorded as it is, not followed. +const loginCompletedByRestore = "LOGIN_COMPLETED_BY_RESTORE: SaaS completes a waiting login when the device's key expiry is turned off, giving it the client's newest node key without a login; headscale keeps the login waiting" + // TestPendingLoginCompat replays a fresh machine's interactive login across // a restart and a stop, and an expired device restarted twice while its // login waits, then restored. Each exchange the client logged is sent as the // client sent it: same machine and node keys, follow-ups to the URL the // server gave. It compares each answer's flags, whether an auth URL is new or // repeats an earlier one, and the devices on each machine with their node -// key. +// key. The replay approves no login itself, so a waiting login that SaaS +// completed on restore shows in the follow-up's answer and the node key. func TestPendingLoginCompat(t *testing.T) { t.Parallel() @@ -528,10 +528,6 @@ func renderNodeKeys(keys []string) string { func (r *pendingReplay) hold(t *testing.T, prefix string, checks []pendingCheck) { t.Helper() - checks = slices.DeleteFunc(checks, func(c pendingCheck) bool { - return notCompared(pendingLoginNotCompared, prefix+c.name) - }) - const holdTicks = 5 got := make([]string, len(checks))