Files
Kristoffer Dalby af46fe8867 hscontrol, integration: test OAuth scope enforcement and the oauth-clients CLI
Add scope-enforcement coverage and integration tests for the
oauth-clients CLI across the test matrix.
2026-06-26 09:18:06 +02:00

59 lines
1.9 KiB
Go

package apiv2
import (
"testing"
"github.com/danielgtaylor/huma/v2"
"github.com/go-chi/chi/v5"
"github.com/juanfont/headscale/hscontrol/scope"
)
// selfEnforcedKeyOps are the authenticated operations that intentionally declare
// NO static scope because they multiplex on keyType and authorize inside the
// handler via requireKeyScope (see keys.go). Every other authenticated operation
// must declare a scope.
var selfEnforcedKeyOps = map[string]bool{
"POST /api/v2/tailnet/{tailnet}/keys": true,
"GET /api/v2/tailnet/{tailnet}/keys": true,
"GET /api/v2/tailnet/{tailnet}/keys/{keyId}": true,
"DELETE /api/v2/tailnet/{tailnet}/keys/{keyId}": true,
}
// TestEveryAuthenticatedOperationDeclaresScope is the structural guarantee that no
// v2 operation ships unprotected: any operation that requires authentication
// (non-empty Security) must either declare a required scope via requireScope, or
// be one of the keyType-multiplexed keys operations that self-enforce. A new
// operation added without scope protection fails this test.
func TestEveryAuthenticatedOperationDeclaresScope(t *testing.T) {
api := NewAPI(chi.NewMux(), Backend{})
for path, item := range api.OpenAPI().Paths {
for method, op := range humaOperations(item) {
if op == nil || len(op.Security) == 0 {
continue // unregistered method or a public operation
}
key := method + " " + path
if selfEnforcedKeyOps[key] {
continue
}
if _, ok := op.Metadata[scopeMetaKey].(scope.Scope); !ok {
t.Errorf("operation %q is authenticated but declares no required scope; "+
"wrap it in requireScope, or add it to selfEnforcedKeyOps if it "+
"authorizes inside the handler", key)
}
}
}
}
func humaOperations(item *huma.PathItem) map[string]*huma.Operation {
return map[string]*huma.Operation{
"GET": item.Get,
"POST": item.Post,
"PUT": item.Put,
"DELETE": item.Delete,
"PATCH": item.Patch,
}
}