mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 04:34:07 +09:00
api/v2: check the createKey scope before reading the body
createKey has no static scope, so any token got its body parsed and validated before the handler's scope check.
This commit is contained in:
1 parent
0bf5b0f0af
commit
408b9b7797
3 files changed
+39
-2
No files matched your search
@@ -133,6 +133,7 @@ func registerKeys(api huma.API, b Backend) {
|
||||
Description: "Requires the `auth_keys` scope for an auth key, or `oauth_keys` for an OAuth client (an admin API key is all-access).",
|
||||
Tags: keysTags,
|
||||
Security: security,
|
||||
Middlewares: huma.Middlewares{requireCreateKeyScope(api)},
|
||||
MaxBodyBytes: maxBodyBytes,
|
||||
Errors: []int{
|
||||
http.StatusBadRequest,
|
||||
@@ -318,6 +319,22 @@ func requireKeyScope(ctx context.Context, need scope.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// requireCreateKeyScope refuses, before huma reads the body, a token that may
|
||||
// create neither kind of key; the handler then checks the kind it got.
|
||||
func requireCreateKeyScope(api huma.API) func(huma.Context, func(huma.Context)) {
|
||||
return func(ctx huma.Context, next func(huma.Context)) {
|
||||
if requireKeyScope(ctx.Context(), scope.AuthKeys) != nil &&
|
||||
requireKeyScope(ctx.Context(), scope.OAuthKeys) != nil {
|
||||
_ = huma.WriteErr(api, ctx, http.StatusForbidden,
|
||||
"token is missing the required scope auth_keys or oauth_keys")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// createAuthKey creates a machine auth key (pre-auth key). Ownership: tags -> a
|
||||
// tagged key; no tags -> owned by the API key's user. An OAuth access token must
|
||||
// mint tagged keys, and each tag must be within the token's grant.
|
||||
|
||||
@@ -51,7 +51,7 @@ var apiv2CompatSkipReasons = map[string]string{
|
||||
"oauth/R3/3/status": missingScopeNotFound,
|
||||
"oauth/K3/0/status": missingScopeNotFound,
|
||||
"oauth/J2/0/status": missingScopeNotFound,
|
||||
"oauth/K4/0/status": bodyBeforeScope,
|
||||
"oauth/K4/0/status": missingScopeNotFound,
|
||||
"oauth/D1/0/message": missingScopeText,
|
||||
|
||||
"oauth/O1/0/scope": scopeEchoAsSent,
|
||||
@@ -200,7 +200,6 @@ func notCompared(prefixes []string, key string) bool {
|
||||
// Differences a planned change removes.
|
||||
const (
|
||||
scopeEchoAsSent = "SCOPE_ECHO_AS_SENT: SaaS echoes a token's scopes sorted and without repeats; headscale echoes the request as sent"
|
||||
bodyBeforeScope = "BODY_BEFORE_SCOPE: headscale validates a createKey body before the token's scope and answers 422; SaaS answers 404 for the missing scope"
|
||||
retagRemovesUnownedTag = "RETAG_REMOVES_UNOWNED_TAG: SaaS refuses a token's retag of a device holding a tag outside the token's grant with 400 cannot remove current tags; headscale replaces the tags"
|
||||
)
|
||||
|
||||
|
||||
@@ -181,6 +181,27 @@ func TestAPIv2OAuthMatrix_ScopeNarrowing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIv2OAuthMatrix_CreateKeyScopeBeforeBody: createKey declares no static
|
||||
// scope, so a token that may create neither kind of key must be refused before
|
||||
// huma reads and validates the body.
|
||||
func TestAPIv2OAuthMatrix_CreateKeyScopeBeforeBody(t *testing.T) {
|
||||
_, baseURL, admin := newOAuthTestServer(t)
|
||||
invalid := map[string]any{"tags": []int{0}}
|
||||
|
||||
for _, s := range scope.Known() {
|
||||
held := []scope.Scope{s}
|
||||
mayCreate := scope.Grants(held, scope.AuthKeys) || scope.Grants(held, scope.OAuthKeys)
|
||||
|
||||
status, body := apiReq(t, http.MethodPost, baseURL+"/api/v2/tailnet/-/keys",
|
||||
mintScopedToken(t, baseURL, admin, s), invalid)
|
||||
if mayCreate {
|
||||
assert.Equalf(t, http.StatusUnprocessableEntity, status, "%s: %s", s, body)
|
||||
} else {
|
||||
assert.Truef(t, scopeDenied(status, body), "%s: %d %s", s, status, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIv2OAuthMatrix_TagNarrowing proves P1 for tags at mint time: a client may
|
||||
// only mint a token for tags within its grant (closing the /oauth/token tags-param
|
||||
// path). Only an untagged client with the "all" scope may request any tag.
|
||||
|
||||
Reference in new issue
Block a user