git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #907 from oauth2-proxy/alpha-config

Introduce alpha configuration option to enable testing of structured configuration

commit87c67b09a7bcb07eabcdf13c713613bf2e1bf79e authorJoel Speed <Joel.speed@hotmail.co.uk> dateTue 1 Dec 2020, 09:28 (6 years ago) parentd587030d749c11
23 files changed+662−71
CHANGELOG.md+1 contrib/local-environment/Makefile+8 contrib/local-environment/docker-compose-alpha-config.yaml+19 contrib/local-environment/oauth2-proxy-alpha-config.cfg+10 contrib/local-environment/oauth2-proxy-alpha-config.yaml+17 go.mod+1 go.sum+3 main.go+107−16 main_suite_test.go+16 main_test.go+215 oauthproxy_test.go+2−2 pkg/apis/options/alpha_options.go+16 pkg/apis/options/legacy_options.go+10−2 pkg/apis/options/legacy_options_test.go+2−3 pkg/apis/options/load.go+28 pkg/apis/options/load_test.go+192−1 pkg/apis/options/options.go−2 pkg/apis/options/util/util.go+1−4 pkg/apis/options/util/util_test.go+3−12 pkg/header/injector_test.go+7−7 pkg/validation/common.go+1−10 pkg/validation/common_test.go+1−10 pkg/validation/header_test.go+2−2
CHANGELOG.md+1
@@ -43,6 +43,7 @@
4343
4444## Changes since v6.1.1
4545
46+- [#907](https://github.com/oauth2-proxy/oauth2-proxy/pull/907) Introduce alpha configuration option to enable testing of structured configuration (@JoelSpeed)
4647- [#938](https://github.com/oauth2-proxy/oauth2-proxy/pull/938) Cleanup missed provider renaming refactor methods (@NickMeves)
4748- [#925](https://github.com/oauth2-proxy/oauth2-proxy/pull/925) Fix basic auth legacy header conversion (@JoelSpeed)
4849- [#916](https://github.com/oauth2-proxy/oauth2-proxy/pull/916) Add AlphaOptions struct to prepare for alpha config loading (@JoelSpeed)
contrib/local-environment/Makefile+8
@@ -6,6 +6,14 @@ up:
66%:
77 docker-compose $*
88
9+.PHONY: alpha-config-up
10+alpha-config-up:
11+ docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml up -d
12+
13+.PHONY: alpha-config-%
14+alpha-config-%:
15+ docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml $*
16+
917.PHONY: nginx-up
1018nginx-up:
1119 docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml up -d
contrib/local-environment/docker-compose-alpha-config.yamlnew file+19
@@ -0,0 +1,19 @@
1+# This docker-compose file can be used to bring up an example instance of oauth2-proxy
2+# for manual testing and exploration of features.
3+# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
4+# etcd for storage for Dex and HTTPBin as an example upstream.
5+# This file also uses alpha configuration when configuring OAuth2 Proxy.
6+#
7+# This file is an extension of the main compose file and must be used with it
8+# docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml <command>
9+# Alternatively:
10+# make alpha-config-<command> (eg make nginx-up, make nginx-down)
11+#
12+# Access http://localhost:4180 to initiate a login cycle
13+version: '3.0'
14+services:
15+ oauth2-proxy:
16+ command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml
17+ volumes:
18+ - "./oauth2-proxy-alpha-config.cfg:/oauth2-proxy.cfg"
19+ - "./oauth2-proxy-alpha-config.yaml:/oauth2-proxy-alpha-config.yaml"
contrib/local-environment/oauth2-proxy-alpha-config.cfgnew file+10
@@ -0,0 +1,10 @@
1+http_address="0.0.0.0:4180"
2+cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3+provider="oidc"
4+email_domains="example.com"
5+oidc_issuer_url="http://dex.localhost:4190/dex"
6+client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
7+client_id="oauth2-proxy"
8+cookie_secure="false"
9+
10+redirect_url="http://localhost:4180/oauth2/callback"
contrib/local-environment/oauth2-proxy-alpha-config.yamlnew file+17
@@ -0,0 +1,17 @@
1+upstreams:
2+ - id: httpbin
3+ path: /
4+ uri: http://httpbin
5+injectRequestHeaders:
6+- name: X-Forwarded-Groups
7+ values:
8+ - claim: groups
9+- name: X-Forwarded-User
10+ values:
11+ - claim: user
12+- name: X-Forwarded-Email
13+ values:
14+ - claim: email
15+- name: X-Forwarded-Preferred-Username
16+ values:
17+ - claim: preferred_username
go.mod+1
@@ -11,6 +11,7 @@ require (
1111 github.com/dgrijalva/jwt-go v3.2.0+incompatible
1212 github.com/frankban/quicktest v1.10.0 // indirect
1313 github.com/fsnotify/fsnotify v1.4.9
14+ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
1415 github.com/go-redis/redis/v8 v8.2.3
1516 github.com/justinas/alice v1.2.0
1617 github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa
go.sum+3
@@ -64,7 +64,10 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
6464github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
6565github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
6666github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
67+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
6768github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
69+github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
70+github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
6871github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
6972github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
7073github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
main.go+107−16
@@ -9,42 +9,45 @@ import (
99 "syscall"
1010 "time"
1111
12+ "github.com/ghodss/yaml"
1213 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1314 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1415 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation"
16+ "github.com/spf13/pflag"
1517)
1618
1719func main() {
1820 logger.SetFlags(logger.Lshortfile)
19− flagSet := options.NewFlagSet()
2021
21− config := flagSet.String("config", "", "path to config file")
22− showVersion := flagSet.Bool("version", false, "print version string")
23−
24− err := flagSet.Parse(os.Args[1:])
25− if err != nil {
26− logger.Printf("ERROR: Failed to parse flags: %v", err)
27− os.Exit(1)
28− }
22+ configFlagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ContinueOnError)
23+ config := configFlagSet.String("config", "", "path to config file")
24+ alphaConfig := configFlagSet.String("alpha-config", "", "path to alpha config file (use at your own risk - the structure in this config file may change between minor releases)")
25+ convertConfig := configFlagSet.Bool("convert-config-to-alpha", false, "if true, the proxy will load configuration as normal and convert existing configuration to the alpha config structure, and print it to stdout")
26+ showVersion := configFlagSet.Bool("version", false, "print version string")
27+ configFlagSet.Parse(os.Args[1:])
2928
3029 if *showVersion {
3130 fmt.Printf("oauth2-proxy %s (built with %s)\n", VERSION, runtime.Version())
3231 return
3332 }
3433
35− legacyOpts := options.NewLegacyOptions()
36− err = options.Load(*config, flagSet, legacyOpts)
37− if err != nil {
38− logger.Errorf("ERROR: Failed to load config: %v", err)
39− os.Exit(1)
34+ if *convertConfig && *alphaConfig != "" {
35+ logger.Fatal("cannot use alpha-config and conver-config-to-alpha together")
4036 }
4137
42− opts, err := legacyOpts.ToOptions()
38+ opts, err := loadConfiguration(*config, *alphaConfig, configFlagSet, os.Args[1:])
4339 if err != nil {
44− logger.Errorf("ERROR: Failed to convert config: %v", err)
40+ logger.Printf("ERROR: %v", err)
4541 os.Exit(1)
4642 }
4743
44+ if *convertConfig {
45+ if err := printConvertedConfig(opts); err != nil {
46+ logger.Fatalf("ERROR: could not convert config: %v", err)
47+ }
48+ return
49+ }
50+
4851 err = validation.Validate(opts)
4952 if err != nil {
5053 logger.Printf("%s", err)
@@ -74,3 +77,91 @@ func main() {
7477 }()
7578 s.ListenAndServe()
7679}
80+
81+// loadConfiguration will load in the user's configuration.
82+// It will either load the alpha configuration (if alphaConfig is given)
83+// or the legacy configuration.
84+func loadConfiguration(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
85+ if alphaConfig != "" {
86+ logger.Printf("WARNING: You are using alpha configuration. The structure in this configuration file may change without notice. You MUST remove conflicting options from your existing configuration.")
87+ return loadAlphaOptions(config, alphaConfig, extraFlags, args)
88+ }
89+ return loadLegacyOptions(config, extraFlags, args)
90+}
91+
92+// loadLegacyOptions loads the old toml options using the legacy flagset
93+// and legacy options struct.
94+func loadLegacyOptions(config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
95+ optionsFlagSet := options.NewLegacyFlagSet()
96+ optionsFlagSet.AddFlagSet(extraFlags)
97+ if err := optionsFlagSet.Parse(args); err != nil {
98+ return nil, fmt.Errorf("failed to parse flags: %v", err)
99+ }
100+
101+ legacyOpts := options.NewLegacyOptions()
102+ if err := options.Load(config, optionsFlagSet, legacyOpts); err != nil {
103+ return nil, fmt.Errorf("failed to load config: %v", err)
104+ }
105+
106+ opts, err := legacyOpts.ToOptions()
107+ if err != nil {
108+ return nil, fmt.Errorf("failed to convert config: %v", err)
109+ }
110+
111+ return opts, nil
112+}
113+
114+// loadAlphaOptions loads the old style config excluding options converted to
115+// the new alpha format, then merges the alpha options, loaded from YAML,
116+// into the core configuration.
117+func loadAlphaOptions(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
118+ opts, err := loadOptions(config, extraFlags, args)
119+ if err != nil {
120+ return nil, fmt.Errorf("failed to load core options: %v", err)
121+ }
122+
123+ alphaOpts := &options.AlphaOptions{}
124+ if err := options.LoadYAML(alphaConfig, alphaOpts); err != nil {
125+ return nil, fmt.Errorf("failed to load alpha options: %v", err)
126+ }
127+
128+ alphaOpts.MergeInto(opts)
129+ return opts, nil
130+}
131+
132+// loadOptions loads the configuration using the old style format into the
133+// core options.Options struct.
134+// This means that none of the options that have been converted to alpha config
135+// will be loaded using this method.
136+func loadOptions(config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
137+ optionsFlagSet := options.NewFlagSet()
138+ optionsFlagSet.AddFlagSet(extraFlags)
139+ if err := optionsFlagSet.Parse(args); err != nil {
140+ return nil, fmt.Errorf("failed to parse flags: %v", err)
141+ }
142+
143+ opts := options.NewOptions()
144+ if err := options.Load(config, optionsFlagSet, opts); err != nil {
145+ return nil, fmt.Errorf("failed to load config: %v", err)
146+ }
147+
148+ return opts, nil
149+}
150+
151+// printConvertedConfig extracts alpha options from the loaded configuration
152+// and renders these to stdout in YAML format.
153+func printConvertedConfig(opts *options.Options) error {
154+ alphaConfig := &options.AlphaOptions{}
155+ alphaConfig.ExtractFrom(opts)
156+
157+ data, err := yaml.Marshal(alphaConfig)
158+ if err != nil {
159+ return fmt.Errorf("unable to marshal config: %v", err)
160+ }
161+
162+ if _, err := os.Stdout.Write(data); err != nil {
163+ return fmt.Errorf("unable to write output: %v", err)
164+ }
165+
166+ return nil
167+}
main_suite_test.gonew file+16
@@ -0,0 +1,16 @@
1+package main
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+func TestMainSuite(t *testing.T) {
12+ logger.SetOutput(GinkgoWriter)
13+
14+ RegisterFailHandler(Fail)
15+ RunSpecs(t, "Main Suite")
16+}
main_test.gonew file+215
@@ -0,0 +1,215 @@
1+package main
2+
3+import (
4+ "errors"
5+ "io/ioutil"
6+ "os"
7+ "time"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
10+ . "github.com/onsi/ginkgo"
11+ . "github.com/onsi/ginkgo/extensions/table"
12+ . "github.com/onsi/gomega"
13+ "github.com/spf13/pflag"
14+)
15+
16+var _ = Describe("Configuration Loading Suite", func() {
17+ const testLegacyConfig = `
18+upstreams="http://httpbin"
19+set_basic_auth="true"
20+basic_auth_password="super-secret-password"
21+`
22+
23+ const testAlphaConfig = `
24+upstreams:
25+ - id: /
26+ path: /
27+ uri: http://httpbin
28+ flushInterval: 1s
29+ passHostHeader: true
30+ proxyWebSockets: true
31+injectRequestHeaders:
32+- name: Authorization
33+ values:
34+ - claim: user
35+ prefix: "Basic "
36+ basicAuthPassword:
37+ value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
38+- name: X-Forwarded-Groups
39+ values:
40+ - claim: groups
41+- name: X-Forwarded-User
42+ values:
43+ - claim: user
44+- name: X-Forwarded-Email
45+ values:
46+ - claim: email
47+- name: X-Forwarded-Preferred-Username
48+ values:
49+ - claim: preferred_username
50+injectResponseHeaders:
51+- name: Authorization
52+ values:
53+ - claim: user
54+ prefix: "Basic "
55+ basicAuthPassword:
56+ value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
57+`
58+
59+ const testCoreConfig = `
60+http_address="0.0.0.0:4180"
61+cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
62+provider="oidc"
63+email_domains="example.com"
64+oidc_issuer_url="http://dex.localhost:4190/dex"
65+client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
66+client_id="oauth2-proxy"
67+cookie_secure="false"
68+
69+redirect_url="http://localhost:4180/oauth2/callback"
70+`
71+
72+ boolPtr := func(b bool) *bool {
73+ return &b
74+ }
75+
76+ durationPtr := func(d time.Duration) *options.Duration {
77+ du := options.Duration(d)
78+ return &du
79+ }
80+
81+ testExpectedOptions := func() *options.Options {
82+ opts, err := options.NewLegacyOptions().ToOptions()
83+ Expect(err).ToNot(HaveOccurred())
84+
85+ opts.HTTPAddress = "0.0.0.0:4180"
86+ opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
87+ opts.ProviderType = "oidc"
88+ opts.EmailDomains = []string{"example.com"}
89+ opts.OIDCIssuerURL = "http://dex.localhost:4190/dex"
90+ opts.ClientSecret = "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
91+ opts.ClientID = "oauth2-proxy"
92+ opts.Cookie.Secure = false
93+ opts.RawRedirectURL = "http://localhost:4180/oauth2/callback"
94+
95+ opts.UpstreamServers = options.Upstreams{
96+ {
97+ ID: "/",
98+ Path: "/",
99+ URI: "http://httpbin",
100+ FlushInterval: durationPtr(options.DefaultUpstreamFlushInterval),
101+ PassHostHeader: boolPtr(true),
102+ ProxyWebSockets: boolPtr(true),
103+ },
104+ }
105+
106+ authHeader := options.Header{
107+ Name: "Authorization",
108+ Values: []options.HeaderValue{
109+ {
110+ ClaimSource: &options.ClaimSource{
111+ Claim: "user",
112+ Prefix: "Basic ",
113+ BasicAuthPassword: &options.SecretSource{
114+ Value: []byte("super-secret-password"),
115+ },
116+ },
117+ },
118+ },
119+ }
120+
121+ opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...)
122+ opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader)
123+ return opts
124+ }
125+
126+ type loadConfigurationTableInput struct {
127+ configContent string
128+ alphaConfigContent string
129+ args []string
130+ extraFlags func() *pflag.FlagSet
131+ expectedOptions func() *options.Options
132+ expectedErr error
133+ }
134+
135+ DescribeTable("LoadConfiguration",
136+ func(in loadConfigurationTableInput) {
137+ var configFileName, alphaConfigFileName string
138+
139+ defer func() {
140+ if configFileName != "" {
141+ Expect(os.Remove(configFileName)).To(Succeed())
142+ }
143+ if alphaConfigFileName != "" {
144+ Expect(os.Remove(alphaConfigFileName)).To(Succeed())
145+ }
146+ }()
147+
148+ if in.configContent != "" {
149+ By("Writing the config to a temporary file", func() {
150+ file, err := ioutil.TempFile("", "oauth2-proxy-test-config-XXXX.cfg")
151+ Expect(err).ToNot(HaveOccurred())
152+ defer file.Close()
153+
154+ configFileName = file.Name()
155+
156+ _, err = file.WriteString(in.configContent)
157+ Expect(err).ToNot(HaveOccurred())
158+ })
159+ }
160+
161+ if in.alphaConfigContent != "" {
162+ By("Writing the config to a temporary file", func() {
163+ file, err := ioutil.TempFile("", "oauth2-proxy-test-alpha-config-XXXX.yaml")
164+ Expect(err).ToNot(HaveOccurred())
165+ defer file.Close()
166+
167+ alphaConfigFileName = file.Name()
168+
169+ _, err = file.WriteString(in.alphaConfigContent)
170+ Expect(err).ToNot(HaveOccurred())
171+ })
172+ }
173+
174+ extraFlags := pflag.NewFlagSet("test-flagset", pflag.ExitOnError)
175+ if in.extraFlags != nil {
176+ extraFlags = in.extraFlags()
177+ }
178+
179+ opts, err := loadConfiguration(configFileName, alphaConfigFileName, extraFlags, in.args)
180+ if in.expectedErr != nil {
181+ Expect(err).To(MatchError(in.expectedErr.Error()))
182+ } else {
183+ Expect(err).ToNot(HaveOccurred())
184+ }
185+ Expect(in.expectedOptions).ToNot(BeNil())
186+ Expect(opts).To(Equal(in.expectedOptions()))
187+ },
188+ Entry("with legacy configuration", loadConfigurationTableInput{
189+ configContent: testCoreConfig + testLegacyConfig,
190+ expectedOptions: testExpectedOptions,
191+ }),
192+ Entry("with alpha configuration", loadConfigurationTableInput{
193+ configContent: testCoreConfig,
194+ alphaConfigContent: testAlphaConfig,
195+ expectedOptions: testExpectedOptions,
196+ }),
197+ Entry("with bad legacy configuration", loadConfigurationTableInput{
198+ configContent: testCoreConfig + "unknown_field=\"something\"",
199+ expectedOptions: func() *options.Options { return nil },
200+ expectedErr: errors.New("failed to load config: error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_field"),
201+ }),
202+ Entry("with bad alpha configuration", loadConfigurationTableInput{
203+ configContent: testCoreConfig,
204+ alphaConfigContent: testAlphaConfig + ":",
205+ expectedOptions: func() *options.Options { return nil },
206+ expectedErr: errors.New("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line 34: did not find expected key"),
207+ }),
208+ Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{
209+ configContent: testCoreConfig + "unknown_field=\"something\"",
210+ alphaConfigContent: testAlphaConfig,
211+ expectedOptions: func() *options.Options { return nil },
212+ expectedErr: errors.New("failed to load core options: failed to load config: error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_field"),
213+ }),
214+ )
215+})
oauthproxy_test.go+2−2
@@ -515,7 +515,7 @@ func TestBasicAuthPassword(t *testing.T) {
515515 ClaimSource: &options.ClaimSource{
516516 Claim: "email",
517517 BasicAuthPassword: &options.SecretSource{
518− Value: []byte(base64.StdEncoding.EncodeToString([]byte(basicAuthPassword))),
518+ Value: []byte(basicAuthPassword),
519519 },
520520 },
521521 },
@@ -1408,7 +1408,7 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) {
14081408 ClaimSource: &options.ClaimSource{
14091409 Claim: "user",
14101410 BasicAuthPassword: &options.SecretSource{
1411− Value: []byte(base64.StdEncoding.EncodeToString([]byte("This is a secure password"))),
1411+ Value: []byte("This is a secure password"),
14121412 },
14131413 },
14141414 },
pkg/apis/options/alpha_options.go+16
@@ -29,3 +29,19 @@ type AlphaOptions struct {
2929 // or from a static secret value.
3030 InjectResponseHeaders []Header `json:"injectResponseHeaders,omitempty"`
3131}
32+
33+// MergeInto replaces alpha options in the Options struct with the values
34+// from the AlphaOptions
35+func (a *AlphaOptions) MergeInto(opts *Options) {
36+ opts.UpstreamServers = a.Upstreams
37+ opts.InjectRequestHeaders = a.InjectRequestHeaders
38+ opts.InjectResponseHeaders = a.InjectResponseHeaders
39+}
40+
41+// ExtractFrom populates the fields in the AlphaOptions with the values from
42+// the Options
43+func (a *AlphaOptions) ExtractFrom(opts *Options) {
44+ a.Upstreams = opts.UpstreamServers
45+ a.InjectRequestHeaders = opts.InjectRequestHeaders
46+ a.InjectResponseHeaders = opts.InjectResponseHeaders
47+}
pkg/apis/options/legacy_options.go+10−2
@@ -1,7 +1,6 @@
11package options
22
33import (
4− "encoding/base64"
54 "fmt"
65 "net/url"
76 "strconv"
@@ -40,6 +39,15 @@ func NewLegacyOptions() *LegacyOptions {
4039 }
4140}
4241
42+func NewLegacyFlagSet() *pflag.FlagSet {
43+ flagSet := NewFlagSet()
44+
45+ flagSet.AddFlagSet(legacyUpstreamsFlagSet())
46+ flagSet.AddFlagSet(legacyHeadersFlagSet())
47+
48+ return flagSet
49+}
50+
4351func (l *LegacyOptions) ToOptions() (*Options, error) {
4452 upstreams, err := l.LegacyUpstreams.convert()
4553 if err != nil {
@@ -235,7 +243,7 @@ func getBasicAuthHeader(preferEmailToUser bool, basicAuthPassword string) Header
235243 Claim: claim,
236244 Prefix: "Basic ",
237245 BasicAuthPassword: &SecretSource{
238− Value: []byte(base64.StdEncoding.EncodeToString([]byte(basicAuthPassword))),
246+ Value: []byte(basicAuthPassword),
239247 },
240248 },
241249 },
pkg/apis/options/legacy_options_test.go+2−3
@@ -1,7 +1,6 @@
11package options
22
33import (
4− "encoding/base64"
54 "time"
65
76 . "github.com/onsi/ginkgo"
@@ -332,7 +331,7 @@ var _ = Describe("Legacy Options", func() {
332331 Claim: "user",
333332 Prefix: "Basic ",
334333 BasicAuthPassword: &SecretSource{
335− Value: []byte(base64.StdEncoding.EncodeToString([]byte(basicAuthSecret))),
334+ Value: []byte(basicAuthSecret),
336335 },
337336 },
338337 },
@@ -372,7 +371,7 @@ var _ = Describe("Legacy Options", func() {
372371 Claim: "email",
373372 Prefix: "Basic ",
374373 BasicAuthPassword: &SecretSource{
375− Value: []byte(base64.StdEncoding.EncodeToString([]byte(basicAuthSecret))),
374+ Value: []byte(basicAuthSecret),
376375 },
377376 },
378377 },
pkg/apis/options/load.go+28
@@ -1,10 +1,13 @@
11package options
22
33import (
4+ "errors"
45 "fmt"
6+ "io/ioutil"
57 "reflect"
68 "strings"
79
10+ "github.com/ghodss/yaml"
811 "github.com/mitchellh/mapstructure"
912 "github.com/spf13/pflag"
1013 "github.com/spf13/viper"
@@ -132,3 +135,28 @@ func isUnexported(name string) bool {
132135 first := string(name[0])
133136 return first == strings.ToLower(first)
134137}
138+
139+// LoadYAML will load a YAML based configuration file into the options interface provided.
140+func LoadYAML(configFileName string, into interface{}) error {
141+ v := viper.New()
142+ v.SetConfigFile(configFileName)
143+ v.SetConfigType("yaml")
144+ v.SetTypeByDefaultValue(true)
145+
146+ if configFileName == "" {
147+ return errors.New("no configuration file provided")
148+ }
149+
150+ data, err := ioutil.ReadFile(configFileName)
151+ if err != nil {
152+ return fmt.Errorf("unable to load config file: %w", err)
153+ }
154+
155+ // UnmarshalStrict will return an error if the config includes options that are
156+ // not mapped to felds of the into struct
157+ if err := yaml.UnmarshalStrict(data, into, yaml.DisallowUnknownFields); err != nil {
158+ return fmt.Errorf("error unmarshalling config: %w", err)
159+ }
160+
161+ return nil
162+}
pkg/apis/options/load_test.go+192−1
@@ -1,9 +1,11 @@
11package options
22
33import (
4+ "errors"
45 "fmt"
56 "io/ioutil"
67 "os"
8+ "time"
79
810 . "github.com/onsi/ginkgo"
911 . "github.com/onsi/ginkgo/extensions/table"
@@ -295,10 +297,199 @@ var _ = Describe("Load", func() {
295297 expectedOutput: NewOptions(),
296298 }),
297299 Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{
298− flagSet: NewFlagSet,
300+ flagSet: NewLegacyFlagSet,
299301 input: &LegacyOptions{},
300302 expectedOutput: NewLegacyOptions(),
301303 }),
302304 )
303305 })
304306})
307+
308+var _ = Describe("LoadYAML", func() {
309+ Context("with a testOptions structure", func() {
310+ type TestOptionSubStruct struct {
311+ StringSliceOption []string `yaml:"stringSliceOption,omitempty"`
312+ }
313+
314+ type TestOptions struct {
315+ StringOption string `yaml:"stringOption,omitempty"`
316+ Sub TestOptionSubStruct `yaml:"sub,omitempty"`
317+
318+ // Check that embedded fields can be unmarshalled
319+ TestOptionSubStruct `yaml:",inline,squash"`
320+ }
321+
322+ var testOptionsConfigBytesFull = []byte(`
323+stringOption: foo
324+stringSliceOption:
325+- a
326+- b
327+- c
328+sub:
329+ stringSliceOption:
330+ - d
331+ - e
332+`)
333+
334+ type loadYAMLTableInput struct {
335+ configFile []byte
336+ input interface{}
337+ expectedErr error
338+ expectedOutput interface{}
339+ }
340+
341+ DescribeTable("LoadYAML",
342+ func(in loadYAMLTableInput) {
343+ var configFileName string
344+
345+ if in.configFile != nil {
346+ By("Creating a config file")
347+ configFile, err := ioutil.TempFile("", "oauth2-proxy-test-config-file")
348+ Expect(err).ToNot(HaveOccurred())
349+ defer configFile.Close()
350+
351+ _, err = configFile.Write(in.configFile)
352+ Expect(err).ToNot(HaveOccurred())
353+ defer os.Remove(configFile.Name())
354+
355+ configFileName = configFile.Name()
356+ }
357+
358+ var input interface{}
359+ if in.input != nil {
360+ input = in.input
361+ } else {
362+ input = &TestOptions{}
363+ }
364+ err := LoadYAML(configFileName, input)
365+ if in.expectedErr != nil {
366+ Expect(err).To(MatchError(in.expectedErr.Error()))
367+ } else {
368+ Expect(err).ToNot(HaveOccurred())
369+ }
370+ Expect(input).To(Equal(in.expectedOutput))
371+ },
372+ Entry("with a valid input", loadYAMLTableInput{
373+ configFile: testOptionsConfigBytesFull,
374+ input: &TestOptions{},
375+ expectedOutput: &TestOptions{
376+ StringOption: "foo",
377+ Sub: TestOptionSubStruct{
378+ StringSliceOption: []string{"d", "e"},
379+ },
380+ TestOptionSubStruct: TestOptionSubStruct{
381+ StringSliceOption: []string{"a", "b", "c"},
382+ },
383+ },
384+ }),
385+ Entry("with no config file", loadYAMLTableInput{
386+ configFile: nil,
387+ input: &TestOptions{},
388+ expectedOutput: &TestOptions{},
389+ expectedErr: errors.New("no configuration file provided"),
390+ }),
391+ Entry("with invalid YAML", loadYAMLTableInput{
392+ configFile: []byte("\tfoo: bar"),
393+ input: &TestOptions{},
394+ expectedOutput: &TestOptions{},
395+ expectedErr: errors.New("error unmarshalling config: error converting YAML to JSON: yaml: found character that cannot start any token"),
396+ }),
397+ Entry("with extra fields in the YAML", loadYAMLTableInput{
398+ configFile: append(testOptionsConfigBytesFull, []byte("foo: bar\n")...),
399+ input: &TestOptions{},
400+ expectedOutput: &TestOptions{
401+ StringOption: "foo",
402+ Sub: TestOptionSubStruct{
403+ StringSliceOption: []string{"d", "e"},
404+ },
405+ TestOptionSubStruct: TestOptionSubStruct{
406+ StringSliceOption: []string{"a", "b", "c"},
407+ },
408+ },
409+ expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: unknown field \"foo\""),
410+ }),
411+ Entry("with an incorrect type for a string field", loadYAMLTableInput{
412+ configFile: []byte(`stringOption: ["a", "b"]`),
413+ input: &TestOptions{},
414+ expectedOutput: &TestOptions{},
415+ expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go struct field TestOptions.StringOption of type string"),
416+ }),
417+ Entry("with an incorrect type for an array field", loadYAMLTableInput{
418+ configFile: []byte(`stringSliceOption: "a"`),
419+ input: &TestOptions{},
420+ expectedOutput: &TestOptions{},
421+ expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field TestOptions.StringSliceOption of type []string"),
422+ }),
423+ )
424+ })
425+
426+ It("should load a full example AlphaOptions", func() {
427+ config := []byte(`
428+upstreams:
429+- id: httpbin
430+ path: /
431+ uri: http://httpbin
432+ flushInterval: 500ms
433+injectRequestHeaders:
434+- name: X-Forwarded-User
435+ values:
436+ - claim: user
437+injectResponseHeaders:
438+- name: X-Secret
439+ values:
440+ - value: c2VjcmV0
441+`)
442+
443+ By("Creating a config file")
444+ configFile, err := ioutil.TempFile("", "oauth2-proxy-test-alpha-config-file")
445+ Expect(err).ToNot(HaveOccurred())
446+ defer configFile.Close()
447+
448+ _, err = configFile.Write(config)
449+ Expect(err).ToNot(HaveOccurred())
450+ defer os.Remove(configFile.Name())
451+
452+ configFileName := configFile.Name()
453+
454+ By("Loading the example config")
455+ into := &AlphaOptions{}
456+ Expect(LoadYAML(configFileName, into)).To(Succeed())
457+
458+ flushInterval := Duration(500 * time.Millisecond)
459+
460+ Expect(into).To(Equal(&AlphaOptions{
461+ Upstreams: []Upstream{
462+ {
463+ ID: "httpbin",
464+ Path: "/",
465+ URI: "http://httpbin",
466+ FlushInterval: &flushInterval,
467+ },
468+ },
469+ InjectRequestHeaders: []Header{
470+ {
471+ Name: "X-Forwarded-User",
472+ Values: []HeaderValue{
473+ {
474+ ClaimSource: &ClaimSource{
475+ Claim: "user",
476+ },
477+ },
478+ },
479+ },
480+ },
481+ InjectResponseHeaders: []Header{
482+ {
483+ Name: "X-Secret",
484+ Values: []HeaderValue{
485+ {
486+ SecretSource: &SecretSource{
487+ Value: []byte("secret"),
488+ },
489+ },
490+ },
491+ },
492+ },
493+ }))
494+ })
495+})
pkg/apis/options/options.go−2
@@ -246,8 +246,6 @@ func NewFlagSet() *pflag.FlagSet {
246246
247247 flagSet.AddFlagSet(cookieFlagSet())
248248 flagSet.AddFlagSet(loggingFlagSet())
249− flagSet.AddFlagSet(legacyUpstreamsFlagSet())
250− flagSet.AddFlagSet(legacyHeadersFlagSet())
251249
252250 return flagSet
253251}
pkg/apis/options/util/util.go+1−4
@@ -1,7 +1,6 @@
11package util
22
33import (
4− "encoding/base64"
54 "errors"
65 "io/ioutil"
76 "os"
@@ -13,9 +12,7 @@ import (
1312func GetSecretValue(source *options.SecretSource) ([]byte, error) {
1413 switch {
1514 case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "":
16− value := make([]byte, base64.StdEncoding.DecodedLen(len(source.Value)))
17− decoded, err := base64.StdEncoding.Decode(value, source.Value)
18− return value[:decoded], err
15+ return source.Value, nil
1916 case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "":
2017 return []byte(os.Getenv(source.FromEnv)), nil
2118 case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "":
pkg/apis/options/util/util_test.go+3−12
@@ -1,7 +1,6 @@
11package util
22
33import (
4− "encoding/base64"
54 "io/ioutil"
65 "os"
76 "path"
@@ -31,20 +30,12 @@ var _ = Describe("GetSecretValue", func() {
3130 os.RemoveAll(fileDir)
3231 })
3332
34− It("returns the correct value from base64", func() {
35− originalValue := []byte("secret-value-1")
36− b64Value := base64.StdEncoding.EncodeToString((originalValue))
37−
38− // Once encoded, the originalValue could have a decoded length longer than
39− // its actual length, ensure we trim this.
40− // This assertion ensures we are testing the triming
41− Expect(len(originalValue)).To(BeNumerically("<", base64.StdEncoding.DecodedLen(len(b64Value))))
42−
33+ It("returns the correct value from the string value", func() {
4334 value, err := GetSecretValue(&options.SecretSource{
44− Value: []byte(b64Value),
35+ Value: []byte("secret-value-1"),
4536 })
4637 Expect(err).ToNot(HaveOccurred())
47− Expect(value).To(Equal(originalValue))
38+ Expect(string(value)).To(Equal("secret-value-1"))
4839 })
4940
5041 It("returns the correct value from the environment", func() {
pkg/header/injector_test.go+7−7
@@ -49,14 +49,14 @@ var _ = Describe("Injector Suite", func() {
4949 },
5050 expectedErr: nil,
5151 }),
52− Entry("with a static valued header from base64", newInjectorTableInput{
52+ Entry("with a static valued header from string", newInjectorTableInput{
5353 headers: []options.Header{
5454 {
5555 Name: "Secret",
5656 Values: []options.HeaderValue{
5757 {
5858 SecretSource: &options.SecretSource{
59− Value: []byte(base64.StdEncoding.EncodeToString([]byte("super-secret"))),
59+ Value: []byte("super-secret"),
6060 },
6161 },
6262 },
@@ -200,7 +200,7 @@ var _ = Describe("Injector Suite", func() {
200200 ClaimSource: &options.ClaimSource{
201201 Claim: "user",
202202 BasicAuthPassword: &options.SecretSource{
203− Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
203+ Value: []byte("basic-password"),
204204 },
205205 },
206206 },
@@ -349,7 +349,7 @@ var _ = Describe("Injector Suite", func() {
349349 ClaimSource: &options.ClaimSource{
350350 Claim: "user",
351351 BasicAuthPassword: &options.SecretSource{
352− Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))),
352+ Value: []byte("basic-password"),
353353 },
354354 },
355355 },
@@ -380,17 +380,17 @@ var _ = Describe("Injector Suite", func() {
380380 Values: []options.HeaderValue{
381381 {
382382 SecretSource: &options.SecretSource{
383− Value: []byte(base64.StdEncoding.EncodeToString([]byte("major=1"))),
383+ Value: []byte("major=1"),
384384 },
385385 },
386386 {
387387 SecretSource: &options.SecretSource{
388− Value: []byte(base64.StdEncoding.EncodeToString([]byte("minor=2"))),
388+ Value: []byte("minor=2"),
389389 },
390390 },
391391 {
392392 SecretSource: &options.SecretSource{
393− Value: []byte(base64.StdEncoding.EncodeToString([]byte("patch=3"))),
393+ Value: []byte("patch=3"),
394394 },
395395 },
396396 },
pkg/validation/common.go+1−10
@@ -1,7 +1,6 @@
11package validation
22
33import (
4− "encoding/base64"
54 "fmt"
65 "os"
76
@@ -13,7 +12,7 @@ const multipleValuesForSecretSource = "multiple values specified for secret sour
1312func validateSecretSource(source options.SecretSource) string {
1413 switch {
1514 case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "":
16− return validateSecretSourceValue(source.Value)
15+ return ""
1716 case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "":
1817 return validateSecretSourceEnv(source.FromEnv)
1918 case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "":
@@ -23,14 +22,6 @@ func validateSecretSource(source options.SecretSource) string {
2322 }
2423}
2524
26−func validateSecretSourceValue(value []byte) string {
27− dst := make([]byte, len(value))
28− if _, err := base64.StdEncoding.Decode(dst, value); err != nil {
29− return fmt.Sprintf("error decoding secret value: %v", err)
30− }
31− return ""
32−}
33−
3425func validateSecretSourceEnv(key string) string {
3526 if value := os.Getenv(key); value == "" {
3627 return fmt.Sprintf("error loading secret from environent: no value for for key %q", key)
pkg/validation/common_test.go+1−10
@@ -1,7 +1,6 @@
11package validation
22
33import (
4− "encoding/base64"
54 "io/ioutil"
65 "os"
76
@@ -17,7 +16,7 @@ var _ = Describe("Common", func() {
1716 var validSecretSourceFile string
1817
1918 BeforeEach(func() {
20− validSecretSourceValue = []byte(base64.StdEncoding.EncodeToString([]byte("This is a secret source value")))
19+ validSecretSourceValue = []byte("This is a secret source value")
2120 Expect(os.Setenv(validSecretSourceEnv, "This is a secret source env")).To(Succeed())
2221 tmp, err := ioutil.TempFile("", "oauth2-proxy-secret-source-test")
2322 Expect(err).ToNot(HaveOccurred())
@@ -110,14 +109,6 @@ var _ = Describe("Common", func() {
110109 },
111110 expectedMsg: "",
112111 }),
113− Entry("with an invalid Value", validateSecretSourceTableInput{
114− source: func() options.SecretSource {
115− return options.SecretSource{
116− Value: []byte("Invalid Base64 Value"),
117− }
118− },
119− expectedMsg: "error decoding secret value: illegal base64 data at input byte 7",
120− }),
121112 Entry("with an invalid FromEnv", validateSecretSourceTableInput{
122113 source: func() options.SecretSource {
123114 return options.SecretSource{
pkg/validation/header_test.go+2−2
@@ -148,7 +148,7 @@ var _ = Describe("Headers", func() {
148148 ClaimSource: &options.ClaimSource{
149149 Claim: "user",
150150 BasicAuthPassword: &options.SecretSource{
151− Value: []byte("secret"),
151+ FromEnv: "UNKNOWN_ENV",
152152 },
153153 },
154154 },
@@ -157,7 +157,7 @@ var _ = Describe("Headers", func() {
157157 validHeader1,
158158 },
159159 expectedMsgs: []string{
160− "invalid header \"With-Invalid-Basic-Auth\": invalid values: invalid basicAuthPassword: error decoding secret value: illegal base64 data at input byte 4",
160+ "invalid header \"With-Invalid-Basic-Auth\": invalid values: invalid basicAuthPassword: error loading secret from environent: no value for for key \"UNKNOWN_ENV\"",
161161 },
162162 }),
163163 )