mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-11-30 16:46:27 +00:00
489 lines
14 KiB
Go
489 lines
14 KiB
Go
|
// Copyright 2015 go-swagger maintainers
|
||
|
//
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
// you may not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
// See the License for the specific language governing permissions and
|
||
|
// limitations under the License.
|
||
|
|
||
|
package middleware
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
fpath "path"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/go-openapi/runtime/security"
|
||
|
"github.com/go-openapi/swag"
|
||
|
|
||
|
"github.com/go-openapi/analysis"
|
||
|
"github.com/go-openapi/errors"
|
||
|
"github.com/go-openapi/loads"
|
||
|
"github.com/go-openapi/spec"
|
||
|
"github.com/go-openapi/strfmt"
|
||
|
|
||
|
"github.com/go-openapi/runtime"
|
||
|
"github.com/go-openapi/runtime/middleware/denco"
|
||
|
)
|
||
|
|
||
|
// RouteParam is a object to capture route params in a framework agnostic way.
|
||
|
// implementations of the muxer should use these route params to communicate with the
|
||
|
// swagger framework
|
||
|
type RouteParam struct {
|
||
|
Name string
|
||
|
Value string
|
||
|
}
|
||
|
|
||
|
// RouteParams the collection of route params
|
||
|
type RouteParams []RouteParam
|
||
|
|
||
|
// Get gets the value for the route param for the specified key
|
||
|
func (r RouteParams) Get(name string) string {
|
||
|
vv, _, _ := r.GetOK(name)
|
||
|
if len(vv) > 0 {
|
||
|
return vv[len(vv)-1]
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
// GetOK gets the value but also returns booleans to indicate if a key or value
|
||
|
// is present. This aids in validation and satisfies an interface in use there
|
||
|
//
|
||
|
// The returned values are: data, has key, has value
|
||
|
func (r RouteParams) GetOK(name string) ([]string, bool, bool) {
|
||
|
for _, p := range r {
|
||
|
if p.Name == name {
|
||
|
return []string{p.Value}, true, p.Value != ""
|
||
|
}
|
||
|
}
|
||
|
return nil, false, false
|
||
|
}
|
||
|
|
||
|
// NewRouter creates a new context aware router middleware
|
||
|
func NewRouter(ctx *Context, next http.Handler) http.Handler {
|
||
|
if ctx.router == nil {
|
||
|
ctx.router = DefaultRouter(ctx.spec, ctx.api)
|
||
|
}
|
||
|
|
||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||
|
if _, rCtx, ok := ctx.RouteInfo(r); ok {
|
||
|
next.ServeHTTP(rw, rCtx)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Not found, check if it exists in the other methods first
|
||
|
if others := ctx.AllowedMethods(r); len(others) > 0 {
|
||
|
ctx.Respond(rw, r, ctx.analyzer.RequiredProduces(), nil, errors.MethodNotAllowed(r.Method, others))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
ctx.Respond(rw, r, ctx.analyzer.RequiredProduces(), nil, errors.NotFound("path %s was not found", r.URL.EscapedPath()))
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// RoutableAPI represents an interface for things that can serve
|
||
|
// as a provider of implementations for the swagger router
|
||
|
type RoutableAPI interface {
|
||
|
HandlerFor(string, string) (http.Handler, bool)
|
||
|
ServeErrorFor(string) func(http.ResponseWriter, *http.Request, error)
|
||
|
ConsumersFor([]string) map[string]runtime.Consumer
|
||
|
ProducersFor([]string) map[string]runtime.Producer
|
||
|
AuthenticatorsFor(map[string]spec.SecurityScheme) map[string]runtime.Authenticator
|
||
|
Authorizer() runtime.Authorizer
|
||
|
Formats() strfmt.Registry
|
||
|
DefaultProduces() string
|
||
|
DefaultConsumes() string
|
||
|
}
|
||
|
|
||
|
// Router represents a swagger aware router
|
||
|
type Router interface {
|
||
|
Lookup(method, path string) (*MatchedRoute, bool)
|
||
|
OtherMethods(method, path string) []string
|
||
|
}
|
||
|
|
||
|
type defaultRouteBuilder struct {
|
||
|
spec *loads.Document
|
||
|
analyzer *analysis.Spec
|
||
|
api RoutableAPI
|
||
|
records map[string][]denco.Record
|
||
|
}
|
||
|
|
||
|
type defaultRouter struct {
|
||
|
spec *loads.Document
|
||
|
routers map[string]*denco.Router
|
||
|
}
|
||
|
|
||
|
func newDefaultRouteBuilder(spec *loads.Document, api RoutableAPI) *defaultRouteBuilder {
|
||
|
return &defaultRouteBuilder{
|
||
|
spec: spec,
|
||
|
analyzer: analysis.New(spec.Spec()),
|
||
|
api: api,
|
||
|
records: make(map[string][]denco.Record),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DefaultRouter creates a default implemenation of the router
|
||
|
func DefaultRouter(spec *loads.Document, api RoutableAPI) Router {
|
||
|
builder := newDefaultRouteBuilder(spec, api)
|
||
|
if spec != nil {
|
||
|
for method, paths := range builder.analyzer.Operations() {
|
||
|
for path, operation := range paths {
|
||
|
fp := fpath.Join(spec.BasePath(), path)
|
||
|
debugLog("adding route %s %s %q", method, fp, operation.ID)
|
||
|
builder.AddRoute(method, fp, operation)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return builder.Build()
|
||
|
}
|
||
|
|
||
|
// RouteAuthenticator is an authenticator that can compose several authenticators together.
|
||
|
// It also knows when it contains an authenticator that allows for anonymous pass through.
|
||
|
// Contains a group of 1 or more authenticators that have a logical AND relationship
|
||
|
type RouteAuthenticator struct {
|
||
|
Authenticator map[string]runtime.Authenticator
|
||
|
Schemes []string
|
||
|
Scopes map[string][]string
|
||
|
allScopes []string
|
||
|
commonScopes []string
|
||
|
allowAnonymous bool
|
||
|
}
|
||
|
|
||
|
func (ra *RouteAuthenticator) AllowsAnonymous() bool {
|
||
|
return ra.allowAnonymous
|
||
|
}
|
||
|
|
||
|
// AllScopes returns a list of unique scopes that is the combination
|
||
|
// of all the scopes in the requirements
|
||
|
func (ra *RouteAuthenticator) AllScopes() []string {
|
||
|
return ra.allScopes
|
||
|
}
|
||
|
|
||
|
// CommonScopes returns a list of unique scopes that are common in all the
|
||
|
// scopes in the requirements
|
||
|
func (ra *RouteAuthenticator) CommonScopes() []string {
|
||
|
return ra.commonScopes
|
||
|
}
|
||
|
|
||
|
// Authenticate Authenticator interface implementation
|
||
|
func (ra *RouteAuthenticator) Authenticate(req *http.Request, route *MatchedRoute) (bool, interface{}, error) {
|
||
|
if ra.allowAnonymous {
|
||
|
route.Authenticator = ra
|
||
|
return true, nil, nil
|
||
|
}
|
||
|
// iterate in proper order
|
||
|
var lastResult interface{}
|
||
|
for _, scheme := range ra.Schemes {
|
||
|
if authenticator, ok := ra.Authenticator[scheme]; ok {
|
||
|
applies, princ, err := authenticator.Authenticate(&security.ScopedAuthRequest{
|
||
|
Request: req,
|
||
|
RequiredScopes: ra.Scopes[scheme],
|
||
|
})
|
||
|
if !applies {
|
||
|
return false, nil, nil
|
||
|
}
|
||
|
if err != nil {
|
||
|
route.Authenticator = ra
|
||
|
return true, nil, err
|
||
|
}
|
||
|
lastResult = princ
|
||
|
}
|
||
|
}
|
||
|
route.Authenticator = ra
|
||
|
return true, lastResult, nil
|
||
|
}
|
||
|
|
||
|
func stringSliceUnion(slices ...[]string) []string {
|
||
|
unique := make(map[string]struct{})
|
||
|
var result []string
|
||
|
for _, slice := range slices {
|
||
|
for _, entry := range slice {
|
||
|
if _, ok := unique[entry]; ok {
|
||
|
continue
|
||
|
}
|
||
|
unique[entry] = struct{}{}
|
||
|
result = append(result, entry)
|
||
|
}
|
||
|
}
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
func stringSliceIntersection(slices ...[]string) []string {
|
||
|
unique := make(map[string]int)
|
||
|
var intersection []string
|
||
|
|
||
|
total := len(slices)
|
||
|
var emptyCnt int
|
||
|
for _, slice := range slices {
|
||
|
if len(slice) == 0 {
|
||
|
emptyCnt++
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
for _, entry := range slice {
|
||
|
unique[entry]++
|
||
|
if unique[entry] == total-emptyCnt { // this entry appeared in all the non-empty slices
|
||
|
intersection = append(intersection, entry)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return intersection
|
||
|
}
|
||
|
|
||
|
// RouteAuthenticators represents a group of authenticators that represent a logical OR
|
||
|
type RouteAuthenticators []RouteAuthenticator
|
||
|
|
||
|
// AllowsAnonymous returns true when there is an authenticator that means optional auth
|
||
|
func (ras RouteAuthenticators) AllowsAnonymous() bool {
|
||
|
for _, ra := range ras {
|
||
|
if ra.AllowsAnonymous() {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Authenticate method implemention so this collection can be used as authenticator
|
||
|
func (ras RouteAuthenticators) Authenticate(req *http.Request, route *MatchedRoute) (bool, interface{}, error) {
|
||
|
var lastError error
|
||
|
var allowsAnon bool
|
||
|
var anonAuth RouteAuthenticator
|
||
|
|
||
|
for _, ra := range ras {
|
||
|
if ra.AllowsAnonymous() {
|
||
|
anonAuth = ra
|
||
|
allowsAnon = true
|
||
|
continue
|
||
|
}
|
||
|
applies, usr, err := ra.Authenticate(req, route)
|
||
|
if !applies || err != nil || usr == nil {
|
||
|
if err != nil {
|
||
|
lastError = err
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
return applies, usr, nil
|
||
|
}
|
||
|
|
||
|
if allowsAnon && lastError == nil {
|
||
|
route.Authenticator = &anonAuth
|
||
|
return true, nil, lastError
|
||
|
}
|
||
|
return lastError != nil, nil, lastError
|
||
|
}
|
||
|
|
||
|
type routeEntry struct {
|
||
|
PathPattern string
|
||
|
BasePath string
|
||
|
Operation *spec.Operation
|
||
|
Consumes []string
|
||
|
Consumers map[string]runtime.Consumer
|
||
|
Produces []string
|
||
|
Producers map[string]runtime.Producer
|
||
|
Parameters map[string]spec.Parameter
|
||
|
Handler http.Handler
|
||
|
Formats strfmt.Registry
|
||
|
Binder *UntypedRequestBinder
|
||
|
Authenticators RouteAuthenticators
|
||
|
Authorizer runtime.Authorizer
|
||
|
}
|
||
|
|
||
|
// MatchedRoute represents the route that was matched in this request
|
||
|
type MatchedRoute struct {
|
||
|
routeEntry
|
||
|
Params RouteParams
|
||
|
Consumer runtime.Consumer
|
||
|
Producer runtime.Producer
|
||
|
Authenticator *RouteAuthenticator
|
||
|
}
|
||
|
|
||
|
// HasAuth returns true when the route has a security requirement defined
|
||
|
func (m *MatchedRoute) HasAuth() bool {
|
||
|
return len(m.Authenticators) > 0
|
||
|
}
|
||
|
|
||
|
// NeedsAuth returns true when the request still
|
||
|
// needs to perform authentication
|
||
|
func (m *MatchedRoute) NeedsAuth() bool {
|
||
|
return m.HasAuth() && m.Authenticator == nil
|
||
|
}
|
||
|
|
||
|
func (d *defaultRouter) Lookup(method, path string) (*MatchedRoute, bool) {
|
||
|
mth := strings.ToUpper(method)
|
||
|
debugLog("looking up route for %s %s", method, path)
|
||
|
if Debug {
|
||
|
if len(d.routers) == 0 {
|
||
|
debugLog("there are no known routers")
|
||
|
}
|
||
|
for meth := range d.routers {
|
||
|
debugLog("got a router for %s", meth)
|
||
|
}
|
||
|
}
|
||
|
if router, ok := d.routers[mth]; ok {
|
||
|
if m, rp, ok := router.Lookup(fpath.Clean(path)); ok && m != nil {
|
||
|
if entry, ok := m.(*routeEntry); ok {
|
||
|
debugLog("found a route for %s %s with %d parameters", method, path, len(entry.Parameters))
|
||
|
var params RouteParams
|
||
|
for _, p := range rp {
|
||
|
v, err := pathUnescape(p.Value)
|
||
|
if err != nil {
|
||
|
debugLog("failed to escape %q: %v", p.Value, err)
|
||
|
v = p.Value
|
||
|
}
|
||
|
// a workaround to handle fragment/composing parameters until they are supported in denco router
|
||
|
// check if this parameter is a fragment within a path segment
|
||
|
if xpos := strings.Index(entry.PathPattern, fmt.Sprintf("{%s}", p.Name)) + len(p.Name) + 2; xpos < len(entry.PathPattern) && entry.PathPattern[xpos] != '/' {
|
||
|
// extract fragment parameters
|
||
|
ep := strings.Split(entry.PathPattern[xpos:], "/")[0]
|
||
|
pnames, pvalues := decodeCompositParams(p.Name, v, ep, nil, nil)
|
||
|
for i, pname := range pnames {
|
||
|
params = append(params, RouteParam{Name: pname, Value: pvalues[i]})
|
||
|
}
|
||
|
} else {
|
||
|
// use the parameter directly
|
||
|
params = append(params, RouteParam{Name: p.Name, Value: v})
|
||
|
}
|
||
|
}
|
||
|
return &MatchedRoute{routeEntry: *entry, Params: params}, true
|
||
|
}
|
||
|
} else {
|
||
|
debugLog("couldn't find a route by path for %s %s", method, path)
|
||
|
}
|
||
|
} else {
|
||
|
debugLog("couldn't find a route by method for %s %s", method, path)
|
||
|
}
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
func (d *defaultRouter) OtherMethods(method, path string) []string {
|
||
|
mn := strings.ToUpper(method)
|
||
|
var methods []string
|
||
|
for k, v := range d.routers {
|
||
|
if k != mn {
|
||
|
if _, _, ok := v.Lookup(fpath.Clean(path)); ok {
|
||
|
methods = append(methods, k)
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return methods
|
||
|
}
|
||
|
|
||
|
// convert swagger parameters per path segment into a denco parameter as multiple parameters per segment are not supported in denco
|
||
|
var pathConverter = regexp.MustCompile(`{(.+?)}([^/]*)`)
|
||
|
|
||
|
func decodeCompositParams(name string, value string, pattern string, names []string, values []string) ([]string, []string) {
|
||
|
pleft := strings.Index(pattern, "{")
|
||
|
names = append(names, name)
|
||
|
if pleft < 0 {
|
||
|
if strings.HasSuffix(value, pattern) {
|
||
|
values = append(values, value[:len(value)-len(pattern)])
|
||
|
} else {
|
||
|
values = append(values, "")
|
||
|
}
|
||
|
} else {
|
||
|
toskip := pattern[:pleft]
|
||
|
pright := strings.Index(pattern, "}")
|
||
|
vright := strings.Index(value, toskip)
|
||
|
if vright >= 0 {
|
||
|
values = append(values, value[:vright])
|
||
|
} else {
|
||
|
values = append(values, "")
|
||
|
value = ""
|
||
|
}
|
||
|
return decodeCompositParams(pattern[pleft+1:pright], value[vright+len(toskip):], pattern[pright+1:], names, values)
|
||
|
}
|
||
|
return names, values
|
||
|
}
|
||
|
|
||
|
func (d *defaultRouteBuilder) AddRoute(method, path string, operation *spec.Operation) {
|
||
|
mn := strings.ToUpper(method)
|
||
|
|
||
|
bp := fpath.Clean(d.spec.BasePath())
|
||
|
if len(bp) > 0 && bp[len(bp)-1] == '/' {
|
||
|
bp = bp[:len(bp)-1]
|
||
|
}
|
||
|
|
||
|
debugLog("operation: %#v", *operation)
|
||
|
if handler, ok := d.api.HandlerFor(method, strings.TrimPrefix(path, bp)); ok {
|
||
|
consumes := d.analyzer.ConsumesFor(operation)
|
||
|
produces := d.analyzer.ProducesFor(operation)
|
||
|
parameters := d.analyzer.ParamsFor(method, strings.TrimPrefix(path, bp))
|
||
|
|
||
|
// add API defaults if not part of the spec
|
||
|
if defConsumes := d.api.DefaultConsumes(); defConsumes != "" && !swag.ContainsStringsCI(consumes, defConsumes) {
|
||
|
consumes = append(consumes, defConsumes)
|
||
|
}
|
||
|
|
||
|
if defProduces := d.api.DefaultProduces(); defProduces != "" && !swag.ContainsStringsCI(produces, defProduces) {
|
||
|
produces = append(produces, defProduces)
|
||
|
}
|
||
|
|
||
|
record := denco.NewRecord(pathConverter.ReplaceAllString(path, ":$1"), &routeEntry{
|
||
|
BasePath: bp,
|
||
|
PathPattern: path,
|
||
|
Operation: operation,
|
||
|
Handler: handler,
|
||
|
Consumes: consumes,
|
||
|
Produces: produces,
|
||
|
Consumers: d.api.ConsumersFor(normalizeOffers(consumes)),
|
||
|
Producers: d.api.ProducersFor(normalizeOffers(produces)),
|
||
|
Parameters: parameters,
|
||
|
Formats: d.api.Formats(),
|
||
|
Binder: NewUntypedRequestBinder(parameters, d.spec.Spec(), d.api.Formats()),
|
||
|
Authenticators: d.buildAuthenticators(operation),
|
||
|
Authorizer: d.api.Authorizer(),
|
||
|
})
|
||
|
d.records[mn] = append(d.records[mn], record)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (d *defaultRouteBuilder) buildAuthenticators(operation *spec.Operation) RouteAuthenticators {
|
||
|
requirements := d.analyzer.SecurityRequirementsFor(operation)
|
||
|
var auths []RouteAuthenticator
|
||
|
for _, reqs := range requirements {
|
||
|
var schemes []string
|
||
|
scopes := make(map[string][]string, len(reqs))
|
||
|
var scopeSlices [][]string
|
||
|
for _, req := range reqs {
|
||
|
schemes = append(schemes, req.Name)
|
||
|
scopes[req.Name] = req.Scopes
|
||
|
scopeSlices = append(scopeSlices, req.Scopes)
|
||
|
}
|
||
|
|
||
|
definitions := d.analyzer.SecurityDefinitionsForRequirements(reqs)
|
||
|
authenticators := d.api.AuthenticatorsFor(definitions)
|
||
|
auths = append(auths, RouteAuthenticator{
|
||
|
Authenticator: authenticators,
|
||
|
Schemes: schemes,
|
||
|
Scopes: scopes,
|
||
|
allScopes: stringSliceUnion(scopeSlices...),
|
||
|
commonScopes: stringSliceIntersection(scopeSlices...),
|
||
|
allowAnonymous: len(reqs) == 1 && reqs[0].Name == "",
|
||
|
})
|
||
|
}
|
||
|
return auths
|
||
|
}
|
||
|
|
||
|
func (d *defaultRouteBuilder) Build() *defaultRouter {
|
||
|
routers := make(map[string]*denco.Router)
|
||
|
for method, records := range d.records {
|
||
|
router := denco.New()
|
||
|
_ = router.Build(records)
|
||
|
routers[method] = router
|
||
|
}
|
||
|
return &defaultRouter{
|
||
|
spec: d.spec,
|
||
|
routers: routers,
|
||
|
}
|
||
|
}
|