hscontrol: compare a zero-length policy write

SaaS takes an empty POST /acl as a reset to its allow-all default, while
its validate refuses the same body. Headscale's v1 and v2 refuse both.
This commit is contained in:
Kristoffer Dalby committed 2026-10-30 14:55:37 +00:00
1 parent 6413c4a81b
commit cae755878a
1 file changed
+11 -17
+11 -17
View File
@@ -74,21 +74,18 @@ var apiv2CompatSkipReasons = map[string]string{
"oauth/device:before/all/device-oauth-client": deviceOAuthClientMaker,
"oauth/device:joiner/all/device-oauth-client": deviceOAuthClientMaker,
"policy-api/P2/1/status": emptyPolicyReset,
"policy-api/P2/1/outcome": emptyPolicyReset,
"policy-api/P2/1/enforced": emptyPolicyReset,
"policy-api/P2/2/body": emptyPolicyReset,
"policy-api/P2/2/etag": emptyPolicyReset,
"policy-api/P7/0/body": hujsonFormatted,
"policy-api/P7/1/body": hujsonFormatted,
}
// apiv2CompatNotCompared lists, by key prefix, checks left out: SaaS's
// answer there is in doubt, so it is no reference.
var apiv2CompatNotCompared = []string{
"policy-api/P2/1/status",
"policy-api/P2/1/outcome",
"policy-api/P2/1/enforced",
"policy-api/P2/1/v1-verdict",
"policy-api/P2/1/v1-stored",
"policy-api/P2/1/v1-enforced",
"policy-api/P2/2/body",
"policy-api/P2/2/etag",
"policy-api/P2/1/v1-verdict": emptyPolicyReset,
"policy-api/P2/1/v1-stored": emptyPolicyReset,
"policy-api/P2/1/v1-enforced": emptyPolicyReset,
}
// apiv2CompatNotReplayed lists steps left out, keyed <test_id>/<step>, with
@@ -123,6 +120,7 @@ const (
deviceOAuthClientMaker = "DEVICE_OAUTH_CLIENT_ID: SaaS names, in oauthClientId, the OAuth client whose token made the device's key; headscale does not record which client that was, so it leaves the field out"
missingScopeNotFound = "MISSING_SCOPE_NOT_FOUND: SaaS answers a call outside the token's scopes with 404 not found; headscale answers 403"
unknownScopeGranted = "UNKNOWN_SCOPE_GRANTED: SaaS ignores an unknown requested scope and grants the client's own; headscale grants the unknown scope alone"
emptyPolicyReset = "EMPTY_POLICY_RESET: SaaS takes a zero-length policy write as a reset to its default policy and enforces it; headscale refuses the body"
)
// TestAPIv2OAuthCompat replays OAuth clients, token exchanges, auth keys,
@@ -350,17 +348,13 @@ func (r *apiReplay) step(t *testing.T, step testcapture.Step) {
}
}
keyPrefix := r.c.TestID + "/" + step.Name + "/"
checks = slices.DeleteFunc(checks, func(ch apiCheck) bool {
return notCompared(apiv2CompatNotCompared, keyPrefix+ch.name)
})
if len(checks) == 0 {
return
}
// Marked here, so a run filtered to other steps does not call these
// entries stale.
keyPrefix := r.c.TestID + "/" + step.Name + "/"
for _, ch := range checks {
r.evaluated[keyPrefix+ch.name] = true
}