auth: refuse requested tags with SaaS's bare text

Clients show it as it is, and it carried an internal prefix.
This commit is contained in:
Kristoffer Dalby committed 2026-11-03 17:05:00 +01:00
1 parent 4ec3f3c161
commit 197895be2c
2 files changed
+20

No files matched your search

+5
View File
@@ -495,6 +495,11 @@ func (h *Headscale) handleRegisterWithAuthKey(
return nil, NewHTTPError(http.StatusUnauthorized, perr.Error(), nil)
}
// The client shows this text to the user as it is.
if errors.Is(err, state.ErrRequestedTagsInvalidOrNotPermitted) {
return nil, NewHTTPError(http.StatusBadRequest, err.Error(), nil)
}
return nil, err
}
@@ -349,6 +349,9 @@ func (run *reauthRun) cellChecks(
check("outcome", registerOutcome(step.Register[login.Node]), func() string {
return registerOutcome(r.clients[cell].RegisterAttempts()[prev.attempts[cell]:])
})
check("tag-refusal", tagRefusal(step.Register[login.Node]), func() string {
return tagRefusal(r.clients[cell].RegisterAttempts()[prev.attempts[cell]:])
})
}
if slices.ContainsFunc(want, func(v deviceView) bool { _, ok := run.saasPrev[v.id]; return ok }) {
@@ -464,6 +467,18 @@ func loginReplayable(step testcapture.Step, a testcapture.Action, keyTags map[st
strings.HasSuffix(registerOutcome(step.Register[a.Node]), "accepted")
}
// tagRefusal is the text of a login's refused-tags answer: a client shows it as
// it is, so unlike other refusals its words are compared.
func tagRefusal(attempts []testcapture.RegisterAttempt) string {
for _, a := range attempts {
if strings.Contains(a.Err, "requested tags") {
return a.Err
}
}
return ""
}
// registerOutcome reads a login's register exchanges up to the answer that
// ends it: "node-key-expired" for each answer asking for a new node key,
// then "accepted", "refused" or "auth-url". Errors of a login the client