Files
CrimsonandGitHub 085e6a46c1 feat: Add a detailed scope check to authenticator that can be configured using an openapi file (#581)
* Add apispec package which can be used to extract info from openapi files

* Add additional check to enforce scope use based on openapi file

* Set clientID and organizationID in traces of failed auth requests

* Run go mod tidy

* Add handling to log requests to undocumented routes in control plane middleware

* Use list of methods actually in spec instead of hardcoding the methods

* Ensure that request contains all scopes needed for an endpoint
2026-03-26 13:56:52 +00:00

111 lines
3.0 KiB
Go

package apispec
import (
"net/http"
"sort"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/routers"
chi "github.com/go-chi/chi/v5"
)
// implements routers.Router using chi for path matching
// OpenAPI path templates use {param} syntax which is directly compatible with chi
type Router struct {
mux *chi.Mux
doc *openapi3.T
operations map[string]map[string]*openapi3.Operation // openapi path → METHOD → operation
methods map[string]struct{} // all HTTP methods registered in the spec
}
// NewRouter builds a Router from a parsed OpenAPI document
// gives programatic access to documented info such as oauth2 scopes
func NewRouter(doc *openapi3.T) *Router {
mux := chi.NewRouter()
ops := map[string]map[string]*openapi3.Operation{}
methods := map[string]struct{}{}
for path, item := range doc.Paths.Map() {
ops[path] = map[string]*openapi3.Operation{}
for method, op := range item.Operations() {
method = strings.ToUpper(method)
ops[path][method] = op
methods[method] = struct{}{}
mux.Method(method, path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
}
}
return &Router{mux: mux, doc: doc, operations: ops, methods: methods}
}
// FindRoute implements routers.Router interface
func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) {
rctx := chi.NewRouteContext()
if !r.mux.Match(rctx, req.Method, req.URL.Path) {
// Determine whether the path itself exists under a different method
for m := range r.methods {
if m == req.Method {
continue
}
if r.mux.Match(chi.NewRouteContext(), m, req.URL.Path) {
return nil, nil, routers.ErrMethodNotAllowed
}
}
return nil, nil, routers.ErrPathNotFound
}
pattern := rctx.RoutePattern()
pathOps, ok := r.operations[pattern]
if !ok {
return nil, nil, routers.ErrPathNotFound
}
op, ok := pathOps[req.Method]
if !ok {
return nil, nil, routers.ErrMethodNotAllowed
}
pathParams := make(map[string]string, len(rctx.URLParams.Keys))
for i, key := range rctx.URLParams.Keys {
pathParams[key] = rctx.URLParams.Values[i]
}
return &routers.Route{
Spec: r.doc,
Path: pattern,
PathItem: r.doc.Paths.Find(pattern),
Method: req.Method,
Operation: op,
}, pathParams, nil
}
func (r *Router) Oauth2Scopes() []string {
seen := map[string]struct{}{}
var result []string
for _, item := range r.doc.Paths.Map() {
for _, op := range item.Operations() {
if op == nil || op.Security == nil {
continue
}
for _, secReq := range *op.Security {
for schemeName, scopeNames := range secReq {
ref, ok := r.doc.Components.SecuritySchemes[schemeName]
if !ok || ref.Value == nil || ref.Value.Type != "oauth2" {
continue
}
for _, s := range scopeNames {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
result = append(result, s)
}
}
}
}
}
}
sort.Strings(result)
return result
}
var _ routers.Router = (*Router)(nil)