Add Upstreams options struct with validation
This commit is contained in:
		
							parent
							
								
									fb1bef2757
								
							
						
					
					
						commit
						b6b5194190
					
				|  | @ -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"` | ||||||
|  | } | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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}, | ||||||
|  | 		}), | ||||||
|  | 	) | ||||||
|  | }) | ||||||
|  | @ -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…
	
		Reference in New Issue