mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-04-23 12:18:50 +02:00
Add Upstreams options struct with validation
This commit is contained in:
parent
fb1bef2757
commit
b6b5194190
60
pkg/apis/options/upstreams.go
Normal file
60
pkg/apis/options/upstreams.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Upstreams is a collection of definitions for upstream servers.
|
||||||
|
type Upstreams []Upstream
|
||||||
|
|
||||||
|
// Upstream represents the configuration for an upstream server.
|
||||||
|
// Requests will be proxied to this upstream if the path matches the request path.
|
||||||
|
type Upstream struct {
|
||||||
|
// ID should be a unique identifier for the upstream.
|
||||||
|
// This value is required for all upstreams.
|
||||||
|
ID string `json:"id"`
|
||||||
|
|
||||||
|
// Path is used to map requests to the upstream server.
|
||||||
|
// The closest match will take precedence and all Paths must be unique.
|
||||||
|
Path string `json:"path"`
|
||||||
|
|
||||||
|
// The URI of the upstream server. This may be an HTTP(S) server of a File
|
||||||
|
// based URL. It may include a path, in which case all requests will be served
|
||||||
|
// under that path.
|
||||||
|
// Eg:
|
||||||
|
// - http://localhost:8080
|
||||||
|
// - https://service.localhost
|
||||||
|
// - https://service.localhost/path
|
||||||
|
// - file://host/path
|
||||||
|
// If the URI's path is "/base" and the incoming request was for "/dir",
|
||||||
|
// the upstream request will be for "/base/dir".
|
||||||
|
URI string `json:"uri"`
|
||||||
|
|
||||||
|
// InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.
|
||||||
|
// This option is insecure and will allow potential Man-In-The-Middle attacks
|
||||||
|
// betweem OAuth2 Proxy and the usptream server.
|
||||||
|
// Defaults to false.
|
||||||
|
InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify"`
|
||||||
|
|
||||||
|
// Static will make all requests to this upstream have a static response.
|
||||||
|
// The response will have a body of "Authenticated" and a response code
|
||||||
|
// matching StaticCode.
|
||||||
|
// If StaticCode is not set, the response will return a 200 response.
|
||||||
|
Static bool `json:"static"`
|
||||||
|
|
||||||
|
// StaticCode determines the response code for the Static response.
|
||||||
|
// This option can only be used with Static enabled.
|
||||||
|
StaticCode *int `json:"staticCode,omitempty"`
|
||||||
|
|
||||||
|
// FlushInterval is the period between flushing the response buffer when
|
||||||
|
// streaming response from the upstream.
|
||||||
|
// Defaults to 1 second.
|
||||||
|
FlushInterval *time.Duration `json:"flushInterval,omitempty"`
|
||||||
|
|
||||||
|
// PassHostHeader determines whether the request host header should be proxied
|
||||||
|
// to the upstream server.
|
||||||
|
// Defaults to true.
|
||||||
|
PassHostHeader bool `json:"passHostHeader"`
|
||||||
|
|
||||||
|
// ProxyWebSockets enables proxying of websockets to upstream servers
|
||||||
|
// Defaults to true.
|
||||||
|
ProxyWebSockets bool `json:"proxyWebSockets"`
|
||||||
|
}
|
113
pkg/validation/upstreams.go
Normal file
113
pkg/validation/upstreams.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateUpstreams(upstreams options.Upstreams) []string {
|
||||||
|
msgs := []string{}
|
||||||
|
ids := make(map[string]struct{})
|
||||||
|
paths := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, upstream := range upstreams {
|
||||||
|
msgs = append(msgs, validateUpstream(upstream, ids, paths)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateUpstream validates that the upstream has valid options and that
|
||||||
|
// the ids and paths are unique across all options
|
||||||
|
func validateUpstream(upstream options.Upstream, ids, paths map[string]struct{}) []string {
|
||||||
|
msgs := []string{}
|
||||||
|
|
||||||
|
if upstream.ID == "" {
|
||||||
|
msgs = append(msgs, "upstream has empty id: ids are required for all upstreams")
|
||||||
|
}
|
||||||
|
if upstream.Path == "" {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has empty path: paths are required for all upstreams", upstream.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure upstream IDs are unique
|
||||||
|
if _, ok := ids[upstream.ID]; ok {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("multiple upstreams found with id %q: upstream ids must be unique", upstream.ID))
|
||||||
|
}
|
||||||
|
ids[upstream.ID] = struct{}{}
|
||||||
|
|
||||||
|
// Ensure upstream Paths are unique
|
||||||
|
if _, ok := paths[upstream.Path]; ok {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("multiple upstreams found with path %q: upstream paths must be unique", upstream.Path))
|
||||||
|
}
|
||||||
|
paths[upstream.Path] = struct{}{}
|
||||||
|
|
||||||
|
msgs = append(msgs, validateUpstreamURI(upstream)...)
|
||||||
|
msgs = append(msgs, validateStaticUpstream(upstream)...)
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateStaticUpstream checks that the StaticCode is only set when Static
|
||||||
|
// is set, and that any options that do not make sense for a static upstream
|
||||||
|
// are not set.
|
||||||
|
func validateStaticUpstream(upstream options.Upstream) []string {
|
||||||
|
msgs := []string{}
|
||||||
|
|
||||||
|
if !upstream.Static && upstream.StaticCode != nil {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has staticCode (%d), but is not a static upstream, set 'static' for a static response", upstream.ID, *upstream.StaticCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks after this only make sense when the upstream is static
|
||||||
|
if !upstream.Static {
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
if upstream.URI != "" {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has uri, but is a static upstream, this will have no effect.", upstream.ID))
|
||||||
|
}
|
||||||
|
if upstream.InsecureSkipTLSVerify {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has insecureSkipTLSVerify, but is a static upstream, this will have no effect.", upstream.ID))
|
||||||
|
}
|
||||||
|
if upstream.FlushInterval != nil && *upstream.FlushInterval != time.Second {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has flushInterval, but is a static upstream, this will have no effect.", upstream.ID))
|
||||||
|
}
|
||||||
|
if !upstream.PassHostHeader {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has passHostHeader, but is a static upstream, this will have no effect.", upstream.ID))
|
||||||
|
}
|
||||||
|
if !upstream.ProxyWebSockets {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has proxyWebSockets, but is a static upstream, this will have no effect.", upstream.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateUpstreamURI(upstream options.Upstream) []string {
|
||||||
|
msgs := []string{}
|
||||||
|
|
||||||
|
if !upstream.Static && upstream.URI == "" {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has empty uri: uris are required for all non-static upstreams", upstream.ID))
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks after this only make sense the upstream is not static
|
||||||
|
if upstream.Static {
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(upstream.URI)
|
||||||
|
if err != nil {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has invalid uri: %v", upstream.ID, err))
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "http", "https", "file":
|
||||||
|
// Valid, do nothing
|
||||||
|
default:
|
||||||
|
msgs = append(msgs, fmt.Sprintf("upstream %q has invalid scheme: %q", upstream.ID, u.Scheme))
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgs
|
||||||
|
}
|
191
pkg/validation/upstreams_test.go
Normal file
191
pkg/validation/upstreams_test.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/ginkgo/extensions/table"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Upstreams", func() {
|
||||||
|
type validateUpstreamTableInput struct {
|
||||||
|
upstreams options.Upstreams
|
||||||
|
errStrings []string
|
||||||
|
}
|
||||||
|
|
||||||
|
flushInterval := 5 * time.Second
|
||||||
|
staticCode200 := 200
|
||||||
|
|
||||||
|
validHTTPUpstream := options.Upstream{
|
||||||
|
ID: "validHTTPUpstream",
|
||||||
|
Path: "/validHTTPUpstream",
|
||||||
|
URI: "http://localhost:8080",
|
||||||
|
}
|
||||||
|
validStaticUpstream := options.Upstream{
|
||||||
|
ID: "validStaticUpstream",
|
||||||
|
Path: "/validStaticUpstream",
|
||||||
|
Static: true,
|
||||||
|
PassHostHeader: true, // This would normally be defaulted
|
||||||
|
ProxyWebSockets: true, // this would normally be defaulted
|
||||||
|
}
|
||||||
|
validFileUpstream := options.Upstream{
|
||||||
|
ID: "validFileUpstream",
|
||||||
|
Path: "/validFileUpstream",
|
||||||
|
URI: "file://var/lib/foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyIDMsg := "upstream has empty id: ids are required for all upstreams"
|
||||||
|
emptyPathMsg := "upstream \"foo\" has empty path: paths are required for all upstreams"
|
||||||
|
emptyURIMsg := "upstream \"foo\" has empty uri: uris are required for all non-static upstreams"
|
||||||
|
invalidURIMsg := "upstream \"foo\" has invalid uri: parse \":\": missing protocol scheme"
|
||||||
|
invalidURISchemeMsg := "upstream \"foo\" has invalid scheme: \"ftp\""
|
||||||
|
staticWithURIMsg := "upstream \"foo\" has uri, but is a static upstream, this will have no effect."
|
||||||
|
staticWithInsecureMsg := "upstream \"foo\" has insecureSkipTLSVerify, but is a static upstream, this will have no effect."
|
||||||
|
staticWithFlushIntervalMsg := "upstream \"foo\" has flushInterval, but is a static upstream, this will have no effect."
|
||||||
|
staticWithPassHostHeaderMsg := "upstream \"foo\" has passHostHeader, but is a static upstream, this will have no effect."
|
||||||
|
staticWithProxyWebSocketsMsg := "upstream \"foo\" has proxyWebSockets, but is a static upstream, this will have no effect."
|
||||||
|
multipleIDsMsg := "multiple upstreams found with id \"foo\": upstream ids must be unique"
|
||||||
|
multiplePathsMsg := "multiple upstreams found with path \"/foo\": upstream paths must be unique"
|
||||||
|
staticCodeMsg := "upstream \"foo\" has staticCode (200), but is not a static upstream, set 'static' for a static response"
|
||||||
|
|
||||||
|
DescribeTable("validateUpstreams",
|
||||||
|
func(o *validateUpstreamTableInput) {
|
||||||
|
Expect(validateUpstreams(o.upstreams)).To(ConsistOf(o.errStrings))
|
||||||
|
},
|
||||||
|
Entry("with no upstreams", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{},
|
||||||
|
errStrings: []string{},
|
||||||
|
}),
|
||||||
|
Entry("with valid upstreams", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
validHTTPUpstream,
|
||||||
|
validStaticUpstream,
|
||||||
|
validFileUpstream,
|
||||||
|
},
|
||||||
|
errStrings: []string{},
|
||||||
|
}),
|
||||||
|
Entry("with an empty ID", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "",
|
||||||
|
Path: "/foo",
|
||||||
|
URI: "http://localhost:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{emptyIDMsg},
|
||||||
|
}),
|
||||||
|
Entry("with an empty Path", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Path: "",
|
||||||
|
URI: "http://localhost:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{emptyPathMsg},
|
||||||
|
}),
|
||||||
|
Entry("with an empty Path", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Path: "",
|
||||||
|
URI: "http://localhost:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{emptyPathMsg},
|
||||||
|
}),
|
||||||
|
Entry("with an empty URI", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Path: "/foo",
|
||||||
|
URI: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{emptyURIMsg},
|
||||||
|
}),
|
||||||
|
Entry("with an invalid URI", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Path: "/foo",
|
||||||
|
URI: ":",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{invalidURIMsg},
|
||||||
|
}),
|
||||||
|
Entry("with an invalid URI scheme", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Path: "/foo",
|
||||||
|
URI: "ftp://foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{invalidURISchemeMsg},
|
||||||
|
}),
|
||||||
|
Entry("with a static upstream and invalid optons", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Path: "/foo",
|
||||||
|
URI: "ftp://foo",
|
||||||
|
Static: true,
|
||||||
|
FlushInterval: &flushInterval,
|
||||||
|
PassHostHeader: false,
|
||||||
|
ProxyWebSockets: false,
|
||||||
|
InsecureSkipTLSVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{
|
||||||
|
staticWithURIMsg,
|
||||||
|
staticWithInsecureMsg,
|
||||||
|
staticWithFlushIntervalMsg,
|
||||||
|
staticWithPassHostHeaderMsg,
|
||||||
|
staticWithProxyWebSocketsMsg,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Entry("with duplicate IDs", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Path: "/foo1",
|
||||||
|
URI: "http://foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Path: "/foo2",
|
||||||
|
URI: "http://foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{multipleIDsMsg},
|
||||||
|
}),
|
||||||
|
Entry("with duplicate Paths", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "foo1",
|
||||||
|
Path: "/foo",
|
||||||
|
URI: "http://foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "foo2",
|
||||||
|
Path: "/foo",
|
||||||
|
URI: "http://foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{multiplePathsMsg},
|
||||||
|
}),
|
||||||
|
Entry("when a static code is supplied without static", &validateUpstreamTableInput{
|
||||||
|
upstreams: options.Upstreams{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Path: "/foo",
|
||||||
|
StaticCode: &staticCode200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errStrings: []string{emptyURIMsg, staticCodeMsg},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
16
pkg/validation/validation_suite_test.go
Normal file
16
pkg/validation/validation_suite_test.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidationSuite(t *testing.T) {
|
||||||
|
logger.SetOutput(GinkgoWriter)
|
||||||
|
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Validation Suite")
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user