From 31a4c34726da114c9185afa60f65107263dd208f Mon Sep 17 00:00:00 2001 From: tuunit Date: Sat, 4 May 2024 16:41:54 +0200 Subject: [PATCH] introduce mapstructure decoder for yaml parsing remove color output in tests for better readability in github actions bugfix: remove google as default provider for alpha options fix conversion flow for toml to yaml revert ginkgo color deactivation revert claim- and secret source back to pointers regenerate alpha config --- .golangci.yml | 2 +- docs/docs/configuration/alpha_config.md | 18 +--- main.go | 35 +++++--- main_test.go | 39 +++++---- oauthproxy_test.go | 6 +- pkg/apis/options/alpha_options.go | 25 ++++-- pkg/apis/options/common.go | 63 -------------- pkg/apis/options/duration.go | 43 +++++++++ pkg/apis/options/header.go | 4 +- pkg/apis/options/legacy_options.go | 6 +- pkg/apis/options/legacy_options_test.go | 12 +-- pkg/apis/options/load.go | 111 +++++++++++++++--------- pkg/apis/options/load_test.go | 24 ++--- pkg/apis/options/providers.go | 6 +- pkg/apis/options/secret_source.go | 14 +++ pkg/apis/options/upstreams.go | 4 +- pkg/apis/options/util/util.go | 2 +- pkg/apis/options/util/util_test.go | 2 +- pkg/header/injector_test.go | 16 ++-- pkg/http/http_suite_test.go | 12 +-- pkg/http/server_test.go | 8 +- pkg/middleware/headers_test.go | 8 +- pkg/upstream/http.go | 4 +- pkg/upstream/http_test.go | 24 ++--- pkg/validation/common_test.go | 4 +- pkg/validation/header.go | 2 - pkg/validation/header_test.go | 4 +- pkg/validation/upstreams.go | 2 +- pkg/validation/upstreams_test.go | 2 +- 29 files changed, 269 insertions(+), 233 deletions(-) delete mode 100644 pkg/apis/options/common.go create mode 100644 pkg/apis/options/duration.go create mode 100644 pkg/apis/options/secret_source.go diff --git a/.golangci.yml b/.golangci.yml index edab12d0..62e1df8e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -37,7 +37,7 @@ linters: - linters: - revive path: _test\.go - text: 'dot-imports:' + text: "dot-imports:" # # If we have tests in shared test folders, these can be less strictly linted - linters: - bodyclose diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md index 28645ceb..dcfb9648 100644 --- a/docs/docs/configuration/alpha_config.md +++ b/docs/docs/configuration/alpha_config.md @@ -204,16 +204,6 @@ ClaimSource allows loading a header value from a claim within the session | `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the
claim if it is non-empty. | | `basicAuthPassword` | _[SecretSource](#secretsource)_ | BasicAuthPassword converts this claim into a basic auth header.
Note the value of claim will become the basic auth username and the
basicAuthPassword will be used as the password value. | -### Duration -#### (`string` alias) - -(**Appears on:** [Upstream](#upstream)) - -Duration is as string representation of a period of time. -A duration string is a is a possibly signed sequence of decimal numbers, -each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". -Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". - ### GitHubOptions (**Appears on:** [Provider](#provider)) @@ -275,7 +265,7 @@ make up the header value | Field | Type | Description | | ----- | ---- | ----------- | -| `value` | _[]byte_ | Value expects a base64 encoded string value. | +| `value` | _string_ | Value expects a base64 encoded string value. | | `fromEnv` | _string_ | FromEnv expects the name of an environment variable. | | `fromFile` | _string_ | FromFile expects a path to a file containing the secret value. | | `claim` | _string_ | Claim is the name of the claim in the session that the value should be
loaded from. Available claims: `access_token` `id_token` `created_at`
`expires_on` `refresh_token` `email` `user` `groups` `preferred_username`. | @@ -487,7 +477,7 @@ Only one source within the struct should be defined at any time. | Field | Type | Description | | ----- | ---- | ----------- | -| `value` | _[]byte_ | Value expects a base64 encoded string value. | +| `value` | _string_ | Value expects a base64 encoded string value. | | `fromEnv` | _string_ | FromEnv expects the name of an environment variable. | | `fromFile` | _string_ | FromFile expects a path to a file containing the secret value. | @@ -547,10 +537,10 @@ Requests will be proxied to this upstream if the path matches the request path. | `insecureSkipTLSVerify` | _bool_ | InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.
This option is insecure and will allow potential Man-In-The-Middle attacks
between OAuth2 Proxy and the upstream server.
Defaults to false. | | `static` | _bool_ | 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. | | `staticCode` | _int_ | StaticCode determines the response code for the Static response.
This option can only be used with Static enabled. | -| `flushInterval` | _[Duration](#duration)_ | FlushInterval is the period between flushing the response buffer when
streaming response from the upstream.
Defaults to 1 second. | +| `flushInterval` | _duration_ | FlushInterval is the period between flushing the response buffer when
streaming response from the upstream.
Defaults to 1 second. | | `passHostHeader` | _bool_ | PassHostHeader determines whether the request host header should be proxied
to the upstream server.
Defaults to true. | | `proxyWebSockets` | _bool_ | ProxyWebSockets enables proxying of websockets to upstream servers
Defaults to true. | -| `timeout` | _[Duration](#duration)_ | Timeout is the maximum duration the server will wait for a response from the upstream server.
Defaults to 30 seconds. | +| `timeout` | _duration_ | Timeout is the maximum duration the server will wait for a response from the upstream server.
Defaults to 30 seconds. | | `disableKeepAlives` | _bool_ | DisableKeepAlives disables HTTP keep-alive connections to the upstream server.
Defaults to false. | ### UpstreamConfig diff --git a/main.go b/main.go index cf7e964c..d9fc406b 100644 --- a/main.go +++ b/main.go @@ -67,12 +67,17 @@ func main() { // loadConfiguration will load in the user's configuration. // It will either load the alpha configuration (if alphaConfig is given) // or the legacy configuration. -func loadConfiguration(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { - if alphaConfig != "" { - 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.") - return loadAlphaOptions(config, alphaConfig, extraFlags, args) +func loadConfiguration(config, yamlConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { + opts, err := loadLegacyOptions(config, extraFlags, args) + if err != nil { + return nil, err } - return loadLegacyOptions(config, extraFlags, args) + + if yamlConfig != "" { + 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.") + return loadYamlOptions(yamlConfig, config, extraFlags, args) + } + return opts, err } // loadLegacyOptions loads the old toml options using the legacy flagset @@ -97,17 +102,17 @@ func loadLegacyOptions(config string, extraFlags *pflag.FlagSet, args []string) return opts, nil } -// loadAlphaOptions loads the old style config excluding options converted to +// loadYamlOptions loads the old style config excluding options converted to // the new alpha format, then merges the alpha options, loaded from YAML, // into the core configuration. -func loadAlphaOptions(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { +func loadYamlOptions(yamlConfig, config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { opts, err := loadOptions(config, extraFlags, args) if err != nil { return nil, fmt.Errorf("failed to load core options: %v", err) } - alphaOpts := &options.AlphaOptions{} - if err := options.LoadYAML(alphaConfig, alphaOpts); err != nil { + alphaOpts := options.NewAlphaOptions(opts) + if err := options.LoadYAML(yamlConfig, alphaOpts); err != nil { return nil, fmt.Errorf("failed to load alpha options: %v", err) } @@ -137,10 +142,16 @@ func loadOptions(config string, extraFlags *pflag.FlagSet, args []string) (*opti // printConvertedConfig extracts alpha options from the loaded configuration // and renders these to stdout in YAML format. func printConvertedConfig(opts *options.Options) error { - alphaConfig := &options.AlphaOptions{} - alphaConfig.ExtractFrom(opts) + alphaConfig := options.NewAlphaOptions(opts) - data, err := yaml.Marshal(alphaConfig) + // Generic interface for loading arbitrary yaml structure + var buffer map[string]interface{} + + if err := options.Decode(alphaConfig, &buffer); err != nil { + return fmt.Errorf("unable to decode alpha config into interface: %w", err) + } + + data, err := yaml.Marshal(buffer) if err != nil { return fmt.Errorf("unable to marshal config: %v", err) } diff --git a/main_test.go b/main_test.go index 8e40fe7f..a6ea83c2 100644 --- a/main_test.go +++ b/main_test.go @@ -43,29 +43,35 @@ upstreamConfig: injectRequestHeaders: - name: Authorization values: - - claim: user - prefix: "Basic " - basicAuthPassword: - value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk + - claimSource: + claim: user + prefix: "Basic " + basicAuthPassword: + value: super-secret-password - name: X-Forwarded-Groups values: - - claim: groups + - claimSource: + claim: groups - name: X-Forwarded-User values: - - claim: user + - claimSource: + claim: user - name: X-Forwarded-Email values: - - claim: email + - claimSource: + claim: email - name: X-Forwarded-Preferred-Username values: - - claim: preferred_username + - claimSource: + claim: preferred_username injectResponseHeaders: - name: Authorization values: - - claim: user - prefix: "Basic " - basicAuthPassword: - value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk + - claimSource: + claim: user + prefix: "Basic " + basicAuthPassword: + value: super-secret-password server: bindAddress: "127.0.0.1:4180" providers: @@ -100,9 +106,8 @@ redirect_url="http://localhost:4180/oauth2/callback" return &b } - durationPtr := func(d time.Duration) *options.Duration { - du := options.Duration(d) - return &du + durationPtr := func(d time.Duration) *time.Duration { + return &d } testExpectedOptions := func() *options.Options { @@ -136,7 +141,7 @@ redirect_url="http://localhost:4180/oauth2/callback" Claim: "user", Prefix: "Basic ", BasicAuthPassword: &options.SecretSource{ - Value: []byte("super-secret-password"), + Value: "super-secret-password", }, }, }, @@ -226,7 +231,7 @@ redirect_url="http://localhost:4180/oauth2/callback" opts, err := loadConfiguration(configFileName, alphaConfigFileName, extraFlags, in.args) if in.expectedErr != nil { - Expect(err).To(MatchError(in.expectedErr.Error())) + Expect(err).To(MatchError(ContainSubstring(in.expectedErr.Error()))) } else { Expect(err).ToNot(HaveOccurred()) } diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 488b8cea..7b495bff 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -215,7 +215,7 @@ func TestBasicAuthPassword(t *testing.T) { ClaimSource: &options.ClaimSource{ Claim: "email", BasicAuthPassword: &options.SecretSource{ - Value: []byte(basicAuthPassword), + Value: basicAuthPassword, }, }, }, @@ -1282,7 +1282,7 @@ func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) { ClaimSource: &options.ClaimSource{ Claim: "user", BasicAuthPassword: &options.SecretSource{ - Value: []byte("This is a secure password"), + Value: "This is a secure password", }, }, }, @@ -2044,7 +2044,7 @@ func baseTestOptions() *options.Options { ClaimSource: &options.ClaimSource{ Claim: "user", BasicAuthPassword: &options.SecretSource{ - Value: []byte(base64.StdEncoding.EncodeToString([]byte("This is a secure password"))), + Value: base64.StdEncoding.EncodeToString([]byte("This is a secure password")), }, }, }, diff --git a/pkg/apis/options/alpha_options.go b/pkg/apis/options/alpha_options.go index a438518c..278db401 100644 --- a/pkg/apis/options/alpha_options.go +++ b/pkg/apis/options/alpha_options.go @@ -47,15 +47,11 @@ type AlphaOptions struct { Providers Providers `json:"providers,omitempty"` } -// MergeInto replaces alpha options in the Options struct with the values -// from the AlphaOptions -func (a *AlphaOptions) MergeInto(opts *Options) { - opts.UpstreamServers = a.UpstreamConfig - opts.InjectRequestHeaders = a.InjectRequestHeaders - opts.InjectResponseHeaders = a.InjectResponseHeaders - opts.Server = a.Server - opts.MetricsServer = a.MetricsServer - opts.Providers = a.Providers +// Initialize alpha options with default values and settings of the core options +func NewAlphaOptions(opts *Options) *AlphaOptions { + aOpts := &AlphaOptions{} + aOpts.ExtractFrom(opts) + return aOpts } // ExtractFrom populates the fields in the AlphaOptions with the values from @@ -68,3 +64,14 @@ func (a *AlphaOptions) ExtractFrom(opts *Options) { a.MetricsServer = opts.MetricsServer a.Providers = opts.Providers } + +// MergeInto replaces alpha options in the Options struct with the values +// from the AlphaOptions +func (a *AlphaOptions) MergeInto(opts *Options) { + opts.UpstreamServers = a.UpstreamConfig + opts.InjectRequestHeaders = a.InjectRequestHeaders + opts.InjectResponseHeaders = a.InjectResponseHeaders + opts.Server = a.Server + opts.MetricsServer = a.MetricsServer + opts.Providers = a.Providers +} diff --git a/pkg/apis/options/common.go b/pkg/apis/options/common.go deleted file mode 100644 index 88d24d82..00000000 --- a/pkg/apis/options/common.go +++ /dev/null @@ -1,63 +0,0 @@ -package options - -import ( - "fmt" - "strconv" - "time" -) - -// SecretSource references an individual secret value. -// Only one source within the struct should be defined at any time. -type SecretSource struct { - // Value expects a base64 encoded string value. - Value []byte `json:"value,omitempty"` - - // FromEnv expects the name of an environment variable. - FromEnv string `json:"fromEnv,omitempty"` - - // FromFile expects a path to a file containing the secret value. - FromFile string `json:"fromFile,omitempty"` -} - -// Duration is an alias for time.Duration so that we can ensure the marshalling -// and unmarshalling of string durations is done as users expect. -// Intentional blank line below to keep this first part of the comment out of -// any generated references. - -// Duration is as string representation of a period of time. -// A duration string is a is a possibly signed sequence of decimal numbers, -// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". -// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". -// +reference-gen:alias-name=string -type Duration time.Duration - -// UnmarshalJSON parses the duration string and sets the value of duration -// to the value of the duration string. -func (d *Duration) UnmarshalJSON(data []byte) error { - input := string(data) - if unquoted, err := strconv.Unquote(input); err == nil { - input = unquoted - } - - du, err := time.ParseDuration(input) - if err != nil { - return err - } - *d = Duration(du) - return nil -} - -// MarshalJSON ensures that when the string is marshalled to JSON as a human -// readable string. -func (d *Duration) MarshalJSON() ([]byte, error) { - dStr := fmt.Sprintf("%q", d.Duration().String()) - return []byte(dStr), nil -} - -// Duration returns the time.Duration version of this Duration -func (d *Duration) Duration() time.Duration { - if d == nil { - return time.Duration(0) - } - return time.Duration(*d) -} diff --git a/pkg/apis/options/duration.go b/pkg/apis/options/duration.go new file mode 100644 index 00000000..da13d96e --- /dev/null +++ b/pkg/apis/options/duration.go @@ -0,0 +1,43 @@ +package options + +import ( + "reflect" + "time" + + "github.com/mitchellh/mapstructure" +) + +// Duration is an alias for time.Duration so that we can ensure the marshalling +// and unmarshalling of string durations is done as users expect. +// Intentional blank line below to keep this first part of the comment out of +// any generated references. + +// Duration is as string representation of a period of time. +// A duration string is a is a possibly signed sequence of decimal numbers, +// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". +// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + +// Conversion from string or floating point to golang duration type +// This way floating points will be converted to seconds and strings +// of type 3s or 5m will be parsed with time.ParseDuration +func toDurationHookFunc() mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if t != reflect.TypeOf(time.Duration(0)) { + return data, nil + } + + switch f.Kind() { + case reflect.String: + return time.ParseDuration(data.(string)) + case reflect.Float64: + return time.Duration(data.(float64) * float64(time.Second)), nil + case reflect.Int64: + return time.Duration(data.(int64)), nil + default: + return data, nil + } + } +} diff --git a/pkg/apis/options/header.go b/pkg/apis/options/header.go index 90e6445c..a7e10c44 100644 --- a/pkg/apis/options/header.go +++ b/pkg/apis/options/header.go @@ -21,10 +21,10 @@ type Header struct { // make up the header value type HeaderValue struct { // Allow users to load the value from a secret source - *SecretSource `json:",omitempty"` + *SecretSource `json:"secretSource,omitempty"` // Allow users to load the value from a session claim - *ClaimSource `json:",omitempty"` + *ClaimSource `json:"claimSource,omitempty"` } // ClaimSource allows loading a header value from a claim within the session diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index 12975225..db73f910 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -136,8 +136,8 @@ func (l *LegacyUpstreams) convert() (UpstreamConfig, error) { u.Path = "/" } - flushInterval := Duration(l.FlushInterval) - timeout := Duration(l.Timeout) + flushInterval := l.FlushInterval + timeout := l.Timeout upstream := Upstream{ ID: u.Path, Path: u.Path, @@ -294,7 +294,7 @@ func getBasicAuthHeader(preferEmailToUser bool, basicAuthPassword string) Header Claim: claim, Prefix: "Basic ", BasicAuthPassword: &SecretSource{ - Value: []byte(basicAuthPassword), + Value: basicAuthPassword, }, }, }, diff --git a/pkg/apis/options/legacy_options_test.go b/pkg/apis/options/legacy_options_test.go index 9481cf95..c5f0e4da 100644 --- a/pkg/apis/options/legacy_options_test.go +++ b/pkg/apis/options/legacy_options_test.go @@ -15,8 +15,8 @@ var _ = Describe("Legacy Options", func() { legacyOpts := NewLegacyOptions() // Set upstreams and related options to test their conversion - flushInterval := Duration(5 * time.Second) - timeout := Duration(5 * time.Second) + flushInterval := 5 * time.Second + timeout := 5 * time.Second legacyOpts.LegacyUpstreams.FlushInterval = time.Duration(flushInterval) legacyOpts.LegacyUpstreams.Timeout = time.Duration(timeout) legacyOpts.LegacyUpstreams.PassHostHeader = true @@ -147,8 +147,8 @@ var _ = Describe("Legacy Options", func() { skipVerify := true passHostHeader := false proxyWebSockets := true - flushInterval := Duration(5 * time.Second) - timeout := Duration(5 * time.Second) + flushInterval := 5 * time.Second + timeout := 5 * time.Second disableKeepAlives := true // Test cases and expected outcomes @@ -369,7 +369,7 @@ var _ = Describe("Legacy Options", func() { Claim: "user", Prefix: "Basic ", BasicAuthPassword: &SecretSource{ - Value: []byte(basicAuthSecret), + Value: basicAuthSecret, }, }, }, @@ -409,7 +409,7 @@ var _ = Describe("Legacy Options", func() { Claim: "email", Prefix: "Basic ", BasicAuthPassword: &SecretSource{ - Value: []byte(basicAuthSecret), + Value: basicAuthSecret, }, }, }, diff --git a/pkg/apis/options/load.go b/pkg/apis/options/load.go index c302e8e7..22e775a8 100644 --- a/pkg/apis/options/load.go +++ b/pkg/apis/options/load.go @@ -55,6 +55,76 @@ func Load(configFileName string, flagSet *pflag.FlagSet, into interface{}) error return nil } +// LoadYAML will load a YAML based configuration file into the options interface provided. +func LoadYAML(configFileName string, opts interface{}) error { + buffer, err := loadAndSubstituteEnvs(configFileName) + if err != nil { + return err + } + + // Generic interface for loading arbitrary yaml structure + var intermediate map[string]interface{} + + if err := yaml.Unmarshal(buffer, &intermediate); err != nil { + return fmt.Errorf("error unmarshalling config: %w", err) + } + + return Decode(intermediate, opts) +} + +func Decode(input interface{}, result interface{}) error { + // Using mapstructure to decode arbitrary yaml structure into options and + // merge with existing values instead of overwriting everything. This is especially + // important as we have a lot of default values for boolean which are supposed to be + // true by default. Normally by just parsing through yaml all booleans that aren't + // referenced in the config file would be parsed as false and we cannot identify after + // the fact if they have been explicitly set to false or have not been referenced. + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc(toDurationHookFunc()), + Metadata: nil, // Don't track any metadata + Result: result, // Decode the result into the prefilled options + TagName: "json", // Parse all fields that use the yaml tag + ZeroFields: false, // Don't clean the default values from the result map (options) + ErrorUnused: true, // Throw an error if keys have been used that aren't mapped to any struct fields + IgnoreUntaggedFields: true, // Ignore fields in structures that aren't tagged with yaml + }) + + if err != nil { + return fmt.Errorf("error creating decoder for config: %w", err) + } + + if err := decoder.Decode(input); err != nil { + return fmt.Errorf("error decoding config: %w", err) + } + + return nil +} + +// loadAndSubstituteEnvs reads the yaml config into a generic byte buffer and +// substitute env references +func loadAndSubstituteEnvs(configFileName string) ([]byte, error) { + if configFileName == "" { + return nil, errors.New("no configuration file provided") + } + + unparsedBuffer, err := os.ReadFile(configFileName) + if err != nil { + return nil, fmt.Errorf("unable to load config file: %w", err) + } + + modifiedBuffer, err := normalizeSubstitution(unparsedBuffer) + if err != nil { + return nil, fmt.Errorf("error normalizing substitution string : %w", err) + } + + buffer, err := envsubst.Bytes(modifiedBuffer) + if err != nil { + return nil, fmt.Errorf("error in substituting env variables : %w", err) + } + + return buffer, nil +} + // registerFlags uses `cfg` and `flag` tags to associate flags in the flagSet // to the fields in the options interface provided. // Each exported field in the options must have a `cfg` tag otherwise an error will occur. @@ -140,47 +210,6 @@ func isUnexported(name string) bool { return first == strings.ToLower(first) } -// LoadYAML will load a YAML based configuration file into the options interface provided. -func LoadYAML(configFileName string, into interface{}) error { - buffer, err := loadAndParseYaml(configFileName) - if err != nil { - return err - } - - // UnmarshalStrict will return an error if the config includes options that are - // not mapped to fields of the into struct - if err := yaml.UnmarshalStrict(buffer, into, yaml.DisallowUnknownFields); err != nil { - return fmt.Errorf("error unmarshalling config: %w", err) - } - - return nil -} - -// loadAndParseYaml reads the config from the filesystem and -// execute the environment variable substitution -func loadAndParseYaml(configFileName string) ([]byte, error) { - if configFileName == "" { - return nil, errors.New("no configuration file provided") - } - - unparsedBuffer, err := os.ReadFile(configFileName) - if err != nil { - return nil, fmt.Errorf("unable to load config file: %w", err) - } - - modifiedBuffer, err := normalizeSubstitution(unparsedBuffer) - if err != nil { - return nil, fmt.Errorf("error normalizing substitution string : %w", err) - } - - buffer, err := envsubst.Bytes(modifiedBuffer) - if err != nil { - return nil, fmt.Errorf("error in substituting env variables : %w", err) - } - - return buffer, nil -} - // normalizeSubstitution normalizes dollar signs ($) with numerals like // $1 or $2 properly by correctly escaping them func normalizeSubstitution(unparsedBuffer []byte) ([]byte, error) { diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go index 06123c37..6f327a22 100644 --- a/pkg/apis/options/load_test.go +++ b/pkg/apis/options/load_test.go @@ -355,15 +355,15 @@ var _ = Describe("Load", func() { var _ = Describe("LoadYAML", func() { Context("with a testOptions structure", func() { type TestOptionSubStruct struct { - StringSliceOption []string `yaml:"stringSliceOption,omitempty"` + StringSliceOption []string `json:"stringSliceOption,omitempty"` } type TestOptions struct { - StringOption string `yaml:"stringOption,omitempty"` - Sub TestOptionSubStruct `yaml:"sub,omitempty"` + StringOption string `json:"stringOption,omitempty"` + Sub TestOptionSubStruct `json:"sub,omitempty"` // Check that embedded fields can be unmarshalled - TestOptionSubStruct `yaml:",inline,squash"` + TestOptionSubStruct `json:",inline,squash"` } var testOptionsConfigBytesFull = []byte(` @@ -416,7 +416,7 @@ sub: err := LoadYAML(configFileName, input) if in.expectedErr != nil { - Expect(err).To(MatchError(in.expectedErr.Error())) + Expect(err).To(MatchError(ContainSubstring(in.expectedErr.Error()))) } else { Expect(err).ToNot(HaveOccurred()) } @@ -459,13 +459,13 @@ sub: StringSliceOption: []string{"a", "b", "c"}, }, }, - expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: unknown field \"foo\""), + expectedErr: errors.New("has invalid keys: foo"), }), Entry("with an incorrect type for a string field", loadYAMLTableInput{ configFile: []byte(`stringOption: ["a", "b"]`), input: &TestOptions{}, expectedOutput: &TestOptions{}, - 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"), + expectedErr: errors.New("'stringOption' expected type 'string', got unconvertible type"), }), Entry("with an incorrect type for an array field", loadYAMLTableInput{ configFile: []byte(`stringSliceOption: "a"`), @@ -526,11 +526,13 @@ upstreamConfig: injectRequestHeaders: - name: X-Forwarded-User values: - - claim: user + - claimSource: + claim: user injectResponseHeaders: - name: X-Secret values: - - value: c2VjcmV0 + - secretSource: + value: secret `) By("Creating a config file") @@ -548,7 +550,7 @@ injectResponseHeaders: into := &AlphaOptions{} Expect(LoadYAML(configFileName, into)).To(Succeed()) - flushInterval := Duration(500 * time.Millisecond) + flushInterval := 500 * time.Millisecond Expect(into).To(Equal(&AlphaOptions{ UpstreamConfig: UpstreamConfig{ @@ -579,7 +581,7 @@ injectResponseHeaders: Values: []HeaderValue{ { SecretSource: &SecretSource{ - Value: []byte("secret"), + Value: "secret", }, }, }, diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index 0f254575..c94b6b92 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -238,16 +238,16 @@ type OIDCOptions struct { IssuerURL string `json:"issuerURL,omitempty"` // InsecureAllowUnverifiedEmail prevents failures if an email address in an id_token is not verified // default set to 'false' - InsecureAllowUnverifiedEmail bool `json:"insecureAllowUnverifiedEmail,omitempty"` + InsecureAllowUnverifiedEmail bool `json:"insecureAllowUnverifiedEmail"` // InsecureSkipIssuerVerification skips verification of ID token issuers. When false, ID Token Issuers must match the OIDC discovery URL // default set to 'false' - InsecureSkipIssuerVerification bool `json:"insecureSkipIssuerVerification,omitempty"` + InsecureSkipIssuerVerification bool `json:"insecureSkipIssuerVerification"` // InsecureSkipNonce skips verifying the ID Token's nonce claim that must match // the random nonce sent in the initial OAuth flow. Otherwise, the nonce is checked // after the initial OAuth redeem & subsequent token refreshes. // default set to 'true' // Warning: In a future release, this will change to 'false' by default for enhanced security. - InsecureSkipNonce bool `json:"insecureSkipNonce,omitempty"` + InsecureSkipNonce bool `json:"insecureSkipNonce"` // SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints // default set to 'false' SkipDiscovery bool `json:"skipDiscovery,omitempty"` diff --git a/pkg/apis/options/secret_source.go b/pkg/apis/options/secret_source.go new file mode 100644 index 00000000..2be4d890 --- /dev/null +++ b/pkg/apis/options/secret_source.go @@ -0,0 +1,14 @@ +package options + +// SecretSource references an individual secret value. +// Only one source within the struct should be defined at any time. +type SecretSource struct { + // Value expects a base64 encoded string value. + Value string `json:"value,omitempty"` + + // FromEnv expects the name of an environment variable. + FromEnv string `json:"fromEnv,omitempty"` + + // FromFile expects a path to a file containing the secret value. + FromFile string `json:"fromFile,omitempty"` +} diff --git a/pkg/apis/options/upstreams.go b/pkg/apis/options/upstreams.go index b3c7195f..1002ae07 100644 --- a/pkg/apis/options/upstreams.go +++ b/pkg/apis/options/upstreams.go @@ -79,7 +79,7 @@ type Upstream struct { // FlushInterval is the period between flushing the response buffer when // streaming response from the upstream. // Defaults to 1 second. - FlushInterval *Duration `json:"flushInterval,omitempty"` + FlushInterval *time.Duration `json:"flushInterval,omitempty"` // PassHostHeader determines whether the request host header should be proxied // to the upstream server. @@ -92,7 +92,7 @@ type Upstream struct { // Timeout is the maximum duration the server will wait for a response from the upstream server. // Defaults to 30 seconds. - Timeout *Duration `json:"timeout,omitempty"` + Timeout *time.Duration `json:"timeout,omitempty"` // DisableKeepAlives disables HTTP keep-alive connections to the upstream server. // Defaults to false. diff --git a/pkg/apis/options/util/util.go b/pkg/apis/options/util/util.go index 03f0a134..794a6e91 100644 --- a/pkg/apis/options/util/util.go +++ b/pkg/apis/options/util/util.go @@ -11,7 +11,7 @@ import ( func GetSecretValue(source *options.SecretSource) ([]byte, error) { switch { case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "": - return source.Value, nil + return []byte(source.Value), nil case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "": return []byte(os.Getenv(source.FromEnv)), nil case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "": diff --git a/pkg/apis/options/util/util_test.go b/pkg/apis/options/util/util_test.go index e84db1ec..5c4bfa6d 100644 --- a/pkg/apis/options/util/util_test.go +++ b/pkg/apis/options/util/util_test.go @@ -31,7 +31,7 @@ var _ = Describe("GetSecretValue", func() { It("returns the correct value from the string value", func() { value, err := GetSecretValue(&options.SecretSource{ - Value: []byte("secret-value-1"), + Value: "secret-value-1", }) Expect(err).ToNot(HaveOccurred()) Expect(string(value)).To(Equal("secret-value-1")) diff --git a/pkg/header/injector_test.go b/pkg/header/injector_test.go index 25c276dc..bb37261d 100644 --- a/pkg/header/injector_test.go +++ b/pkg/header/injector_test.go @@ -55,7 +55,7 @@ var _ = Describe("Injector Suite", func() { Values: []options.HeaderValue{ { SecretSource: &options.SecretSource{ - Value: []byte("super-secret"), + Value: "super-secret", }, }, }, @@ -199,7 +199,7 @@ var _ = Describe("Injector Suite", func() { ClaimSource: &options.ClaimSource{ Claim: "user", BasicAuthPassword: &options.SecretSource{ - Value: []byte("basic-password"), + Value: "basic-password", }, }, }, @@ -227,7 +227,7 @@ var _ = Describe("Injector Suite", func() { ClaimSource: &options.ClaimSource{ Claim: "user", BasicAuthPassword: &options.SecretSource{ - Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + Value: base64.StdEncoding.EncodeToString([]byte("basic-password")), }, }, }, @@ -322,7 +322,7 @@ var _ = Describe("Injector Suite", func() { ClaimSource: &options.ClaimSource{ Claim: "user", BasicAuthPassword: &options.SecretSource{ - Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + Value: base64.StdEncoding.EncodeToString([]byte("basic-password")), FromEnv: "SECRET_ENV", }, }, @@ -348,7 +348,7 @@ var _ = Describe("Injector Suite", func() { ClaimSource: &options.ClaimSource{ Claim: "user", BasicAuthPassword: &options.SecretSource{ - Value: []byte("basic-password"), + Value: "basic-password", }, }, }, @@ -379,17 +379,17 @@ var _ = Describe("Injector Suite", func() { Values: []options.HeaderValue{ { SecretSource: &options.SecretSource{ - Value: []byte("major=1"), + Value: "major=1", }, }, { SecretSource: &options.SecretSource{ - Value: []byte("minor=2"), + Value: "minor=2", }, }, { SecretSource: &options.SecretSource{ - Value: []byte("patch=3"), + Value: "patch=3", }, }, }, diff --git a/pkg/http/http_suite_test.go b/pkg/http/http_suite_test.go index 19d4d3ff..219f26ea 100644 --- a/pkg/http/http_suite_test.go +++ b/pkg/http/http_suite_test.go @@ -48,10 +48,10 @@ var _ = BeforeSuite(func() { certOut := new(bytes.Buffer) Expect(pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})).To(Succeed()) - ipv4CertDataSource.Value = certOut.Bytes() + ipv4CertDataSource.Value = certOut.String() keyOut := new(bytes.Buffer) Expect(pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})).To(Succeed()) - ipv4KeyDataSource.Value = keyOut.Bytes() + ipv4KeyDataSource.Value = keyOut.String() }) By("Generating a ipv6 self-signed cert for TLS tests", func() { @@ -61,16 +61,16 @@ var _ = BeforeSuite(func() { certOut := new(bytes.Buffer) Expect(pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})).To(Succeed()) - ipv6CertDataSource.Value = certOut.Bytes() + ipv6CertDataSource.Value = certOut.String() keyOut := new(bytes.Buffer) Expect(pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})).To(Succeed()) - ipv6KeyDataSource.Value = keyOut.Bytes() + ipv6KeyDataSource.Value = keyOut.String() }) By("Setting up a http client", func() { - ipv4cert, err := tls.X509KeyPair(ipv4CertDataSource.Value, ipv4KeyDataSource.Value) + ipv4cert, err := tls.X509KeyPair([]byte(ipv4CertDataSource.Value), []byte(ipv4KeyDataSource.Value)) Expect(err).ToNot(HaveOccurred()) - ipv6cert, err := tls.X509KeyPair(ipv6CertDataSource.Value, ipv6KeyDataSource.Value) + ipv6cert, err := tls.X509KeyPair([]byte(ipv6CertDataSource.Value), []byte(ipv6KeyDataSource.Value)) Expect(err).ToNot(HaveOccurred()) ipv4certificate, err := x509.ParseCertificate(ipv4cert.Certificate[0]) diff --git a/pkg/http/server_test.go b/pkg/http/server_test.go index 8dfa13af..6584b757 100644 --- a/pkg/http/server_test.go +++ b/pkg/http/server_test.go @@ -234,7 +234,7 @@ var _ = Describe("Server", func() { SecureBindAddress: "127.0.0.1:0", TLS: &options.TLS{ Key: &options.SecretSource{ - Value: []byte("invalid"), + Value: "invalid", }, Cert: &ipv4CertDataSource, }, @@ -250,7 +250,7 @@ var _ = Describe("Server", func() { TLS: &options.TLS{ Key: &ipv4KeyDataSource, Cert: &options.SecretSource{ - Value: []byte("invalid"), + Value: "invalid", }, }, }, @@ -506,7 +506,7 @@ var _ = Describe("Server", func() { SecureBindAddress: "[::1]:0", TLS: &options.TLS{ Key: &options.SecretSource{ - Value: []byte("invalid"), + Value: "invalid", }, Cert: &ipv6CertDataSource, }, @@ -523,7 +523,7 @@ var _ = Describe("Server", func() { TLS: &options.TLS{ Key: &ipv6KeyDataSource, Cert: &options.SecretSource{ - Value: []byte("invalid"), + Value: "invalid", }, }, }, diff --git a/pkg/middleware/headers_test.go b/pkg/middleware/headers_test.go index 06440eea..b1848ae0 100644 --- a/pkg/middleware/headers_test.go +++ b/pkg/middleware/headers_test.go @@ -188,7 +188,7 @@ var _ = Describe("Headers Suite", func() { ClaimSource: &options.ClaimSource{ Claim: "user", BasicAuthPassword: &options.SecretSource{ - Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + Value: base64.StdEncoding.EncodeToString([]byte("basic-password")), FromEnv: "SECRET_ENV", }, }, @@ -260,7 +260,7 @@ var _ = Describe("Headers Suite", func() { Values: []options.HeaderValue{ { SecretSource: &options.SecretSource{ - Value: []byte("_oauth2_proxy=ey123123123"), + Value: "_oauth2_proxy=ey123123123", }, }, }, @@ -270,7 +270,7 @@ var _ = Describe("Headers Suite", func() { Values: []options.HeaderValue{ { SecretSource: &options.SecretSource{ - Value: []byte("oauth_user"), + Value: "oauth_user", }, }, }, @@ -416,7 +416,7 @@ var _ = Describe("Headers Suite", func() { ClaimSource: &options.ClaimSource{ Claim: "user", BasicAuthPassword: &options.SecretSource{ - Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + Value: base64.StdEncoding.EncodeToString([]byte("basic-password")), FromEnv: "SECRET_ENV", }, }, diff --git a/pkg/upstream/http.go b/pkg/upstream/http.go index 7a0e6e84..d9d8f152 100644 --- a/pkg/upstream/http.go +++ b/pkg/upstream/http.go @@ -137,12 +137,12 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr // Change default duration for waiting for an upstream response if upstream.Timeout != nil { - transport.ResponseHeaderTimeout = upstream.Timeout.Duration() + transport.ResponseHeaderTimeout = *upstream.Timeout } // Configure options on the SingleHostReverseProxy if upstream.FlushInterval != nil { - proxy.FlushInterval = upstream.FlushInterval.Duration() + proxy.FlushInterval = *upstream.FlushInterval } else { proxy.FlushInterval = options.DefaultUpstreamFlushInterval } diff --git a/pkg/upstream/http_test.go b/pkg/upstream/http_test.go index 31476df0..df264c33 100644 --- a/pkg/upstream/http_test.go +++ b/pkg/upstream/http_test.go @@ -21,8 +21,8 @@ import ( ) var _ = Describe("HTTP Upstream Suite", func() { - defaultFlushInterval := options.Duration(options.DefaultUpstreamFlushInterval) - defaultTimeout := options.Duration(options.DefaultUpstreamTimeout) + defaultFlushInterval := options.DefaultUpstreamFlushInterval + defaultTimeout := options.DefaultUpstreamTimeout truth := true falsum := false @@ -57,9 +57,9 @@ var _ = Describe("HTTP Upstream Suite", func() { req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{}) rw := httptest.NewRecorder() - flush := options.Duration(1 * time.Second) + flush := 1 * time.Second - timeout := options.Duration(options.DefaultUpstreamTimeout) + timeout := options.DefaultUpstreamTimeout upstream := options.Upstream{ ID: in.id, @@ -373,11 +373,11 @@ var _ = Describe("HTTP Upstream Suite", func() { type newUpstreamTableInput struct { proxyWebSockets bool - flushInterval options.Duration + flushInterval time.Duration skipVerify bool sigData *options.SignatureData errorHandler func(http.ResponseWriter, *http.Request, error) - timeout options.Duration + timeout time.Duration disableKeepAlives bool } @@ -406,10 +406,10 @@ var _ = Describe("HTTP Upstream Suite", func() { proxy, ok := upstreamProxy.handler.(*httputil.ReverseProxy) Expect(ok).To(BeTrue()) - Expect(proxy.FlushInterval).To(Equal(in.flushInterval.Duration())) + Expect(proxy.FlushInterval).To(Equal(in.flushInterval)) transport, ok := proxy.Transport.(*http.Transport) Expect(ok).To(BeTrue()) - Expect(transport.ResponseHeaderTimeout).To(Equal(in.timeout.Duration())) + Expect(transport.ResponseHeaderTimeout).To(Equal(in.timeout)) Expect(proxy.ErrorHandler != nil).To(Equal(in.errorHandler != nil)) if in.skipVerify { Expect(transport.TLSClientConfig.InsecureSkipVerify).To(Equal(true)) @@ -428,7 +428,7 @@ var _ = Describe("HTTP Upstream Suite", func() { }), Entry("with a non standard flush interval", &newUpstreamTableInput{ proxyWebSockets: false, - flushInterval: options.Duration(5 * time.Second), + flushInterval: 5 * time.Second, skipVerify: false, sigData: nil, errorHandler: nil, @@ -466,7 +466,7 @@ var _ = Describe("HTTP Upstream Suite", func() { skipVerify: false, sigData: nil, errorHandler: nil, - timeout: options.Duration(5 * time.Second), + timeout: 5 * time.Second, }), Entry("with a DisableKeepAlives", &newUpstreamTableInput{ proxyWebSockets: false, @@ -483,8 +483,8 @@ var _ = Describe("HTTP Upstream Suite", func() { var proxyServer *httptest.Server BeforeEach(func() { - flush := options.Duration(1 * time.Second) - timeout := options.Duration(options.DefaultUpstreamTimeout) + flush := 1 * time.Second + timeout := options.DefaultUpstreamTimeout upstream := options.Upstream{ ID: "websocketProxy", PassHostHeader: &truth, diff --git a/pkg/validation/common_test.go b/pkg/validation/common_test.go index 9e873c35..bb7c2dd6 100644 --- a/pkg/validation/common_test.go +++ b/pkg/validation/common_test.go @@ -9,12 +9,12 @@ import ( ) var _ = Describe("Common", func() { - var validSecretSourceValue []byte + var validSecretSourceValue string const validSecretSourceEnv = "OAUTH2_PROXY_TEST_SECRET_SOURCE_ENV" var validSecretSourceFile string BeforeEach(func() { - validSecretSourceValue = []byte("This is a secret source value") + validSecretSourceValue = "This is a secret source value" Expect(os.Setenv(validSecretSourceEnv, "This is a secret source env")).To(Succeed()) tmp, err := os.CreateTemp("", "oauth2-proxy-secret-source-test") Expect(err).ToNot(HaveOccurred()) diff --git a/pkg/validation/header.go b/pkg/validation/header.go index b1258144..713113f4 100644 --- a/pkg/validation/header.go +++ b/pkg/validation/header.go @@ -51,11 +51,9 @@ func validateHeaderValue(_ string, value options.HeaderValue) []string { func validateHeaderValueClaimSource(claim options.ClaimSource) []string { msgs := []string{} - if claim.Claim == "" { msgs = append(msgs, "claim should not be empty") } - if claim.BasicAuthPassword != nil { msgs = append(msgs, prefixValues("invalid basicAuthPassword: ", validateSecretSource(*claim.BasicAuthPassword))...) } diff --git a/pkg/validation/header_test.go b/pkg/validation/header_test.go index 2d9ef6dd..88849ea1 100644 --- a/pkg/validation/header_test.go +++ b/pkg/validation/header_test.go @@ -30,7 +30,7 @@ var _ = Describe("Headers", func() { Values: []options.HeaderValue{ { SecretSource: &options.SecretSource{ - Value: []byte(base64.StdEncoding.EncodeToString([]byte("secret"))), + Value: base64.StdEncoding.EncodeToString([]byte("secret")), }, }, }, @@ -43,7 +43,7 @@ var _ = Describe("Headers", func() { ClaimSource: &options.ClaimSource{ Claim: "email", BasicAuthPassword: &options.SecretSource{ - Value: []byte(base64.StdEncoding.EncodeToString([]byte("secret"))), + Value: base64.StdEncoding.EncodeToString([]byte("secret")), }, }, }, diff --git a/pkg/validation/upstreams.go b/pkg/validation/upstreams.go index bafed628..52facb4d 100644 --- a/pkg/validation/upstreams.go +++ b/pkg/validation/upstreams.go @@ -69,7 +69,7 @@ func validateStaticUpstream(upstream options.Upstream) []string { 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.Duration() != options.DefaultUpstreamFlushInterval { + if upstream.FlushInterval != nil && *upstream.FlushInterval != options.DefaultUpstreamFlushInterval { msgs = append(msgs, fmt.Sprintf("upstream %q has flushInterval, but is a static upstream, this will have no effect.", upstream.ID)) } if upstream.PassHostHeader != nil { diff --git a/pkg/validation/upstreams_test.go b/pkg/validation/upstreams_test.go index fe431d27..67991b76 100644 --- a/pkg/validation/upstreams_test.go +++ b/pkg/validation/upstreams_test.go @@ -14,7 +14,7 @@ var _ = Describe("Upstreams", func() { errStrings []string } - flushInterval := options.Duration(5 * time.Second) + flushInterval := 5 * time.Second staticCode200 := 200 truth := true