git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add Upstreams options struct with validation
commitb6b5194190e9a8fd04bb4077603bf8d35caeae68 authorJoel Speed <joel.speed@hotmail.co.uk> dateMon 25 May 2020, 17:20 (6 years ago) parentfb1bef2
4 files changed+380
pkg/apis/options/upstreams.go+60 pkg/validation/upstreams.go+113 pkg/validation/upstreams_test.go+191 pkg/validation/validation_suite_test.go+16
pkg/apis/options/upstreams.gonew file+60
@@ -0,0 +1,60 @@
1+package options
2+
3+import "time"
4+
5+// Upstreams is a collection of definitions for upstream servers.
6+type Upstreams []Upstream
7+
8+// Upstream represents the configuration for an upstream server.
9+// Requests will be proxied to this upstream if the path matches the request path.
10+type Upstream struct {
11+ // ID should be a unique identifier for the upstream.
12+ // This value is required for all upstreams.
13+ ID string `json:"id"`
14+
15+ // Path is used to map requests to the upstream server.
16+ // The closest match will take precedence and all Paths must be unique.
17+ Path string `json:"path"`
18+
19+ // The URI of the upstream server. This may be an HTTP(S) server of a File
20+ // based URL. It may include a path, in which case all requests will be served
21+ // under that path.
22+ // Eg:
23+ // - http://localhost:8080
24+ // - https://service.localhost
25+ // - https://service.localhost/path
26+ // - file://host/path
27+ // If the URI's path is "/base" and the incoming request was for "/dir",
28+ // the upstream request will be for "/base/dir".
29+ URI string `json:"uri"`
30+
31+ // InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.
32+ // This option is insecure and will allow potential Man-In-The-Middle attacks
33+ // betweem OAuth2 Proxy and the usptream server.
34+ // Defaults to false.
35+ InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify"`
36+
37+ // Static will make all requests to this upstream have a static response.
38+ // The response will have a body of "Authenticated" and a response code
39+ // matching StaticCode.
40+ // If StaticCode is not set, the response will return a 200 response.
41+ Static bool `json:"static"`
42+
43+ // StaticCode determines the response code for the Static response.
44+ // This option can only be used with Static enabled.
45+ StaticCode *int `json:"staticCode,omitempty"`
46+
47+ // FlushInterval is the period between flushing the response buffer when
48+ // streaming response from the upstream.
49+ // Defaults to 1 second.
50+ FlushInterval *time.Duration `json:"flushInterval,omitempty"`
51+
52+ // PassHostHeader determines whether the request host header should be proxied
53+ // to the upstream server.
54+ // Defaults to true.
55+ PassHostHeader bool `json:"passHostHeader"`
56+
57+ // ProxyWebSockets enables proxying of websockets to upstream servers
58+ // Defaults to true.
59+ ProxyWebSockets bool `json:"proxyWebSockets"`
60+}
pkg/validation/upstreams.gonew file+113
@@ -0,0 +1,113 @@
1+package validation
2+
3+import (
4+ "fmt"
5+ "net/url"
6+ "time"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
9+)
10+
11+func validateUpstreams(upstreams options.Upstreams) []string {
12+ msgs := []string{}
13+ ids := make(map[string]struct{})
14+ paths := make(map[string]struct{})
15+
16+ for _, upstream := range upstreams {
17+ msgs = append(msgs, validateUpstream(upstream, ids, paths)...)
18+ }
19+
20+ return msgs
21+}
22+
23+// validateUpstream validates that the upstream has valid options and that
24+// the ids and paths are unique across all options
25+func validateUpstream(upstream options.Upstream, ids, paths map[string]struct{}) []string {
26+ msgs := []string{}
27+
28+ if upstream.ID == "" {
29+ msgs = append(msgs, "upstream has empty id: ids are required for all upstreams")
30+ }
31+ if upstream.Path == "" {
32+ msgs = append(msgs, fmt.Sprintf("upstream %q has empty path: paths are required for all upstreams", upstream.ID))
33+ }
34+
35+ // Ensure upstream IDs are unique
36+ if _, ok := ids[upstream.ID]; ok {
37+ msgs = append(msgs, fmt.Sprintf("multiple upstreams found with id %q: upstream ids must be unique", upstream.ID))
38+ }
39+ ids[upstream.ID] = struct{}{}
40+
41+ // Ensure upstream Paths are unique
42+ if _, ok := paths[upstream.Path]; ok {
43+ msgs = append(msgs, fmt.Sprintf("multiple upstreams found with path %q: upstream paths must be unique", upstream.Path))
44+ }
45+ paths[upstream.Path] = struct{}{}
46+
47+ msgs = append(msgs, validateUpstreamURI(upstream)...)
48+ msgs = append(msgs, validateStaticUpstream(upstream)...)
49+ return msgs
50+}
51+
52+// validateStaticUpstream checks that the StaticCode is only set when Static
53+// is set, and that any options that do not make sense for a static upstream
54+// are not set.
55+func validateStaticUpstream(upstream options.Upstream) []string {
56+ msgs := []string{}
57+
58+ if !upstream.Static && upstream.StaticCode != nil {
59+ 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))
60+ }
61+
62+ // Checks after this only make sense when the upstream is static
63+ if !upstream.Static {
64+ return msgs
65+ }
66+
67+ if upstream.URI != "" {
68+ msgs = append(msgs, fmt.Sprintf("upstream %q has uri, but is a static upstream, this will have no effect.", upstream.ID))
69+ }
70+ if upstream.InsecureSkipTLSVerify {
71+ msgs = append(msgs, fmt.Sprintf("upstream %q has insecureSkipTLSVerify, but is a static upstream, this will have no effect.", upstream.ID))
72+ }
73+ if upstream.FlushInterval != nil && *upstream.FlushInterval != time.Second {
74+ msgs = append(msgs, fmt.Sprintf("upstream %q has flushInterval, but is a static upstream, this will have no effect.", upstream.ID))
75+ }
76+ if !upstream.PassHostHeader {
77+ msgs = append(msgs, fmt.Sprintf("upstream %q has passHostHeader, but is a static upstream, this will have no effect.", upstream.ID))
78+ }
79+ if !upstream.ProxyWebSockets {
80+ msgs = append(msgs, fmt.Sprintf("upstream %q has proxyWebSockets, but is a static upstream, this will have no effect.", upstream.ID))
81+ }
82+
83+ return msgs
84+}
85+
86+func validateUpstreamURI(upstream options.Upstream) []string {
87+ msgs := []string{}
88+
89+ if !upstream.Static && upstream.URI == "" {
90+ msgs = append(msgs, fmt.Sprintf("upstream %q has empty uri: uris are required for all non-static upstreams", upstream.ID))
91+ return msgs
92+ }
93+
94+ // Checks after this only make sense the upstream is not static
95+ if upstream.Static {
96+ return msgs
97+ }
98+
99+ u, err := url.Parse(upstream.URI)
100+ if err != nil {
101+ msgs = append(msgs, fmt.Sprintf("upstream %q has invalid uri: %v", upstream.ID, err))
102+ return msgs
103+ }
104+
105+ switch u.Scheme {
106+ case "http", "https", "file":
107+ // Valid, do nothing
108+ default:
109+ msgs = append(msgs, fmt.Sprintf("upstream %q has invalid scheme: %q", upstream.ID, u.Scheme))
110+ }
111+
112+ return msgs
113+}
pkg/validation/upstreams_test.gonew file+191
@@ -0,0 +1,191 @@
1+package validation
2+
3+import (
4+ "time"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/ginkgo/extensions/table"
9+ . "github.com/onsi/gomega"
10+)
11+
12+var _ = Describe("Upstreams", func() {
13+ type validateUpstreamTableInput struct {
14+ upstreams options.Upstreams
15+ errStrings []string
16+ }
17+
18+ flushInterval := 5 * time.Second
19+ staticCode200 := 200
20+
21+ validHTTPUpstream := options.Upstream{
22+ ID: "validHTTPUpstream",
23+ Path: "/validHTTPUpstream",
24+ URI: "http://localhost:8080",
25+ }
26+ validStaticUpstream := options.Upstream{
27+ ID: "validStaticUpstream",
28+ Path: "/validStaticUpstream",
29+ Static: true,
30+ PassHostHeader: true, // This would normally be defaulted
31+ ProxyWebSockets: true, // this would normally be defaulted
32+ }
33+ validFileUpstream := options.Upstream{
34+ ID: "validFileUpstream",
35+ Path: "/validFileUpstream",
36+ URI: "file://var/lib/foo",
37+ }
38+
39+ emptyIDMsg := "upstream has empty id: ids are required for all upstreams"
40+ emptyPathMsg := "upstream \"foo\" has empty path: paths are required for all upstreams"
41+ emptyURIMsg := "upstream \"foo\" has empty uri: uris are required for all non-static upstreams"
42+ invalidURIMsg := "upstream \"foo\" has invalid uri: parse \":\": missing protocol scheme"
43+ invalidURISchemeMsg := "upstream \"foo\" has invalid scheme: \"ftp\""
44+ staticWithURIMsg := "upstream \"foo\" has uri, but is a static upstream, this will have no effect."
45+ staticWithInsecureMsg := "upstream \"foo\" has insecureSkipTLSVerify, but is a static upstream, this will have no effect."
46+ staticWithFlushIntervalMsg := "upstream \"foo\" has flushInterval, but is a static upstream, this will have no effect."
47+ staticWithPassHostHeaderMsg := "upstream \"foo\" has passHostHeader, but is a static upstream, this will have no effect."
48+ staticWithProxyWebSocketsMsg := "upstream \"foo\" has proxyWebSockets, but is a static upstream, this will have no effect."
49+ multipleIDsMsg := "multiple upstreams found with id \"foo\": upstream ids must be unique"
50+ multiplePathsMsg := "multiple upstreams found with path \"/foo\": upstream paths must be unique"
51+ staticCodeMsg := "upstream \"foo\" has staticCode (200), but is not a static upstream, set 'static' for a static response"
52+
53+ DescribeTable("validateUpstreams",
54+ func(o *validateUpstreamTableInput) {
55+ Expect(validateUpstreams(o.upstreams)).To(ConsistOf(o.errStrings))
56+ },
57+ Entry("with no upstreams", &validateUpstreamTableInput{
58+ upstreams: options.Upstreams{},
59+ errStrings: []string{},
60+ }),
61+ Entry("with valid upstreams", &validateUpstreamTableInput{
62+ upstreams: options.Upstreams{
63+ validHTTPUpstream,
64+ validStaticUpstream,
65+ validFileUpstream,
66+ },
67+ errStrings: []string{},
68+ }),
69+ Entry("with an empty ID", &validateUpstreamTableInput{
70+ upstreams: options.Upstreams{
71+ {
72+ ID: "",
73+ Path: "/foo",
74+ URI: "http://localhost:8080",
75+ },
76+ },
77+ errStrings: []string{emptyIDMsg},
78+ }),
79+ Entry("with an empty Path", &validateUpstreamTableInput{
80+ upstreams: options.Upstreams{
81+ {
82+ ID: "foo",
83+ Path: "",
84+ URI: "http://localhost:8080",
85+ },
86+ },
87+ errStrings: []string{emptyPathMsg},
88+ }),
89+ Entry("with an empty Path", &validateUpstreamTableInput{
90+ upstreams: options.Upstreams{
91+ {
92+ ID: "foo",
93+ Path: "",
94+ URI: "http://localhost:8080",
95+ },
96+ },
97+ errStrings: []string{emptyPathMsg},
98+ }),
99+ Entry("with an empty URI", &validateUpstreamTableInput{
100+ upstreams: options.Upstreams{
101+ {
102+ ID: "foo",
103+ Path: "/foo",
104+ URI: "",
105+ },
106+ },
107+ errStrings: []string{emptyURIMsg},
108+ }),
109+ Entry("with an invalid URI", &validateUpstreamTableInput{
110+ upstreams: options.Upstreams{
111+ {
112+ ID: "foo",
113+ Path: "/foo",
114+ URI: ":",
115+ },
116+ },
117+ errStrings: []string{invalidURIMsg},
118+ }),
119+ Entry("with an invalid URI scheme", &validateUpstreamTableInput{
120+ upstreams: options.Upstreams{
121+ {
122+ ID: "foo",
123+ Path: "/foo",
124+ URI: "ftp://foo",
125+ },
126+ },
127+ errStrings: []string{invalidURISchemeMsg},
128+ }),
129+ Entry("with a static upstream and invalid optons", &validateUpstreamTableInput{
130+ upstreams: options.Upstreams{
131+ {
132+ ID: "foo",
133+ Path: "/foo",
134+ URI: "ftp://foo",
135+ Static: true,
136+ FlushInterval: &flushInterval,
137+ PassHostHeader: false,
138+ ProxyWebSockets: false,
139+ InsecureSkipTLSVerify: true,
140+ },
141+ },
142+ errStrings: []string{
143+ staticWithURIMsg,
144+ staticWithInsecureMsg,
145+ staticWithFlushIntervalMsg,
146+ staticWithPassHostHeaderMsg,
147+ staticWithProxyWebSocketsMsg,
148+ },
149+ }),
150+ Entry("with duplicate IDs", &validateUpstreamTableInput{
151+ upstreams: options.Upstreams{
152+ {
153+ ID: "foo",
154+ Path: "/foo1",
155+ URI: "http://foo",
156+ },
157+ {
158+ ID: "foo",
159+ Path: "/foo2",
160+ URI: "http://foo",
161+ },
162+ },
163+ errStrings: []string{multipleIDsMsg},
164+ }),
165+ Entry("with duplicate Paths", &validateUpstreamTableInput{
166+ upstreams: options.Upstreams{
167+ {
168+ ID: "foo1",
169+ Path: "/foo",
170+ URI: "http://foo",
171+ },
172+ {
173+ ID: "foo2",
174+ Path: "/foo",
175+ URI: "http://foo",
176+ },
177+ },
178+ errStrings: []string{multiplePathsMsg},
179+ }),
180+ Entry("when a static code is supplied without static", &validateUpstreamTableInput{
181+ upstreams: options.Upstreams{
182+ {
183+ ID: "foo",
184+ Path: "/foo",
185+ StaticCode: &staticCode200,
186+ },
187+ },
188+ errStrings: []string{emptyURIMsg, staticCodeMsg},
189+ }),
190+ )
191+})
pkg/validation/validation_suite_test.gonew file+16
@@ -0,0 +1,16 @@
1+package validation
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+func TestValidationSuite(t *testing.T) {
12+ logger.SetOutput(GinkgoWriter)
13+
14+ RegisterFailHandler(Fail)
15+ RunSpecs(t, "Validation Suite")
16+}