Introduce alpha configuration loading

This commit is contained in:
Joel Speed 2020-11-09 20:17:43 +00:00
parent 5b003a5657
commit f36dfbb494
No known key found for this signature in database
GPG Key ID: 6E80578D6751DEFB
10 changed files with 550 additions and 21 deletions

1
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/frankban/quicktest v1.10.0 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
github.com/go-redis/redis/v8 v8.2.3
github.com/justinas/alice v1.2.0
github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa

3
go.sum
View File

@ -64,7 +64,10 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=

96
main.go
View File

@ -12,36 +12,26 @@ import (
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation"
"github.com/spf13/pflag"
)
func main() {
logger.SetFlags(logger.Lshortfile)
flagSet := options.NewFlagSet()
config := flagSet.String("config", "", "path to config file")
showVersion := flagSet.Bool("version", false, "print version string")
err := flagSet.Parse(os.Args[1:])
if err != nil {
logger.Printf("ERROR: Failed to parse flags: %v", err)
os.Exit(1)
}
configFlagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ContinueOnError)
config := configFlagSet.String("config", "", "path to config file")
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)")
showVersion := configFlagSet.Bool("version", false, "print version string")
configFlagSet.Parse(os.Args[1:])
if *showVersion {
fmt.Printf("oauth2-proxy %s (built with %s)\n", VERSION, runtime.Version())
return
}
legacyOpts := options.NewLegacyOptions()
err = options.Load(*config, flagSet, legacyOpts)
opts, err := loadConfiguration(*config, *alphaConfig, configFlagSet, os.Args[1:])
if err != nil {
logger.Errorf("ERROR: Failed to load config: %v", err)
os.Exit(1)
}
opts, err := legacyOpts.ToOptions()
if err != nil {
logger.Errorf("ERROR: Failed to convert config: %v", err)
logger.Printf("ERROR: %v", err)
os.Exit(1)
}
@ -74,3 +64,73 @@ func main() {
}()
s.ListenAndServe()
}
// 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)
}
return loadLegacyOptions(config, extraFlags, args)
}
// loadLegacyOptions loads the old toml options using the legacy flagset
// and legacy options struct.
func loadLegacyOptions(config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
optionsFlagSet := options.NewLegacyFlagSet()
optionsFlagSet.AddFlagSet(extraFlags)
if err := optionsFlagSet.Parse(args); err != nil {
return nil, fmt.Errorf("failed to parse flags: %v", err)
}
legacyOpts := options.NewLegacyOptions()
if err := options.Load(config, optionsFlagSet, legacyOpts); err != nil {
return nil, fmt.Errorf("failed to load config: %v", err)
}
opts, err := legacyOpts.ToOptions()
if err != nil {
return nil, fmt.Errorf("failed to convert config: %v", err)
}
return opts, nil
}
// loadAlphaOptions 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) {
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 {
return nil, fmt.Errorf("failed to load alpha options: %v", err)
}
alphaOpts.MergeInto(opts)
return opts, nil
}
// loadOptions loads the configuration using the old style format into the
// core options.Options struct.
// This means that none of the options that have been converted to alpha config
// will be loaded using this method.
func loadOptions(config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) {
optionsFlagSet := options.NewFlagSet()
optionsFlagSet.AddFlagSet(extraFlags)
if err := optionsFlagSet.Parse(args); err != nil {
return nil, fmt.Errorf("failed to parse flags: %v", err)
}
opts := options.NewOptions()
if err := options.Load(config, optionsFlagSet, opts); err != nil {
return nil, fmt.Errorf("failed to load config: %v", err)
}
return opts, nil
}

16
main_suite_test.go Normal file
View File

@ -0,0 +1,16 @@
package main
import (
"testing"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestMainSuite(t *testing.T) {
logger.SetOutput(GinkgoWriter)
RegisterFailHandler(Fail)
RunSpecs(t, "Main Suite")
}

215
main_test.go Normal file
View File

@ -0,0 +1,215 @@
package main
import (
"errors"
"io/ioutil"
"os"
"time"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
"github.com/spf13/pflag"
)
var _ = Describe("Configuration Loading Suite", func() {
const testLegacyConfig = `
upstreams="http://httpbin"
set_basic_auth="true"
basic_auth_password="super-secret-password"
`
const testAlphaConfig = `
upstreams:
- id: /
path: /
uri: http://httpbin
flushInterval: 1s
passHostHeader: true
proxyWebSockets: true
injectRequestHeaders:
- name: Authorization
values:
- claim: user
prefix: "Basic "
basicAuthPassword:
value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
- name: X-Forwarded-Groups
values:
- claim: groups
- name: X-Forwarded-User
values:
- claim: user
- name: X-Forwarded-Email
values:
- claim: email
- name: X-Forwarded-Preferred-Username
values:
- claim: preferred_username
injectResponseHeaders:
- name: Authorization
values:
- claim: user
prefix: "Basic "
basicAuthPassword:
value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk
`
const testCoreConfig = `
http_address="0.0.0.0:4180"
cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
provider="oidc"
email_domains="example.com"
oidc_issuer_url="http://dex.localhost:4190/dex"
client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
client_id="oauth2-proxy"
cookie_secure="false"
redirect_url="http://localhost:4180/oauth2/callback"
`
boolPtr := func(b bool) *bool {
return &b
}
durationPtr := func(d time.Duration) *options.Duration {
du := options.Duration(d)
return &du
}
testExpectedOptions := func() *options.Options {
opts, err := options.NewLegacyOptions().ToOptions()
Expect(err).ToNot(HaveOccurred())
opts.HTTPAddress = "0.0.0.0:4180"
opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
opts.ProviderType = "oidc"
opts.EmailDomains = []string{"example.com"}
opts.OIDCIssuerURL = "http://dex.localhost:4190/dex"
opts.ClientSecret = "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
opts.ClientID = "oauth2-proxy"
opts.Cookie.Secure = false
opts.RawRedirectURL = "http://localhost:4180/oauth2/callback"
opts.UpstreamServers = options.Upstreams{
{
ID: "/",
Path: "/",
URI: "http://httpbin",
FlushInterval: durationPtr(options.DefaultUpstreamFlushInterval),
PassHostHeader: boolPtr(true),
ProxyWebSockets: boolPtr(true),
},
}
authHeader := options.Header{
Name: "Authorization",
Values: []options.HeaderValue{
{
ClaimSource: &options.ClaimSource{
Claim: "user",
Prefix: "Basic ",
BasicAuthPassword: &options.SecretSource{
Value: []byte("super-secret-password"),
},
},
},
},
}
opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...)
opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader)
return opts
}
type loadConfigurationTableInput struct {
configContent string
alphaConfigContent string
args []string
extraFlags func() *pflag.FlagSet
expectedOptions func() *options.Options
expectedErr error
}
DescribeTable("LoadConfiguration",
func(in loadConfigurationTableInput) {
var configFileName, alphaConfigFileName string
defer func() {
if configFileName != "" {
Expect(os.Remove(configFileName)).To(Succeed())
}
if alphaConfigFileName != "" {
Expect(os.Remove(alphaConfigFileName)).To(Succeed())
}
}()
if in.configContent != "" {
By("Writing the config to a temporary file", func() {
file, err := ioutil.TempFile("", "oauth2-proxy-test-config-XXXX.cfg")
Expect(err).ToNot(HaveOccurred())
defer file.Close()
configFileName = file.Name()
_, err = file.WriteString(in.configContent)
Expect(err).ToNot(HaveOccurred())
})
}
if in.alphaConfigContent != "" {
By("Writing the config to a temporary file", func() {
file, err := ioutil.TempFile("", "oauth2-proxy-test-alpha-config-XXXX.yaml")
Expect(err).ToNot(HaveOccurred())
defer file.Close()
alphaConfigFileName = file.Name()
_, err = file.WriteString(in.alphaConfigContent)
Expect(err).ToNot(HaveOccurred())
})
}
extraFlags := pflag.NewFlagSet("test-flagset", pflag.ExitOnError)
if in.extraFlags != nil {
extraFlags = in.extraFlags()
}
opts, err := loadConfiguration(configFileName, alphaConfigFileName, extraFlags, in.args)
if in.expectedErr != nil {
Expect(err).To(MatchError(in.expectedErr.Error()))
} else {
Expect(err).ToNot(HaveOccurred())
}
Expect(in.expectedOptions).ToNot(BeNil())
Expect(opts).To(Equal(in.expectedOptions()))
},
Entry("with legacy configuration", loadConfigurationTableInput{
configContent: testCoreConfig + testLegacyConfig,
expectedOptions: testExpectedOptions,
}),
Entry("with alpha configuration", loadConfigurationTableInput{
configContent: testCoreConfig,
alphaConfigContent: testAlphaConfig,
expectedOptions: testExpectedOptions,
}),
Entry("with bad legacy configuration", loadConfigurationTableInput{
configContent: testCoreConfig + "unknown_field=\"something\"",
expectedOptions: func() *options.Options { return nil },
expectedErr: errors.New("failed to load config: error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_field"),
}),
Entry("with bad alpha configuration", loadConfigurationTableInput{
configContent: testCoreConfig,
alphaConfigContent: testAlphaConfig + ":",
expectedOptions: func() *options.Options { return nil },
expectedErr: errors.New("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line 34: did not find expected key"),
}),
Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{
configContent: testCoreConfig + "unknown_field=\"something\"",
alphaConfigContent: testAlphaConfig,
expectedOptions: func() *options.Options { return nil },
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"),
}),
)
})

View File

@ -29,3 +29,11 @@ type AlphaOptions struct {
// or from a static secret value.
InjectResponseHeaders []Header `json:"injectResponseHeaders,omitempty"`
}
// MergeInto replaces alpha options in the Options struct with the values
// from the AlphaOptions
func (a *AlphaOptions) MergeInto(opts *Options) {
opts.UpstreamServers = a.Upstreams
opts.InjectRequestHeaders = a.InjectRequestHeaders
opts.InjectResponseHeaders = a.InjectResponseHeaders
}

View File

@ -39,6 +39,15 @@ func NewLegacyOptions() *LegacyOptions {
}
}
func NewLegacyFlagSet() *pflag.FlagSet {
flagSet := NewFlagSet()
flagSet.AddFlagSet(legacyUpstreamsFlagSet())
flagSet.AddFlagSet(legacyHeadersFlagSet())
return flagSet
}
func (l *LegacyOptions) ToOptions() (*Options, error) {
upstreams, err := l.LegacyUpstreams.convert()
if err != nil {

View File

@ -1,10 +1,13 @@
package options
import (
"errors"
"fmt"
"io/ioutil"
"reflect"
"strings"
"github.com/ghodss/yaml"
"github.com/mitchellh/mapstructure"
"github.com/spf13/pflag"
"github.com/spf13/viper"
@ -132,3 +135,28 @@ func isUnexported(name string) bool {
first := string(name[0])
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 {
v := viper.New()
v.SetConfigFile(configFileName)
v.SetConfigType("yaml")
v.SetTypeByDefaultValue(true)
if configFileName == "" {
return errors.New("no configuration file provided")
}
data, err := ioutil.ReadFile(configFileName)
if err != nil {
return fmt.Errorf("unable to load config file: %w", err)
}
// UnmarshalStrict will return an error if the config includes options that are
// not mapped to felds of the into struct
if err := yaml.UnmarshalStrict(data, into, yaml.DisallowUnknownFields); err != nil {
return fmt.Errorf("error unmarshalling config: %w", err)
}
return nil
}

View File

@ -1,9 +1,11 @@
package options
import (
"errors"
"fmt"
"io/ioutil"
"os"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
@ -295,10 +297,199 @@ var _ = Describe("Load", func() {
expectedOutput: NewOptions(),
}),
Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{
flagSet: NewFlagSet,
flagSet: NewLegacyFlagSet,
input: &LegacyOptions{},
expectedOutput: NewLegacyOptions(),
}),
)
})
})
var _ = Describe("LoadYAML", func() {
Context("with a testOptions structure", func() {
type TestOptionSubStruct struct {
StringSliceOption []string `yaml:"stringSliceOption,omitempty"`
}
type TestOptions struct {
StringOption string `yaml:"stringOption,omitempty"`
Sub TestOptionSubStruct `yaml:"sub,omitempty"`
// Check that embedded fields can be unmarshalled
TestOptionSubStruct `yaml:",inline,squash"`
}
var testOptionsConfigBytesFull = []byte(`
stringOption: foo
stringSliceOption:
- a
- b
- c
sub:
stringSliceOption:
- d
- e
`)
type loadYAMLTableInput struct {
configFile []byte
input interface{}
expectedErr error
expectedOutput interface{}
}
DescribeTable("LoadYAML",
func(in loadYAMLTableInput) {
var configFileName string
if in.configFile != nil {
By("Creating a config file")
configFile, err := ioutil.TempFile("", "oauth2-proxy-test-config-file")
Expect(err).ToNot(HaveOccurred())
defer configFile.Close()
_, err = configFile.Write(in.configFile)
Expect(err).ToNot(HaveOccurred())
defer os.Remove(configFile.Name())
configFileName = configFile.Name()
}
var input interface{}
if in.input != nil {
input = in.input
} else {
input = &TestOptions{}
}
err := LoadYAML(configFileName, input)
if in.expectedErr != nil {
Expect(err).To(MatchError(in.expectedErr.Error()))
} else {
Expect(err).ToNot(HaveOccurred())
}
Expect(input).To(Equal(in.expectedOutput))
},
Entry("with a valid input", loadYAMLTableInput{
configFile: testOptionsConfigBytesFull,
input: &TestOptions{},
expectedOutput: &TestOptions{
StringOption: "foo",
Sub: TestOptionSubStruct{
StringSliceOption: []string{"d", "e"},
},
TestOptionSubStruct: TestOptionSubStruct{
StringSliceOption: []string{"a", "b", "c"},
},
},
}),
Entry("with no config file", loadYAMLTableInput{
configFile: nil,
input: &TestOptions{},
expectedOutput: &TestOptions{},
expectedErr: errors.New("no configuration file provided"),
}),
Entry("with invalid YAML", loadYAMLTableInput{
configFile: []byte("\tfoo: bar"),
input: &TestOptions{},
expectedOutput: &TestOptions{},
expectedErr: errors.New("error unmarshalling config: error converting YAML to JSON: yaml: found character that cannot start any token"),
}),
Entry("with extra fields in the YAML", loadYAMLTableInput{
configFile: append(testOptionsConfigBytesFull, []byte("foo: bar\n")...),
input: &TestOptions{},
expectedOutput: &TestOptions{
StringOption: "foo",
Sub: TestOptionSubStruct{
StringSliceOption: []string{"d", "e"},
},
TestOptionSubStruct: TestOptionSubStruct{
StringSliceOption: []string{"a", "b", "c"},
},
},
expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: unknown field \"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"),
}),
Entry("with an incorrect type for an array field", loadYAMLTableInput{
configFile: []byte(`stringSliceOption: "a"`),
input: &TestOptions{},
expectedOutput: &TestOptions{},
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"),
}),
)
})
It("should load a full example AlphaOptions", func() {
config := []byte(`
upstreams:
- id: httpbin
path: /
uri: http://httpbin
flushInterval: 500ms
injectRequestHeaders:
- name: X-Forwarded-User
values:
- claim: user
injectResponseHeaders:
- name: X-Secret
values:
- value: c2VjcmV0
`)
By("Creating a config file")
configFile, err := ioutil.TempFile("", "oauth2-proxy-test-alpha-config-file")
Expect(err).ToNot(HaveOccurred())
defer configFile.Close()
_, err = configFile.Write(config)
Expect(err).ToNot(HaveOccurred())
defer os.Remove(configFile.Name())
configFileName := configFile.Name()
By("Loading the example config")
into := &AlphaOptions{}
Expect(LoadYAML(configFileName, into)).To(Succeed())
flushInterval := Duration(500 * time.Millisecond)
Expect(into).To(Equal(&AlphaOptions{
Upstreams: []Upstream{
{
ID: "httpbin",
Path: "/",
URI: "http://httpbin",
FlushInterval: &flushInterval,
},
},
InjectRequestHeaders: []Header{
{
Name: "X-Forwarded-User",
Values: []HeaderValue{
{
ClaimSource: &ClaimSource{
Claim: "user",
},
},
},
},
},
InjectResponseHeaders: []Header{
{
Name: "X-Secret",
Values: []HeaderValue{
{
SecretSource: &SecretSource{
Value: []byte("secret"),
},
},
},
},
},
}))
})
})

View File

@ -246,8 +246,6 @@ func NewFlagSet() *pflag.FlagSet {
flagSet.AddFlagSet(cookieFlagSet())
flagSet.AddFlagSet(loggingFlagSet())
flagSet.AddFlagSet(legacyUpstreamsFlagSet())
flagSet.AddFlagSet(legacyHeadersFlagSet())
return flagSet
}