From 31fb61462fda076d5bb89118616e0b066fde3708 Mon Sep 17 00:00:00 2001 From: Justin Hines Date: Wed, 5 Jun 2019 17:23:35 -0400 Subject: [PATCH] sso-auth: refactor configuration using go-micro to allow multiple providers --- cmd/sso-auth/main.go | 23 +- go.mod | 15 +- go.sum | 359 ++++++++++++++-- internal/auth/authenticator.go | 54 +-- internal/auth/authenticator_test.go | 183 ++++---- internal/auth/configuration.go | 512 +++++++++++++++++++++++ internal/auth/configuration_test.go | 279 ++++++++++++ internal/auth/metrics_test.go | 11 +- internal/auth/middleware_test.go | 64 ++- internal/auth/mux.go | 55 +-- internal/auth/mux_test.go | 64 +-- internal/auth/options.go | 361 ++++------------ internal/auth/options_test.go | 103 ----- internal/auth/providers/google.go | 14 +- internal/auth/providers/google_admin.go | 7 +- internal/auth/providers/test_provider.go | 20 +- internal/auth/static_files_test.go | 6 +- internal/proxy/options.go | 49 ++- internal/proxy/options_test.go | 38 +- internal/proxy/proxy.go | 7 +- internal/proxy/proxy_config.go | 3 + 21 files changed, 1417 insertions(+), 810 deletions(-) create mode 100644 internal/auth/configuration.go create mode 100644 internal/auth/configuration_test.go delete mode 100644 internal/auth/options_test.go diff --git a/cmd/sso-auth/main.go b/cmd/sso-auth/main.go index 367ea33c..72a4a485 100644 --- a/cmd/sso-auth/main.go +++ b/cmd/sso-auth/main.go @@ -16,25 +16,26 @@ func init() { func main() { logger := log.NewLogEntry() - opts, err := auth.NewOptions() + config, err := auth.LoadConfig() if err != nil { - logger.Error(err, "error loading in env vars") + logger.Error(err, "error loading in config from env vars") os.Exit(1) } - err = opts.Validate() + err = config.Validate() if err != nil { - logger.Error(err, "error validating opts") + logger.Error(err, "error validating config") os.Exit(1) } - statsdClient, err := auth.NewStatsdClient(opts.StatsdHost, opts.StatsdPort) + sc := config.MetricsConfig.StatsdConfig + statsdClient, err := auth.NewStatsdClient(sc.Host, sc.Port) if err != nil { logger.Error(err, "error creating statsd client") os.Exit(1) } - authMux, err := auth.NewAuthenticatorMux(opts, statsdClient) + authMux, err := auth.NewAuthenticatorMux(config, statsdClient) if err != nil { logger.Error(err, "error creating new AuthenticatorMux") os.Exit(1) @@ -43,13 +44,13 @@ func main() { // we leave the message field blank, which will inherit the stdlib timeout page which is sufficient // and better than other naive messages we would currently place here - timeoutHandler := http.TimeoutHandler(authMux, opts.RequestTimeout, "") + timeoutHandler := http.TimeoutHandler(authMux, config.ServerConfig.TimeoutConfig.Request, "") s := &http.Server{ - Addr: fmt.Sprintf(":%d", opts.Port), - ReadTimeout: opts.TCPReadTimeout, - WriteTimeout: opts.TCPWriteTimeout, - Handler: auth.NewLoggingHandler(os.Stdout, timeoutHandler, opts.RequestLogging, statsdClient), + Addr: fmt.Sprintf(":%d", config.ServerConfig.Port), + ReadTimeout: config.ServerConfig.TimeoutConfig.Read, + WriteTimeout: config.ServerConfig.TimeoutConfig.Write, + Handler: auth.NewLoggingHandler(os.Stdout, timeoutHandler, config.LoggingConfig.Enable, statsdClient), } logger.Fatal(s.ListenAndServe()) diff --git a/go.mod b/go.mod index cb908a28..61df78d7 100644 --- a/go.mod +++ b/go.mod @@ -2,21 +2,20 @@ module github.com/buzzfeed/sso require ( github.com/18F/hmacauth v0.0.0-20151013130326-9232a6386b73 - github.com/BurntSushi/toml v0.3.1 // indirect github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/datadog/datadog-go v0.0.0-20180822151419-281ae9f2d895 github.com/imdario/mergo v0.3.7 github.com/kelseyhightower/envconfig v1.3.0 - github.com/kr/pretty v0.1.0 // indirect github.com/mccutchen/go-httpbin v1.1.1 + github.com/micro/go-micro v1.5.0 github.com/miscreant/miscreant-go v0.0.0-20181010193435-325cbd69228b + github.com/mitchellh/mapstructure v1.1.2 github.com/rakyll/statik v0.1.6 - github.com/sirupsen/logrus v1.3.0 - github.com/spf13/viper v1.3.2 - golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect - golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1 - golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 - google.golang.org/api v0.1.0 + github.com/sirupsen/logrus v1.4.2 + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/sync v0.0.0-20190423024810-112230192c58 + golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 + google.golang.org/api v0.5.0 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index 0274e116..1cad5d50 100644 --- a/go.sum +++ b/go.sum @@ -1,137 +1,406 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw= +cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= github.com/18F/hmacauth v0.0.0-20151013130326-9232a6386b73 h1:FBZKuR39QOQwCah/AzpwDtgveu3ammIVGsxSKcuThT4= github.com/18F/hmacauth v0.0.0-20151013130326-9232a6386b73/go.mod h1:sOPSg0kyHhwq42XLYw8MpZQ6RpHXcr08rZSfRxg6ZE8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3 h1:wOysYcIdqv3WnvwqFFzrYCFALPED7qkUGaLXu359GSc= github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3/go.mod h1:UMqtWQTnOe4byzwe7Zhwh8f8s+36uszN51sJrSIZlTE= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/datadog/datadog-go v0.0.0-20180822151419-281ae9f2d895 h1:VTq58gB0MvQpLAz2kfeSBchIAi9+zGRMTh+pyfXEoGs= github.com/datadog/datadog-go v0.0.0-20180822151419-281ae9f2d895/go.mod h1:Mo2ZYXXA9Kp6qoXibOPpsSwkwZ67pcianic5+LKUZvY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/forestgiant/sliceutil v0.0.0-20160425183142-94783f95db6c/go.mod h1:pFdJbAhRf7rh6YYMUdIQGyzne6zYL1tCUW8QV2B3UfY= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsouza/go-dockerclient v1.4.1/go.mod h1:PUNHxbowDqRXfRgZqMz1OeGtbWC6VKyZvJ99hDjB0qs= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/gliderlabs/ssh v0.1.4/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-log/log v0.1.0/go.mod h1:4mBwpdRMFLiuXZDCwU2lKQFsoSCo72j3HqBK9d81N2M= +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= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-redsync/redsync v1.2.0/go.mod h1:QClK/s99KRhfKdpxLTMsI5mSu43iLp0NfOneLPie+78= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd/go.mod h1:3LVOLeyx9XVvwPgrt2be44XgSqndprz1G18rSk8KD84= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/joncalhoun/qson v0.0.0-20170526102502-8a9cab3a62b1/go.mod h1:DFXrEwSRX0p/aSvxE21319menCBFeQO0jXpRj7LEZUA= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM= github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/lucas-clemente/quic-go v0.11.2/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= +github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/marten-seemann/qtls v0.2.4/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE= github.com/mccutchen/go-httpbin v1.1.1/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I= +github.com/micro/cli v0.2.0/go.mod h1:jRT9gmfVKWSS6pkKcXQ8YhUyj6bzwxK8Fp5b0Y7qNnk= +github.com/micro/go-micro v1.5.0 h1:nzetg9HEh0MiO2lPmuwR7+QeL3GjpOtBtujRwhHwn0g= +github.com/micro/go-micro v1.5.0/go.mod h1:ZB5riGDNr9XqX9TF8rAbVQ8Smkm1ljuw18I5NHaJiaQ= +github.com/micro/mdns v0.1.0/go.mod h1:KJ0dW7KmicXU2BV++qkLlmHYcVv7/hHnbtguSWt9Aoc= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.13/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miscreant/miscreant-go v0.0.0-20181010193435-325cbd69228b h1:VPhrxAgvd0d0xSZP4P8zzWZntso2NE0m69dmDEXM53I= github.com/miscreant/miscreant-go v0.0.0-20181010193435-325cbd69228b/go.mod h1:Vj6lPE3LxPymcFxg7hm9aDIJWCyhJMnxSNC/y9ZHtN8= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= +github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= +github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/prometheus/tsdb v0.8.0/go.mod h1:fSI0j+IUQrDd7+ZtR9WKIGtoYAYAJUKcKhYLG25tN4g= github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs= github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1 h1:VeAkjQVzKLmu+JnFcK96TPbkuaTIqwGGAzQ9hgwPjVg= -golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed h1:uPxWBzB3+mlnjy9W58qY1j/cjyFjutgw/Vhan2zLy/A= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190530171427-2b03ca6e44eb/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190603231351-8aaa1484dc10 h1:9LyHDLeGJwq8p/x0xbF8aG63cLC6EDjlNbGYc7dacDc= +golang.org/x/tools v0.0.0-20190603231351-8aaa1484dc10/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM= +google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw= +google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 h1:wuGevabY6r+ivPNagjUXGGxF+GqgMd+dBhjsxW4q9u4= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/validator.v9 v9.29.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/redis.v3 v3.6.4/go.mod h1:6XeGv/CrsUFDU9aVbUdNykN7k1zVmoeg83KC9RbQfiU= +gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= +gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= +gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.11.0/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= +gopkg.in/telegram-bot-api.v4 v4.6.4/go.mod h1:5DpGO5dbumb40px+dXcwCpcjmeHNYLpk0bp3XRNvWDM= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190604153307-63e9ff576adb/go.mod h1:JlmFZigtG9vBVR3QGIQ9g/Usz4BzH+Xm6Z8iHQWRYUw= diff --git a/internal/auth/authenticator.go b/internal/auth/authenticator.go index e90c3c1a..dc2c4d37 100644 --- a/internal/auth/authenticator.go +++ b/internal/auth/authenticator.go @@ -63,48 +63,14 @@ type getProfileResponse struct { Groups []string `json:"groups"` } -// SetCookieStore sets the cookie store to use a miscreant cipher -func SetCookieStore(opts *Options, providerSlug string) func(*Authenticator) error { - return func(a *Authenticator) error { - decodedAuthCodeSecret, err := base64.StdEncoding.DecodeString(opts.AuthCodeSecret) - if err != nil { - return err - } - authCodeCipher, err := aead.NewMiscreantCipher([]byte(decodedAuthCodeSecret)) - if err != nil { - return err - } - - cookieName := fmt.Sprintf("%s_%s", opts.CookieName, providerSlug) - cookieStore, err := sessions.NewCookieStore(cookieName, - sessions.CreateMiscreantCookieCipher(opts.decodedCookieSecret), - func(c *sessions.CookieStore) error { - c.CookieDomain = opts.CookieDomain - c.CookieHTTPOnly = opts.CookieHTTPOnly - c.CookieExpire = opts.CookieExpire - c.CookieSecure = opts.CookieSecure - return nil - }) - - if err != nil { - return err - } - - a.csrfStore = cookieStore - a.sessionStore = cookieStore - a.AuthCodeCipher = authCodeCipher - return nil - } -} - // NewAuthenticator creates a Authenticator struct and applies the optional functions slice to the struct. -func NewAuthenticator(opts *Options, optionFuncs ...func(*Authenticator) error) (*Authenticator, error) { +func NewAuthenticator(config Configuration, optionFuncs ...func(*Authenticator) error) (*Authenticator, error) { logger := log.NewLogEntry() templates := templates.NewHTMLTemplate() proxyRootDomains := []string{} - for _, domain := range opts.ProxyRootDomains { + for _, domain := range config.AuthorizeConfig.ProxyConfig.Domains { if !strings.HasPrefix(domain, ".") { domain = fmt.Sprintf(".%s", domain) } @@ -112,14 +78,14 @@ func NewAuthenticator(opts *Options, optionFuncs ...func(*Authenticator) error) } p := &Authenticator{ - ProxyClientID: opts.ProxyClientID, - ProxyClientSecret: opts.ProxyClientSecret, - EmailDomains: opts.EmailDomains, - ProxyRootDomains: proxyRootDomains, - Host: opts.Host, - Scheme: opts.Scheme, - - templates: templates, + ProxyClientID: config.ClientConfigs["proxy"].ID, + ProxyClientSecret: config.ClientConfigs["proxy"].Secret, + EmailDomains: config.AuthorizeConfig.EmailConfig.Domains, + Host: config.ServerConfig.Host, + Scheme: config.ServerConfig.Scheme, + + ProxyRootDomains: proxyRootDomains, + templates: templates, } p.ServeMux = p.newMux() diff --git a/internal/auth/authenticator_test.go b/internal/auth/authenticator_test.go index 92f28aee..df2fafc7 100644 --- a/internal/auth/authenticator_test.go +++ b/internal/auth/authenticator_test.go @@ -91,10 +91,10 @@ func setMockRedirectURL() func(*Authenticator) error { } } -func assignProvider(opts *Options) func(*Authenticator) error { +func assignProvider(pc ProviderConfig) func(*Authenticator) error { return func(a *Authenticator) error { var err error - a.provider, err = newProvider(opts) + a.provider, err = newProvider(pc, SessionConfig{SessionLifetimeTTL: 1 * time.Hour}) return err } } @@ -103,25 +103,6 @@ func assignProvider(opts *Options) func(*Authenticator) error { var testEncodedCookieSecret = "x7xzsM1Ky4vGQPwqy6uTztfr3jtm/pIdRbJXgE0q8kU=" var testAuthCodeSecret = "qICChm3wdjbjcWymm7PefwtPP6/PZv+udkFEubTeE38=" -func testOpts(t *testing.T, proxyClientID, proxyClientSecret string) *Options { - opts, err := NewOptions() - if err != nil { - t.Fatalf("error while instantiating config options: %s", err.Error()) - } - opts.ProxyClientID = proxyClientID - opts.ProxyClientSecret = proxyClientSecret - opts.CookieSecret = testEncodedCookieSecret - opts.ClientID = "bazquux" - opts.ClientSecret = "xyzzyplugh" - opts.AuthCodeSecret = testAuthCodeSecret - opts.ProxyRootDomains = []string{"example.com"} - opts.Host = "/" - opts.EmailDomains = []string{"*"} - opts.StatsdPort = 8125 - opts.StatsdHost = "localhost" - return opts -} - func newRevokeServer(accessToken string) (*url.URL, *httptest.Server) { s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { r.ParseForm() @@ -445,9 +426,8 @@ func TestSignIn(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - opts := testOpts(t, "test", "secret") - opts.Validate() - auth, err := NewAuthenticator(opts, + config := testConfiguration(t) + auth, err := NewAuthenticator(config, setMockValidator(tc.validEmail), setMockSessionStore(tc.mockSessionStore), setMockTempl(), @@ -482,6 +462,7 @@ func TestSignIn(t *testing.T) { if tc.expectedSignInPage { expectedSignInResp := &signInResp{ ProviderName: provider.Data().ProviderName, + ProviderSlug: "test", EmailDomains: auth.EmailDomains, Redirect: u.String(), Destination: tc.expectedDestinationURL, @@ -532,10 +513,11 @@ func TestSignOutPage(t *testing.T) { ExpectedStatusCode: http.StatusOK, Method: "GET", expectedSignOutResp: &signOutResp{ - Version: VERSION, - Redirect: "http://service.example.com", - Destination: "service.example.com", - Email: "test@example.com", + ProviderSlug: "test", + Version: VERSION, + Redirect: "http://service.example.com", + Destination: "service.example.com", + Email: "test@example.com", }, }, { @@ -587,17 +569,17 @@ func TestSignOutPage(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { u, _ := url.Parse("/sign_out") - opts := testOpts(t, "abced", "testtest") - opts.Validate() + config := testConfiguration(t) provider := providers.NewTestProvider(u) provider.RevokeError = tc.RevokeError - p, _ := NewAuthenticator(opts, func(p *Authenticator) error { - p.Validator = func(string) bool { return true } - return nil - }, setMockSessionStore(tc.mockSessionStore), - setMockTempl(), setTestProvider(provider)) + p, _ := NewAuthenticator(config, + setMockValidator(true), + setMockSessionStore(tc.mockSessionStore), + setMockTempl(), + setTestProvider(provider), + ) params, _ := url.ParseQuery(u.RawQuery) @@ -675,13 +657,17 @@ func TestValidateEndpoint(t *testing.T) { defer s.Close() validateURL, _ := url.Parse(s.URL) - opts := testOpts(t, proxyClientID, proxyClientSecret) - opts.Validate() - - proxy, _ := NewAuthenticator(opts) - proxy.provider = &providers.ProviderData{ + config := testConfiguration(t) + config.ClientConfigs["proxy"] = ClientConfig{ + ID: proxyClientID, + Secret: proxyClientSecret, + } + provider := &providers.ProviderData{ ValidateURL: validateURL, } + authenticator, _ := NewAuthenticator(config, + SetProvider(provider), + ) u, _ := url.Parse(tc.Endpoint) params, _ := url.ParseQuery(u.RawQuery) @@ -693,7 +679,7 @@ func TestValidateEndpoint(t *testing.T) { req.Header.Add("X-Access-Token", tc.AccessToken) rw := httptest.NewRecorder() - proxy.ServeMux.ServeHTTP(rw, req) + authenticator.ServeMux.ServeHTTP(rw, req) if rw.Code != tc.ExpectedStatusCode { t.Errorf("expected status code %v but response status code is %v", tc.ExpectedStatusCode, rw.Code) @@ -818,10 +804,9 @@ func TestProxyOAuthRedirect(t *testing.T) { t.Run(tc.name, func(t *testing.T) { now := time.Now() - opts := testOpts(t, "clientId", "clientSecret") - opts.Validate() + config := testConfiguration(t) - proxy, _ := NewAuthenticator(opts, setMockAuthCodeCipher(tc.mockCipher, nil)) + proxy, _ := NewAuthenticator(config, setMockAuthCodeCipher(tc.mockCipher, nil)) params := url.Values{} for paramKey, val := range tc.paramsMap { params.Set(paramKey, val) @@ -895,10 +880,9 @@ func TestRefreshEndpoint(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - opts := testOpts(t, "client_id", "client_secret") - opts.Validate() + config := testConfiguration(t) - p, _ := NewAuthenticator(opts) + p, _ := NewAuthenticator(config) p.provider = &testRefreshProvider{refreshFunc: tc.refreshFunc} params := url.Values{} params.Set("refresh_token", tc.refreshToken) @@ -966,12 +950,10 @@ func TestGetProfile(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - opts := testOpts(t, "client_id", "client_secret") - opts.Validate() - p, _ := NewAuthenticator(opts, func(p *Authenticator) error { - p.Validator = func(string) bool { return true } - return nil - }) + config := testConfiguration(t) + p, _ := NewAuthenticator(config, + setMockValidator(true), + ) u, _ := url.Parse("http://example.com") testProvider := providers.NewTestProvider(u) testProvider.Groups = tc.groupEmails @@ -1069,13 +1051,11 @@ func TestRedeemCode(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - opts := testOpts(t, "client_id", "client_secret") - opts.Validate() + config := testConfiguration(t) - proxy, _ := NewAuthenticator(opts, func(p *Authenticator) error { - p.Validator = func(string) bool { return true } - return nil - }) + proxy, _ := NewAuthenticator(config, + setMockValidator(true), + ) testURL, err := url.Parse("example.com") if err != nil { @@ -1186,10 +1166,11 @@ func TestRedeemEndpoint(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - opts := testOpts(t, "client_id", "client_secret") - opts.Validate() - p, _ := NewAuthenticator(opts, setMockAuthCodeCipher(tc.mockCipher, tc.sessionState), - setMockSessionStore(&sessions.MockSessionStore{})) + config := testConfiguration(t) + p, _ := NewAuthenticator(config, + setMockAuthCodeCipher(tc.mockCipher, tc.sessionState), + setMockSessionStore(&sessions.MockSessionStore{}), + ) params := url.Values{} for k, v := range tc.paramsMap { @@ -1459,12 +1440,12 @@ func TestOAuthCallback(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - opts := testOpts(t, "client_id", "client_secret") - opts.Validate() - proxy, _ := NewAuthenticator(opts, func(p *Authenticator) error { - p.Validator = func(string) bool { return tc.validEmail } - return nil - }, setMockCSRFStore(tc.csrfResp), setMockSessionStore(tc.sessionStore)) + config := testConfiguration(t) + proxy, _ := NewAuthenticator(config, + setMockValidator(tc.validEmail), + setMockCSRFStore(tc.csrfResp), + setMockSessionStore(tc.sessionStore), + ) testURL, err := url.Parse("http://example.com") if err != nil { @@ -1504,9 +1485,8 @@ func TestOAuthCallback(t *testing.T) { } func TestGlobalHeaders(t *testing.T) { - opts := testOpts(t, "abced", "testtest") - opts.Validate() - proxy, _ := NewAuthenticator(opts, setMockCSRFStore(&sessions.MockCSRFStore{})) + config := testConfiguration(t) + proxy, _ := NewAuthenticator(config, setMockCSRFStore(&sessions.MockCSRFStore{})) // see middleware.go expectedHeaders := securityHeaders @@ -1543,7 +1523,6 @@ func TestGlobalHeaders(t *testing.T) { } func TestOAuthStart(t *testing.T) { - testCases := []struct { Name string RedirectURI string @@ -1580,12 +1559,9 @@ func TestOAuthStart(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - - opts := testOpts(t, "abced", "testtest") - opts.Validate() - u, _ := url.Parse("http://example.com") - provider := providers.NewTestProvider(u) - proxy, _ := NewAuthenticator(opts, + config := testConfiguration(t) + provider := providers.NewTestProvider(nil) + proxy, _ := NewAuthenticator(config, setTestProvider(provider), setMockValidator(true), setMockRedirectURL(), @@ -1598,7 +1574,7 @@ func TestOAuthStart(t *testing.T) { if tc.ProxyRedirectURI != "" { // NOTE: redirect signatures tested in middleware_test.go now := time.Now() - sig := redirectURLSignature(tc.ProxyRedirectURI, now, "testtest") + sig := redirectURLSignature(tc.ProxyRedirectURI, now, config.ClientConfigs["proxy"].Secret) b64sig := base64.URLEncoding.EncodeToString(sig) redirectParams := url.Values{} redirectParams.Add("redirect_uri", tc.ProxyRedirectURI) @@ -1624,19 +1600,14 @@ func TestOAuthStart(t *testing.T) { } func TestGoogleProviderApiSettings(t *testing.T) { - opts := testOpts(t, "abced", "testtest") - opts.Provider = "google" - opts.Validate() - - proxy, err := NewAuthenticator(opts, - assignProvider(opts), - setMockValidator(true), + provider, err := newProvider( + ProviderConfig{ProviderType: "google"}, + SessionConfig{SessionLifetimeTTL: 1 * time.Hour}, ) if err != nil { - t.Fatalf("unexpected err provisioning authenticator: %v", err) + t.Fatalf("unexpected err generating google provider: %v", err) } - - p := proxy.provider.Data() + p := provider.Data() testutil.Equal(t, "https://accounts.google.com/o/oauth2/auth?access_type=offline", p.SignInURL.String()) testutil.Equal(t, "https://www.googleapis.com/oauth2/v3/token", @@ -1647,27 +1618,17 @@ func TestGoogleProviderApiSettings(t *testing.T) { } func TestGoogleGroupInvalidFile(t *testing.T) { - opts := testOpts(t, "abced", "testtest") - opts.Provider = "google" - opts.GoogleAdminEmail = "admin@example.com" - opts.GoogleServiceAccountJSON = "file_doesnt_exist.json" - opts.Validate() - _, err := NewAuthenticator(opts, assignProvider(opts), func(p *Authenticator) error { - p.Validator = func(string) bool { return true } - return nil - }) + _, err := newProvider( + ProviderConfig{ + ProviderType: "google", + GoogleProviderConfig: GoogleProviderConfig{ + Credentials: "file_doesnt_exist.json", + }, + }, + SessionConfig{ + SessionLifetimeTTL: 1 * time.Hour, + }, + ) testutil.NotEqual(t, nil, err) testutil.Equal(t, "invalid Google credentials file: file_doesnt_exist.json", err.Error()) } - -func TestUnimplementedProvider(t *testing.T) { - opts := testOpts(t, "abced", "testtest") - opts.Provider = "null_provider" - opts.Validate() - _, err := NewAuthenticator(opts, assignProvider(opts), func(p *Authenticator) error { - p.Validator = func(string) bool { return true } - return nil - }) - testutil.NotEqual(t, nil, err) - testutil.Equal(t, "unimplemented provider: \"null_provider\"", err.Error()) -} diff --git a/internal/auth/configuration.go b/internal/auth/configuration.go new file mode 100644 index 00000000..4dcdf050 --- /dev/null +++ b/internal/auth/configuration.go @@ -0,0 +1,512 @@ +package auth + +import ( + "encoding/base64" + "net/http" + "os" + "time" + + "github.com/micro/go-micro/config" + "github.com/micro/go-micro/config/source/env" + "github.com/mitchellh/mapstructure" + "golang.org/x/xerrors" +) + +// DefaultAuthConfig specifies all the defaults used to configure sso-auth +// All configuration can be set using environment variables. Below is a list of +// configuration variables via their envivronment configuration +// +// SESSION COOKIE_NAME +// SESSION_COOKIE_SECRET +// SESSION_COOKIE_EXPIRE +// SESSION_COOKIE_DOMAIN +// SESSION_COOKIE_REFRESH +// SESSION_COOKIE_SECURE +// SESSION_COOKIE_HTTPONLY +// SESSION_LIFETIME +// SESSION_KEY +// +// CLIENT_PROXY_ID +// CLIENT_PROXY_SECRET +// +// PROVIDER_*_TYPE +// PROVIDER_*_SLUG +// PROVIDER_*_CLIENT_ID +// PROVIDER_*_CLIENT_SECRET +// PROVIDER_*_SCOPE +// +// PROVIDER_*_GOOGLE_CREDENTIALS +// PROVIDER_*_GOOGLE_IMPERSONATE +// +// PROVIDER_*_OKTA_URL +// PROVIDER_*_OKTA_SERVER +// +// PROVIDER_*_GROUPCACHE_INTERVAL_REFRESH +// PROVIDER_*_GROUPCACHE_INTERVAL_PROVIDER +// +// SERVER_SCHEME +// SERVER_HOST +// SERVER_PORT +// SERVER_TIMEOUT_REQUEST +// SERVER_TIMEOUT_WRITE +// SERVER_TIMEOUT_READ +// +// AUTHORIZE_PROXY_DOMAINS +// AUTHORIZE_EMAIL_DOMAINS +// AUTHORIZE_EMAIL_ADDRESSES +// +// METRICS_STATSD_PORT +// METRICS_STATSD_HOST +// +// LOGGING_ENABLE +// LOGGING_LEVEL + +func DefaultAuthConfig() Configuration { + return Configuration{ + ProviderConfigs: map[string]ProviderConfig{}, + ClientConfigs: map[string]ClientConfig{ + "proxy": ClientConfig{}, + }, + ServerConfig: ServerConfig{ + Port: 4180, + Scheme: "https", + TimeoutConfig: TimeoutConfig{ + Write: 30 * time.Second, + Read: 30 * time.Second, + Request: 45 * time.Second, + }, + }, + SessionConfig: SessionConfig{ + SessionLifetimeTTL: (30 * 24) * time.Hour, + CookieConfig: CookieConfig{ + Expire: (7 * 24) * time.Hour, + Name: "_sso_auth", + Secure: true, + HTTPOnly: true, + }, + }, + LoggingConfig: LoggingConfig{ + Enable: true, + Level: "info", + }, + MetricsConfig: MetricsConfig{ + StatsdConfig: StatsdConfig{ + Port: 8125, + Host: "localhost", + }, + }, + // we provide no defaults for these right now + AuthorizeConfig: AuthorizeConfig{ + EmailConfig: EmailConfig{ + Domains: []string{}, + Addresses: []string{}, + }, + ProxyConfig: ProxyConfig{ + Domains: []string{}, + }, + }, + } +} + +// Validator interface ensures all config structs implement Validate() +type Validator interface { + Validate() error +} + +var ( + _ Validator = Configuration{} + _ Validator = ProviderConfig{} + _ Validator = ClientConfig{} + _ Validator = AuthorizeConfig{} + _ Validator = EmailConfig{} + _ Validator = ProxyConfig{} + _ Validator = ServerConfig{} + _ Validator = MetricsConfig{} + _ Validator = GoogleProviderConfig{} + _ Validator = OktaProviderConfig{} + _ Validator = CookieConfig{} + _ Validator = TimeoutConfig{} + _ Validator = StatsdConfig{} + _ Validator = LoggingConfig{} +) + +// Configuration is the parent struct that holds all the configuration +type Configuration struct { + ProviderConfigs map[string]ProviderConfig `mapstructure:"provider"` + ClientConfigs map[string]ClientConfig `mapstructure:"client"` + AuthorizeConfig AuthorizeConfig `mapstructure:"authorize"` + SessionConfig SessionConfig `mapstructure:"session"` + ServerConfig ServerConfig `mapstructure:"server"` + MetricsConfig MetricsConfig `mapstructrue:"metrics"` + LoggingConfig LoggingConfig `mapstructure:"logging"` +} + +func (c Configuration) Validate() error { + for slug, providerConfig := range c.ProviderConfigs { + if err := providerConfig.Validate(); err != nil { + return xerrors.Errorf("invalid provider.%s config: %w", slug, err) + } + } + + for slug, clientConfig := range c.ClientConfigs { + if err := clientConfig.Validate(); err != nil { + return xerrors.Errorf("invalid client.%s config: %w", slug, err) + } + } + + if err := c.SessionConfig.Validate(); err != nil { + return xerrors.Errorf("invalid session config: %w", err) + } + + if err := c.ServerConfig.Validate(); err != nil { + return xerrors.Errorf("invalid server config: %w", err) + } + + if err := c.AuthorizeConfig.Validate(); err != nil { + return xerrors.Errorf("invalid authorize config: %w", err) + } + + if err := c.MetricsConfig.Validate(); err != nil { + return xerrors.Errorf("invalid metrics config: %w", err) + } + + return nil +} + +type ProviderConfig struct { + ProviderType string `mapstructure:"type"` + ProviderSlug string `mapstructure:"slug"` + ClientConfig ClientConfig `mapstructure:"client"` + Scope string `mapstructure:"scope"` + + // provider specific + GoogleProviderConfig GoogleProviderConfig `mapstructure:"google"` + OktaProviderConfig OktaProviderConfig `mapstructure:"okta"` + + // caching + GroupCacheConfig GroupCacheConfig `mapstructure:"groupcache"` +} + +func (pc ProviderConfig) Validate() error { + if pc.ProviderType == "" { + return xerrors.Errorf("invalid provider.type: %q", pc.ProviderType) + } + + // TODO: more validation of provider slug, should conform to simple character space + if pc.ProviderSlug == "" { + return xerrors.Errorf("invalid provider.slug: %q", pc.ProviderSlug) + } + + if err := pc.ClientConfig.Validate(); err != nil { + return xerrors.Errorf("invalid provider.client: %w", err) + } + + switch pc.ProviderType { + case "google": + if err := pc.GoogleProviderConfig.Validate(); err != nil { + return xerrors.Errorf("invalid provider.google config: %w", err) + } + case "okta": + if err := pc.OktaProviderConfig.Validate(); err != nil { + return xerrors.Errorf("invalid provider.okta config: %w", err) + } + case "test": + break + default: + return xerrors.Errorf("unknown provider.type: %q", pc.ProviderType) + } + + if err := pc.GroupCacheConfig.Validate(); err != nil { + return xerrors.Errorf("invalid provider.groupcache config: %w", err) + } + + return nil +} + +type GoogleProviderConfig struct { + Credentials string `mapstructure:"credentials"` + Impersonate string `mapstructure:"impersonate"` + ApprovalPrompt string `mapstructure:"prompt"` +} + +func (gpc GoogleProviderConfig) Validate() error { + // must specify both credentials and impersonate or neither + if (len(gpc.Credentials) > 0) != (len(gpc.Impersonate) > 0) { + return xerrors.New("must specify both google.credentials and google.impersonate") + } + + // verify the credentials file can be opened + if gpc.Credentials != "" { + r, err := os.Open(gpc.Credentials) + if err != nil { + return xerrors.Errorf("invalid google.credentials filepath: %w", err) + } + r.Close() + } + + return nil +} + +type OktaProviderConfig struct { + ServerID string `mapstructure:"server"` + OrgURL string `mapstructure:"url"` +} + +func (opc OktaProviderConfig) Validate() error { + if opc.OrgURL == "" { + return xerrors.New("no okta.url is configured") + } + + if opc.ServerID == "" { + return xerrors.New("no okta.server is configured") + } + + return nil +} + +type GroupCacheConfig struct { + Interval struct { + Provider time.Duration `mapstructure:"provider"` + Refresh time.Duration `mapstructure:"refresh"` + } `mapstructure:"interval"` +} + +func (gcc GroupCacheConfig) Validate() error { + return nil +} + +type SessionConfig struct { + CookieConfig CookieConfig `mapstructure:"cookie"` + + SessionLifetimeTTL time.Duration `mapstructure:"lifetime"` + Key string `mapstructure:"key"` +} + +func (sc SessionConfig) Validate() error { + if sc.Key == "" { + return xerrors.New("no session.key configured") + } + + if err := validateCipherKeyValue(sc.Key); err != nil { + return xerrors.Errorf("invalid session.key: %w", err) + } + + if sc.SessionLifetimeTTL >= (365*24)*time.Hour || sc.SessionLifetimeTTL <= 1*time.Minute { + return xerrors.Errorf("session.lifetime must be between 1 minute and 1 year but is: %v", sc.SessionLifetimeTTL) + } + + if err := sc.CookieConfig.Validate(); err != nil { + return xerrors.Errorf("invalid session.cookie config: %w", err) + } + + return nil +} + +func validateCipherKeyValue(val string) error { + s, err := base64.StdEncoding.DecodeString(val) + if err != nil { + return xerrors.Errorf("expected base64-encoded bytes, as from `openssl rand 32 -base64`: %w", err) + } + + slen := len(s) + if slen != 32 && slen != 64 { + return xerrors.Errorf("expected to decode 32 or 64 base64-encoded bytes, but decoded %d", slen) + } + + return nil +} + +type CookieConfig struct { + Name string `mapstructure:"name"` + Secret string `mapstructure:"secret"` + Domain string `mapstructure:"domain"` + Expire time.Duration `mapstructure:"expire"` + Secure bool `mapstructure:"secure"` + HTTPOnly bool `mapstructure:"httponly"` +} + +func (cc CookieConfig) Validate() error { + // TODO: Validate cookie secret: + if cc.Name == "" { + return xerrors.New("no cookie.name configured") + } + + cookie := &http.Cookie{Name: cc.Name} + if cookie.String() == "" { + return xerrors.Errorf("invalid cookie.name: %q", cc.Name) + } + + if cc.Secret == "" { + return xerrors.New("no cookie.secret configured") + } + + if err := validateCipherKeyValue(cc.Secret); err != nil { + return xerrors.Errorf("invalid cookie.secret: %w", err) + } + + return nil +} + +type ServerConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Scheme string `mapstructure:"scheme"` + + TimeoutConfig TimeoutConfig `mapstructure:"timeout"` +} + +func (sc ServerConfig) Validate() error { + if sc.Host == "" { + return xerrors.New("no server.host configured") + } + + if sc.Port == 0 { + return xerrors.New("no server.port configured") + } + + if err := sc.TimeoutConfig.Validate(); err != nil { + return xerrors.Errorf("invalid server.tcp config: %w", err) + } + + return nil +} + +type TimeoutConfig struct { + Write time.Duration `mapstructure:"write"` + Read time.Duration `mapstructure:"read"` + Request time.Duration `mapstructure:"request"` +} + +func (tc TimeoutConfig) Validate() error { + return nil +} + +type ClientConfig struct { + ID string `mapstructure:"id"` + Secret string `mapstructure:"secret"` +} + +func (cc ClientConfig) Validate() error { + if cc.ID == "" { + return xerrors.New("no client.id configured") + } + + if cc.Secret == "" { + return xerrors.New("no client.secret configured") + } + + return nil +} + +type AuthorizeConfig struct { + EmailConfig EmailConfig `mapstructure:"email"` + ProxyConfig ProxyConfig `mapstructure:"proxy"` +} + +func (ac AuthorizeConfig) Validate() error { + if err := ac.EmailConfig.Validate(); err != nil { + return xerrors.Errorf("invalid authorize.email config: %w", err) + } + + if err := ac.ProxyConfig.Validate(); err != nil { + return xerrors.Errorf("invalid authorize.proxy config: %w", err) + } + + return nil +} + +type EmailConfig struct { + Domains []string `mapstructure:"domains"` + Addresses []string `mapstructure:"addresses"` +} + +func (ec EmailConfig) Validate() error { + if len(ec.Domains) > 0 && len(ec.Addresses) > 0 { + return xerrors.New("can not specify both email.domains and email.addesses") + } + + if len(ec.Domains) == 0 && len(ec.Addresses) == 0 { + return xerrors.New("must specify either email.domains or email.addresses") + } + + return nil +} + +type ProxyConfig struct { + Domains []string `mapstructure:"domains"` +} + +func (pc ProxyConfig) Validate() error { + if len(pc.Domains) == 0 { + return xerrors.New("no proxy.domains configured") + } + + return nil +} + +type MetricsConfig struct { + StatsdConfig StatsdConfig `mapstructure:"statsd"` +} + +func (mc MetricsConfig) Validate() error { + if err := mc.StatsdConfig.Validate(); err != nil { + return xerrors.Errorf("invalid metrics.statsd config: %w", err) + } + + return nil +} + +type LoggingConfig struct { + Enable bool `mapstructure:"enable"` + Level string `mapstructure:"level"` +} + +func (lc LoggingConfig) Validate() error { + return nil +} + +type StatsdConfig struct { + Port int `mapstructure:"port"` + Host string `mapstructure:"host"` +} + +func (sc StatsdConfig) Validate() error { + if sc.Host == "" { + return xerrors.New("no statsd.host configured") + } + + if sc.Port == 0 { + return xerrors.New(" no statsd.port configured") + } + + return nil +} + +// LoadConfig loads all the configuration from env and defaults +func LoadConfig() (Configuration, error) { + c := DefaultAuthConfig() + + conf := config.NewConfig() + err := conf.Load(env.NewSource()) + if err != nil { + return c, err + } + + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + ), + Result: &c, + }) + if err != nil { + return c, err + } + + err = decoder.Decode(conf.Map()) + if err != nil { + return c, err + } + + return c, nil +} diff --git a/internal/auth/configuration_test.go b/internal/auth/configuration_test.go new file mode 100644 index 00000000..0ce8a95b --- /dev/null +++ b/internal/auth/configuration_test.go @@ -0,0 +1,279 @@ +package auth + +import ( + "os" + "reflect" + "testing" + "time" + + "golang.org/x/xerrors" +) + +func testConfiguration(t *testing.T) Configuration { + c := Configuration{ + ServerConfig: ServerConfig{ + Host: "localhost", + Port: 4180, + TimeoutConfig: TimeoutConfig{ + Write: 30 * time.Second, + Read: 30 * time.Second, + }, + }, + SessionConfig: SessionConfig{ + SessionLifetimeTTL: (30 * 24) * time.Hour, + CookieConfig: CookieConfig{ + Name: "_sso_auth", + Secret: "zaPX2fYMyegfOwwMEaMiphwrjgxz0pxoTbxvQiK9zBY=", // generated using `openssl rand -base64 32` + Expire: (7 * 24) * time.Hour, + Secure: true, + HTTPOnly: true, + }, + Key: "CrYro5Kp6CO2aBbVGoHgnh2/YQaz9cqqRYNbtTSUBDs=", // generated using `openssl rand -base64 32` + }, + MetricsConfig: MetricsConfig{ + StatsdConfig: StatsdConfig{ + Host: "localhost", + Port: 8124, + }, + }, + LoggingConfig: LoggingConfig{ + Enable: true, + }, + + // we provide no defaults for these right now + ProviderConfigs: map[string]ProviderConfig{ + "foo": ProviderConfig{ + ProviderType: "test", + ProviderSlug: "foo", + ClientConfig: ClientConfig{ + ID: "foo-client-id", + Secret: "foo-client-secret", + }, + }, + }, + ClientConfigs: map[string]ClientConfig{ + "proxy": ClientConfig{ + ID: "proxy-client-id", + Secret: "proxy-client-secret", + }, + }, + AuthorizeConfig: AuthorizeConfig{ + ProxyConfig: ProxyConfig{ + Domains: []string{"proxy.local", "root.local", "example.com"}, + }, + EmailConfig: EmailConfig{ + Domains: []string{"proxy.local"}, + }, + }, + } + err := c.Validate() + if err != nil { + t.Fatalf("unexpected err initilaizing test configuration: %v", err) + } + return c +} + +func assertEq(want, have interface{}, t *testing.T) { + if !reflect.DeepEqual(want, have) { + t.Errorf("want: %#v", want) + t.Errorf("have: %#v", have) + t.Errorf("expected values to be equal") + } +} + +func TestDefaultConfiguration(t *testing.T) { + want := DefaultAuthConfig() + have, err := LoadConfig() + if err != nil { + t.Fatalf("unexpected err loading config: %v", err) + } + assertEq(want, have, t) +} + +func TestEnvironmentOverridesConfiguration(t *testing.T) { + testCases := []struct { + Name string + EnvOverrides map[string]string + CheckFunc func(c Configuration, t *testing.T) + }{ + { + Name: "Test Server Host Overrides", + EnvOverrides: map[string]string{ + "SERVER_HOST": "example.com", + }, + CheckFunc: func(c Configuration, t *testing.T) { + assertEq("example.com", c.ServerConfig.Host, t) + }, + }, + { + Name: "Test Request Timeout Overrides", + EnvOverrides: map[string]string{ + "SERVER_TIMEOUT_WRITE": "60s", + "SERVER_TIMEOUT_READ": "60s", + }, + CheckFunc: func(c Configuration, t *testing.T) { + assertEq(60*time.Second, c.ServerConfig.TimeoutConfig.Write, t) + assertEq(60*time.Second, c.ServerConfig.TimeoutConfig.Read, t) + }, + }, + { + Name: "Test Providers", + EnvOverrides: map[string]string{ + "PROVIDER_FOO_SLUG": "foo-slug", + "PROVIDER_FOO_TYPE": "foo-type", + "PROVIDER_FOO_CLIENT_ID": "foo-client-id", + }, + CheckFunc: func(c Configuration, t *testing.T) { + foo := c.ProviderConfigs["foo"] + assertEq("foo-type", foo.ProviderType, t) + assertEq("foo-slug", foo.ProviderSlug, t) + assertEq("foo-client-id", foo.ClientConfig.ID, t) + }, + }, + { + Name: "Test Multiple Providers", + EnvOverrides: map[string]string{ + // foo + "PROVIDER_FOO_SLUG": "foo-slug", + "PROVIDER_FOO_TYPE": "foo-type", + "PROVIDER_FOO_CLIENT_ID": "foo-client-id", + // bar + "PROVIDER_BAR_SLUG": "bar-slug", + "PROVIDER_BAR_TYPE": "bar-type", + "PROVIDER_BAR_CLIENT_ID": "bar-client-id", + // baz + "PROVIDER_BAZ_SLUG": "baz-slug", + "PROVIDER_BAZ_TYPE": "baz-type", + "PROVIDER_BAZ_CLIENT_ID": "baz-client-id", + }, + CheckFunc: func(c Configuration, t *testing.T) { + foo := c.ProviderConfigs["foo"] + assertEq("foo-type", foo.ProviderType, t) + assertEq("foo-slug", foo.ProviderSlug, t) + assertEq("foo-client-id", foo.ClientConfig.ID, t) + + bar := c.ProviderConfigs["bar"] + assertEq("bar-type", bar.ProviderType, t) + assertEq("bar-slug", bar.ProviderSlug, t) + assertEq("bar-client-id", bar.ClientConfig.ID, t) + + baz := c.ProviderConfigs["baz"] + assertEq("baz-type", baz.ProviderType, t) + assertEq("baz-slug", baz.ProviderSlug, t) + assertEq("baz-client-id", baz.ClientConfig.ID, t) + }, + }, + { + Name: "Test ENV CSV Lists", + EnvOverrides: map[string]string{ + "AUTHORIZE_EMAIL_DOMAINS": "proxy.local,root.local", + }, + CheckFunc: func(c Configuration, t *testing.T) { + assertEq([]string{"proxy.local", "root.local"}, c.AuthorizeConfig.EmailConfig.Domains, t) + }, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + os.Clearenv() + for k, v := range tc.EnvOverrides { + err := os.Setenv(k, v) + if err != nil { + t.Fatalf("unexpected err setting env: %v", err) + } + } + have, err := LoadConfig() + if err != nil { + t.Fatalf("unexpected err loading config: %v", err) + } + tc.CheckFunc(have, t) + }) + } +} + +func TestConfigValidate(t *testing.T) { + testCases := map[string]struct { + Validator Validator + ExpectedErr error + }{ + "passing configuration": { + Validator: Configuration{ + ServerConfig: ServerConfig{ + Host: "localhost", + Port: 4180, + TimeoutConfig: TimeoutConfig{ + Write: 30 * time.Second, + Read: 30 * time.Second, + }, + }, + SessionConfig: SessionConfig{ + SessionLifetimeTTL: (30 * 24) * time.Hour, + CookieConfig: CookieConfig{ + Name: "_sso_auth", + Secret: "zaPX2fYMyegfOwwMEaMiphwrjgxz0pxoTbxvQiK9zBY=", // generated using `openssl rand -base64 32` + Expire: (7 * 24) * time.Hour, + Secure: true, + HTTPOnly: true, + }, + Key: "zaPX2fYMyegfOwwMEaMiphwrjgxz0pxoTbxvQiK9zBY=", // generated using `openssl rand -base64 32` + }, + MetricsConfig: MetricsConfig{ + StatsdConfig: StatsdConfig{ + Host: "localhost", + Port: 8124, + }, + }, + LoggingConfig: LoggingConfig{ + Enable: true, + }, + // we provide no defaults for these right now + ProviderConfigs: map[string]ProviderConfig{ + "foo": ProviderConfig{ + ProviderType: "test", + ProviderSlug: "foo", + ClientConfig: ClientConfig{ + ID: "foo-client-id", + Secret: "foo-client-secret", + }, + }, + }, + ClientConfigs: map[string]ClientConfig{ + "proxy": ClientConfig{ + ID: "proxy-client-id", + Secret: "proxy-client-secret", + }, + }, + AuthorizeConfig: AuthorizeConfig{ + ProxyConfig: ProxyConfig{ + Domains: []string{"proxy.local", "root.local"}, + }, + EmailConfig: EmailConfig{ + Domains: []string{"proxy.local"}, + }, + }, + }, + ExpectedErr: nil, + }, + "missing host configuration": { + Validator: ServerConfig{ + Port: 4180, + TimeoutConfig: TimeoutConfig{ + Write: 30 * time.Second, + Read: 30 * time.Second, + }, + }, + ExpectedErr: xerrors.New("no server.host configured"), + }, + } + + for testName, tc := range testCases { + t.Run(testName, func(t *testing.T) { + err := tc.Validator.Validate() + if err != nil && tc.ExpectedErr != nil { + assertEq(tc.ExpectedErr.Error(), err.Error(), t) + } else { + assertEq(tc.ExpectedErr, err, t) + } + }) + } +} diff --git a/internal/auth/metrics_test.go b/internal/auth/metrics_test.go index f707d1b8..dc9ded95 100644 --- a/internal/auth/metrics_test.go +++ b/internal/auth/metrics_test.go @@ -64,11 +64,7 @@ func TestNewStatsd(t *testing.T) { t.Fatalf("error %s", err.Error()) } defer pc.Close() - opts, err := NewOptions() - if err != nil { - t.Fatalf("error while instantiating config options: %s", err.Error()) - } - opts.Validate() + client, err := NewStatsdClient(tc.host, tc.port) if err != nil { t.Fatalf("error starting new statsd client: %s", err.Error()) @@ -164,11 +160,6 @@ func TestLogRequestMetrics(t *testing.T) { t.Fatalf("error %s", err.Error()) } defer pc.Close() - opts, err := NewOptions() - if err != nil { - t.Fatalf("error while instantiating config options: %v", err.Error()) - } - opts.Validate() client, _, _ := newTestStatsdClient(t) tagString := strings.Join(tc.expectedTags, ",") diff --git a/internal/auth/middleware_test.go b/internal/auth/middleware_test.go index f537073f..51fb7395 100644 --- a/internal/auth/middleware_test.go +++ b/internal/auth/middleware_test.go @@ -49,9 +49,13 @@ func TestWithMethods(t *testing.T) { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(tc.requestMethod, "/test", nil) rw := httptest.NewRecorder() - options := testOpts(t, "clientId", "clientSecret") - options.Validate() - p, _ := NewAuthenticator(options) + + config := testConfiguration(t) + p, err := NewAuthenticator(config) + if err != nil { + t.Fatalf("unexpected err creating authenticator: %v", err) + } + p.withMethods(createTestHandler(), tc.acceptedMethods...)(rw, req) resp := rw.Result() if resp.StatusCode != tc.expectedCode { @@ -120,9 +124,17 @@ func TestValidateClientID(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - options := testOpts(t, tc.expectedClientID, "secret") - options.Validate() - p, _ := NewAuthenticator(options) + config := testConfiguration(t) + config.ClientConfigs["proxy"] = ClientConfig{ + ID: tc.expectedClientID, + Secret: config.ClientConfigs["proxy"].Secret, + } + + p, err := NewAuthenticator(config) + if err != nil { + t.Fatalf("unexpected err creating authenticator: %v", err) + } + params := url.Values{} if tc.requestBodyClientID != "" { params.Add("client_id", tc.requestBodyClientID) @@ -203,9 +215,17 @@ func TestValidateClientSecret(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - options := testOpts(t, "clientId", tc.expectedClientSecret) - options.Validate() - p, _ := NewAuthenticator(options) + config := testConfiguration(t) + config.ClientConfigs["proxy"] = ClientConfig{ + ID: config.ClientConfigs["proxy"].ID, + Secret: tc.expectedClientSecret, + } + + p, err := NewAuthenticator(config) + if err != nil { + t.Fatalf("unexpected err creating authenticator: %v", err) + } + params := url.Values{} if tc.clientSecretBody != "" { params.Add("client_secret", tc.clientSecretBody) @@ -283,10 +303,13 @@ func TestValidateRedirectURI(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - options := testOpts(t, "clientId", "clientSecret") - options.ProxyRootDomains = []string{"example.com", "example.io"} - options.Validate() - p, _ := NewAuthenticator(options) + config := testConfiguration(t) + config.AuthorizeConfig.ProxyConfig.Domains = []string{"example.com", "example.io"} + p, err := NewAuthenticator(config) + if err != nil { + t.Fatalf("unexpected err creating authenticator: %v", err) + } + params := url.Values{} if tc.redirectURI != "" { params.Add("redirect_uri", tc.redirectURI) @@ -294,6 +317,7 @@ func TestValidateRedirectURI(t *testing.T) { req := httptest.NewRequest("POST", "/test", bytes.NewBufferString(params.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rw := httptest.NewRecorder() + p.validateRedirectURI(createTestHandler())(rw, req) resp := rw.Result() if resp.StatusCode != tc.expectedStatusCode { @@ -399,10 +423,16 @@ func TestValidateSignature(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - options := testOpts(t, "clientId", tc.request.secret) - options.ProxyRootDomains = []string{"example.com", "example.io"} - options.Validate() - p, _ := NewAuthenticator(options) + config := testConfiguration(t) + config.ClientConfigs["proxy"] = ClientConfig{ + ID: config.ClientConfigs["proxy"].ID, + Secret: tc.request.secret, + } + + p, err := NewAuthenticator(config) + if err != nil { + t.Fatalf("unexpected err creating authenticator: %v", err) + } params := url.Values{} params.Add("redirect_uri", tc.request.redirectURI) params.Add("ts", fmt.Sprint(tc.request.timestamp.Unix())) diff --git a/internal/auth/mux.go b/internal/auth/mux.go index 5ae0102e..270315a7 100644 --- a/internal/auth/mux.go +++ b/internal/auth/mux.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" - "github.com/buzzfeed/sso/internal/auth/providers" "github.com/buzzfeed/sso/internal/pkg/hostmux" log "github.com/buzzfeed/sso/internal/pkg/logging" "github.com/buzzfeed/sso/internal/pkg/options" @@ -17,34 +16,33 @@ type AuthenticatorMux struct { authenticators []*Authenticator } -func NewAuthenticatorMux(opts *Options, statsdClient *statsd.Client) (*AuthenticatorMux, error) { +func NewAuthenticatorMux(config Configuration, statsdClient *statsd.Client) (*AuthenticatorMux, error) { logger := log.NewLogEntry() var validator func(string) bool - if len(opts.EmailAddresses) != 0 { - validator = options.NewEmailAddressValidator(opts.EmailAddresses) + if len(config.AuthorizeConfig.EmailConfig.Addresses) != 0 { + validator = options.NewEmailAddressValidator(config.AuthorizeConfig.EmailConfig.Addresses) } else { - validator = options.NewEmailDomainValidator(opts.EmailDomains) + validator = options.NewEmailDomainValidator(config.AuthorizeConfig.EmailConfig.Domains) } - // one day, we will contruct more providers here - idp, err := newProvider(opts) - if err != nil { - logger.Error(err, "error creating new Identity Provider") - return nil, err - } - identityProviders := []providers.Provider{idp} authenticators := []*Authenticator{} - idpMux := http.NewServeMux() - for _, idp := range identityProviders { + + for slug, providerConfig := range config.ProviderConfigs { + idp, err := newProvider(providerConfig, config.SessionConfig) + if err != nil { + logger.Error(err, fmt.Sprintf("error creating provider.%s", slug)) + return nil, err + } + idpSlug := idp.Data().ProviderSlug - authenticator, err := NewAuthenticator(opts, + authenticator, err := NewAuthenticator(config, SetValidator(validator), SetProvider(idp), - SetCookieStore(opts, idpSlug), + SetCookieStore(config.SessionConfig, idpSlug), SetStatsdClient(statsdClient), - SetRedirectURL(opts, idpSlug), + SetRedirectURL(config.ServerConfig, idpSlug), ) if err != nil { logger.Error(err, "error creating new Authenticator") @@ -58,27 +56,6 @@ func NewAuthenticatorMux(opts *Options, statsdClient *statsd.Client) (*Authentic fmt.Sprintf("/%s/", idpSlug), http.StripPrefix(fmt.Sprintf("/%s", idpSlug), authenticator.ServeMux), ) - - // TODO: This should be removed once we feel confident clients have been moved over to the new - // style /slug/ paths. This is kept around to allow graceful migrations/upgrades. - if idpSlug == opts.DefaultProviderSlug { - // setup our mux with the idpslug as the first part of the path - authenticator, err := NewAuthenticator(opts, - SetValidator(validator), - SetProvider(idp), - SetCookieStore(opts, idpSlug), - SetStatsdClient(statsdClient), - SetDefaultRedirectURL(opts), - ) - if err != nil { - logger.Error(err, "error creating new Authenticator") - return nil, err - } - - authenticators = append(authenticators, authenticator) - - idpMux.Handle("/", authenticator.ServeMux) - } } // load static files @@ -90,7 +67,7 @@ func NewAuthenticatorMux(opts *Options, statsdClient *statsd.Client) (*Authentic idpMux.HandleFunc("/robots.txt", RobotsTxt) hostRouter := hostmux.NewRouter() - hostRouter.HandleStatic(opts.Host, idpMux) + hostRouter.HandleStatic(config.ServerConfig.Host, idpMux) healthcheckHandler := setHealthCheck("/ping", hostRouter) diff --git a/internal/auth/mux_test.go b/internal/auth/mux_test.go index 3a501b72..d98df220 100644 --- a/internal/auth/mux_test.go +++ b/internal/auth/mux_test.go @@ -46,14 +46,9 @@ func TestHostHeader(t *testing.T) { } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - opts := testOpts(t, "abced", "testtest") - opts.Host = tc.Host - err := opts.Validate() - if err != nil { - t.Fatalf("unexpected opts error: %v", err) - } - - authMux, err := NewAuthenticatorMux(opts, nil) + config := testConfiguration(t) + config.ServerConfig.Host = tc.Host + authMux, err := NewAuthenticatorMux(config, nil) if err != nil { t.Fatalf("unexpected err creating auth mux: %v", err) } @@ -75,62 +70,15 @@ func TestHostHeader(t *testing.T) { } } -func TestDefaultProvider(t *testing.T) { - testCases := []struct { - Name string - Host string - ExpectedStatusCode int - }{ - { - Name: "similar requests to default path should get same results as slug path", - Host: "example.com", - ExpectedStatusCode: http.StatusBadRequest, - }, - } - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - opts := testOpts(t, "abced", "testtest") - opts.Host = tc.Host - err := opts.Validate() - if err != nil { - t.Fatalf("unexpected opts error: %v", err) - } - - authMux, err := NewAuthenticatorMux(opts, nil) - if err != nil { - t.Fatalf("unexpected err creating auth mux: %v", err) - } - - for _, path := range []string{"/callback", "/google/callback"} { - uri := fmt.Sprintf("http://%s%s", tc.Host, path) - - rw := httptest.NewRecorder() - req := httptest.NewRequest("GET", uri, nil) - - authMux.ServeHTTP(rw, req) - if rw.Code != tc.ExpectedStatusCode { - t.Errorf("got unexpected status code") - t.Errorf("want %v", tc.ExpectedStatusCode) - t.Errorf(" got %v", rw.Code) - t.Errorf(" headers %v", rw) - t.Errorf(" body: %q", rw.Body) - } - } - }) - } -} - func TestRobotsTxt(t *testing.T) { - opts := testOpts(t, "abced", "testtest") - opts.Host = "example.com" - opts.Validate() - authMux, err := NewAuthenticatorMux(opts, nil) + config := testConfiguration(t) + authMux, err := NewAuthenticatorMux(config, nil) if err != nil { t.Fatalf("unexpected err creating auth mux: %v", err) } rw := httptest.NewRecorder() - req := httptest.NewRequest("GET", "https://example.com/robots.txt", nil) + req := httptest.NewRequest("GET", fmt.Sprintf("https://%s/robots.txt", config.ServerConfig.Host), nil) authMux.ServeHTTP(rw, req) if rw.Code != http.StatusOK { diff --git a/internal/auth/options.go b/internal/auth/options.go index 10ec4b68..693bba8c 100644 --- a/internal/auth/options.go +++ b/internal/auth/options.go @@ -3,310 +3,64 @@ package auth import ( "encoding/base64" "fmt" - "net/http" "net/url" "os" "path" - "reflect" - "strings" - "time" "github.com/buzzfeed/sso/internal/auth/providers" + "github.com/buzzfeed/sso/internal/pkg/aead" "github.com/buzzfeed/sso/internal/pkg/groups" + "github.com/buzzfeed/sso/internal/pkg/sessions" "github.com/datadog/datadog-go/statsd" - "github.com/spf13/viper" ) -// Options are config options that can be set by environment variables -// ClientID - string - the OAuth ClientID ie "123456.apps.googleusercontent.com" -// ClientSecret string - the OAuth Client Secret -// OrgName - string - if using Okta as the provider, the Okta domain to use -// ProxyClientID - string - the client id that matches the sso proxy client id -// ProxyClientSecret - string - the client secret that matches the sso proxy client secret -// Scheme - string - The scheme to use for internal redirects -// Host - string - The host that is in the header that is required on incoming requests -// Port - string - Port to listen on -// EmailDomains - []string - authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email -// EmailAddresses - []string - authenticate emails with the specified email address (may be given multiple times). Use * to authenticate any email -// ProxyRootDomains - []string - only redirect to specified proxy domains (may be given multiple times) -// GoogleAdminEmail - string - the google admin to impersonate for api calls -// GoogleServiceAccountJSON - string - the path to the service account json credentials -// CookieSecret - string - the seed string for secure cookies (optionally base64 encoded) -// CookieDomain - string - an optional cookie domain to force cookies to (ie: .yourcompany.com)* -// CookieExpire - duration - expire timeframe for cookie, defaults at 168 hours -// CookieRefresh - duration - refresh the cookie after this duration default 0 -// CookieSecure - bool - set secure (HTTPS) cookie flag -// CookieHTTPOnly - bool - set httponly cookie flag -// RequestTimeout - duration - overall request timeout -// AuthCodeSecret - string - the seed string for secure auth codes (optionally base64 encoded) -// GroupCacheProviderTTL - time.Duration - cache TTL for the group-cache provider used for on-demand group caching -// GroupsCacheRefreshTTL - time.Duratoin - cache TTL for the groups fillcache mechanism used to preemptively fill group caches -// Provider - provider name -// ProviderSlug - string - client-side string used to uniquely identify a specific instantiation of an identity provider -// ProviderServerID - string - if using Okta as the provider, the authorisation server ID (defaults to 'default') -// DefaultProviderSlug - string = default client-side string to use as the default identity provider -// Scope - Oauth scope specification -// ApprovalPrompt - OAuth approval prompt -// RequestLogging - bool to log requests -// StatsdPort - port where statsd client listens -// StatsdHost - host where statsd client listens -type Options struct { - ClientID string `mapstructure:"client_id"` - ClientSecret string `mapstructure:"client_secret"` - ProxyClientID string `mapstructure:"proxy_client_id"` - ProxyClientSecret string `mapstructure:"proxy_client_secret"` - - Scheme string `mapstructure:"scheme"` - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - - EmailDomains []string `mapstructure:"sso_email_domain"` - EmailAddresses []string `mapstructure:"sso_email_addresses"` - ProxyRootDomains []string `mapstructure:"proxy_root_domain"` - - GoogleAdminEmail string `mapstructure:"google_admin_email"` - GoogleServiceAccountJSON string `mapstructure:"google_service_account_json"` - - OrgURL string `mapstructure:"okta_org_url"` - - CookieName string `mapstructure:"cookie_name"` - CookieSecret string `mapstructure:"cookie_secret"` - CookieDomain string `mapstructure:"cookie_domain"` - CookieExpire time.Duration `mapstructure:"cookie_expire"` - CookieRefresh time.Duration `mapstructure:"cookie_refresh"` - CookieSecure bool `mapstructure:"cookie_secure"` - CookieHTTPOnly bool `mapstructure:"cookie_http_only"` - - RequestTimeout time.Duration `mapstructure:"request_timeout"` - TCPWriteTimeout time.Duration `mapstructure:"tcp_write_timeout"` - TCPReadTimeout time.Duration `mapstructure:"tcp_read_timeout"` - - AuthCodeSecret string `mapstructure:"auth_code_secret"` - - GroupCacheProviderTTL time.Duration `mapstructure:"group_cache_provider_ttl"` - GroupsCacheRefreshTTL time.Duration `mapstructure:"groups_cache_refresh_ttl"` - SessionLifetimeTTL time.Duration `mapstructure:"session_lifetime_ttl"` - - // These options allow for other providers besides Google, with potential overrides. - Provider string `mapstructure:"provider"` - ProviderSlug string `mapstructure:"provider_slug"` - ProviderServerID string `mapstructure:"provider_server_id"` - - // These is the default provider - DefaultProviderSlug string `mapstructure:"default_provider_slug"` - - Scope string `mapstructure:"scope"` - ApprovalPrompt string `mapstructure:"approval_prompt"` - - RequestLogging bool `mapstructure:"request_logging"` - - StatsdPort int `mapstructure:"statsd_port"` - StatsdHost string `mapstructure:"statsd_host"` - - // internal values that are set after config validation - decodedCookieSecret []byte - GroupsCacheStopFunc func() -} - -// NewOptions returns new options with the below overrides -func NewOptions() (*Options, error) { - v := viper.New() - options, err := loadVars(v) - if err != nil { - return nil, err - } - return options, nil -} - -// loadVars loads viper variables and returns a filled Options struct -func loadVars(v *viper.Viper) (*Options, error) { - var opts Options - - bindAllOptVars(v, reflect.TypeOf(&opts).Elem(), "mapstructure") - setDefaults(v) - - err := v.Unmarshal(&opts) - if err != nil { - return nil, fmt.Errorf("unable to decode env vars into options struct") - } - return &opts, nil -} - -// bindAllOptVars takes in a struct with tags and uses the tag values to bind env vars -func bindAllOptVars(v *viper.Viper, t reflect.Type, tag string) error { - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - tagValue := field.Tag.Get(tag) - err := v.BindEnv(tagValue) - if err != nil { - return fmt.Errorf("Unable to bind env var: %q", tagValue) - } - } - return nil -} - -// setDefaults sets config defaults for the default viper instance -func setDefaults(v *viper.Viper) { - defaultVars := map[string]interface{}{ - "port": 4180, - "scheme": "https", - "cookie_expire": "168h", - "cookie_name": "_sso_auth", - "cookie_refresh": "1h", - "cookie_secure": true, - "cookie_http_only": true, - "tcp_write_timeout": "30s", - "tcp_read_timeout": "30s", - "groups_cache_refresh_ttl": "10m", - "group_cache_provider_ttl": "10m", - "session_lifetime_ttl": "720h", - "provider": "google", - "provider_slug": "google", - "provider_server_id": "default", - "default_provider_slug": "google", - "approval_prompt": "force", - "request_logging": true, - "request_timeout": "2s", - } - for key, value := range defaultVars { - v.SetDefault(key, value) - } -} - -func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) { - parsed, err := url.Parse(toParse) - if err != nil { - return nil, append(msgs, fmt.Sprintf( - "error parsing %s-url=%q %s", urltype, toParse, err)) - } - return parsed, msgs -} - -// Validate validates options -func (o *Options) Validate() error { - msgs := make([]string, 0) - if o.CookieSecret == "" { - msgs = append(msgs, "missing setting: cookie-secret") - } - if o.ClientID == "" { - msgs = append(msgs, "missing setting: client-id") - } - if o.ClientSecret == "" { - msgs = append(msgs, "missing setting: client-secret") - } - if len(o.EmailDomains) == 0 && len(o.EmailAddresses) == 0 { - msgs = append(msgs, "missing setting for email validation: email-domain or email-address required.\n use email-domain=* to authorize all email addresses") - } - if len(o.ProxyRootDomains) == 0 { - msgs = append(msgs, "missing setting: proxy-root-domain") - } - if o.ProxyClientID == "" { - msgs = append(msgs, "missing setting: proxy-client-id") - } - if o.ProxyClientSecret == "" { - msgs = append(msgs, "missing setting: proxy-client-secret") - } - if o.Host == "" { - msgs = append(msgs, "missing setting: required-host-header") - } - - if len(o.OrgURL) > 0 { - o.OrgURL = strings.Trim(o.OrgURL, `"`) - } - if len(o.ProviderServerID) > 0 { - o.ProviderServerID = strings.Trim(o.ProviderServerID, `"`) - } - - decodedCookieSecret, err := base64.StdEncoding.DecodeString(o.CookieSecret) - if err != nil { - msgs = append(msgs, "Invalid value for COOKIE_SECRET; expected base64-encoded bytes, as from `openssl rand 32 -base64`") - } - - validCookieSecretLength := false - for _, i := range []int{32, 64} { - if len(decodedCookieSecret) == i { - validCookieSecretLength = true - } - } - - if !validCookieSecretLength { - msgs = append(msgs, fmt.Sprintf("Invalid value for COOKIE_SECRET; must decode to 32 or 64 bytes, but decoded to %d bytes", len(decodedCookieSecret))) - } - - o.decodedCookieSecret = decodedCookieSecret - - if o.CookieRefresh >= o.CookieExpire { - msgs = append(msgs, fmt.Sprintf( - "cookie_refresh (%s) must be less than "+ - "cookie_expire (%s)", - o.CookieRefresh.String(), - o.CookieExpire.String())) - } - - msgs = validateCookieName(o, msgs) - - if o.StatsdHost == "" { - msgs = append(msgs, "missing setting: no host specified for statsd metrics collections") - } - - if o.StatsdPort == 0 { - msgs = append(msgs, "missing setting: no port specified for statsd metrics collections") - } - - if len(msgs) != 0 { - return fmt.Errorf("Invalid configuration:\n %s", - strings.Join(msgs, "\n ")) - } - return nil -} - -func validateCookieName(o *Options, msgs []string) []string { - cookie := &http.Cookie{Name: o.CookieName} - if cookie.String() == "" { - return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.CookieName)) - } - return msgs -} - -func newProvider(o *Options) (providers.Provider, error) { +func newProvider(pc ProviderConfig, sc SessionConfig) (providers.Provider, error) { p := &providers.ProviderData{ - ProviderSlug: o.ProviderSlug, - Scope: o.Scope, - ClientID: o.ClientID, - ClientSecret: o.ClientSecret, - ApprovalPrompt: o.ApprovalPrompt, - SessionLifetimeTTL: o.SessionLifetimeTTL, + ProviderSlug: pc.ProviderSlug, + Scope: pc.Scope, + ClientID: pc.ClientConfig.ID, + ClientSecret: pc.ClientConfig.Secret, + SessionLifetimeTTL: sc.SessionLifetimeTTL, } var singleFlightProvider providers.Provider - switch o.Provider { + switch pc.ProviderType { case providers.GoogleProviderName: // Google - if o.GoogleServiceAccountJSON != "" { - _, err := os.Open(o.GoogleServiceAccountJSON) + gpc := pc.GoogleProviderConfig + p.ApprovalPrompt = gpc.ApprovalPrompt + + if gpc.Credentials != "" { + _, err := os.Open(gpc.Credentials) if err != nil { - return nil, fmt.Errorf("invalid Google credentials file: %s", o.GoogleServiceAccountJSON) + return nil, fmt.Errorf("invalid Google credentials file: %s", gpc.Credentials) } } - googleProvider, err := providers.NewGoogleProvider(p, o.GoogleAdminEmail, o.GoogleServiceAccountJSON) + + googleProvider, err := providers.NewGoogleProvider(p, gpc.Impersonate, gpc.Credentials) if err != nil { return nil, err } - cache := groups.NewFillCache(googleProvider.PopulateMembers, o.GroupsCacheRefreshTTL) + + cache := groups.NewFillCache(googleProvider.PopulateMembers, pc.GroupCacheConfig.Interval.Refresh) googleProvider.GroupsCache = cache - o.GroupsCacheStopFunc = cache.Stop + singleFlightProvider = providers.NewSingleFlightProvider(googleProvider) case providers.OktaProviderName: - oktaProvider, err := providers.NewOktaProvider(p, o.OrgURL, o.ProviderServerID) + opc := pc.OktaProviderConfig + + oktaProvider, err := providers.NewOktaProvider(p, opc.OrgURL, opc.ServerID) if err != nil { return nil, err } - tags := []string{"provider:okta"} - groupsCache := providers.NewGroupCache(oktaProvider, o.GroupCacheProviderTTL, oktaProvider.StatsdClient, tags) - singleFlightProvider = providers.NewSingleFlightProvider(groupsCache) + tags := []string{"provider:okta"} + cache := providers.NewGroupCache(oktaProvider, pc.GroupCacheConfig.Interval.Provider, oktaProvider.StatsdClient, tags) + singleFlightProvider = providers.NewSingleFlightProvider(cache) + case "test": + return providers.NewTestProvider(nil), nil default: - return nil, fmt.Errorf("unimplemented provider: %q", o.Provider) + return nil, fmt.Errorf("unimplemented provider.type: %q", pc.ProviderType) } return singleFlightProvider, nil @@ -334,35 +88,64 @@ func SetStatsdClient(statsdClient *statsd.Client) func(*Authenticator) error { } } -// SetRedirectURL takes an options struct and identity provider slug to construct the +// SetRedirectURL takes an identity provider slug to construct the // url callback using the slug and configured redirect url. -func SetRedirectURL(opts *Options, slug string) func(*Authenticator) error { +func SetRedirectURL(serverConfig ServerConfig, slug string) func(*Authenticator) error { return func(a *Authenticator) error { a.redirectURL = &url.URL{ - Scheme: opts.Scheme, - Host: opts.Host, + Scheme: serverConfig.Scheme, + Host: serverConfig.Host, Path: path.Join(slug, "callback"), } return nil } } -// SetDefaultRedirectURL takes an options struct to construct the url callback and redirect. -func SetDefaultRedirectURL(opts *Options) func(*Authenticator) error { +// SetValidator sets the email validator +func SetValidator(validator func(string) bool) func(*Authenticator) error { return func(a *Authenticator) error { - a.redirectURL = &url.URL{ - Scheme: opts.Scheme, - Host: opts.Host, - Path: path.Join("callback"), - } + a.Validator = validator return nil } } -// SetValidator sets the email validator -func SetValidator(validator func(string) bool) func(*Authenticator) error { +// SetCookieStore sets the cookie store to use a miscreant cipher +func SetCookieStore(sessionConfig SessionConfig, providerSlug string) func(*Authenticator) error { return func(a *Authenticator) error { - a.Validator = validator + decodedKey, err := base64.StdEncoding.DecodeString(sessionConfig.Key) + if err != nil { + return err + } + + codeCipher, err := aead.NewMiscreantCipher([]byte(decodedKey)) + if err != nil { + return err + } + + cc := sessionConfig.CookieConfig + decodedCookieSecret, err := base64.StdEncoding.DecodeString(cc.Secret) + if err != nil { + return err + } + + cookieName := fmt.Sprintf("%s_%s", cc.Name, providerSlug) + cookieStore, err := sessions.NewCookieStore(cookieName, + sessions.CreateMiscreantCookieCipher(decodedCookieSecret), + func(c *sessions.CookieStore) error { + c.CookieDomain = cc.Domain + c.CookieHTTPOnly = cc.HTTPOnly + c.CookieExpire = cc.Expire + c.CookieSecure = cc.Secure + return nil + }) + + if err != nil { + return err + } + + a.csrfStore = cookieStore + a.sessionStore = cookieStore + a.AuthCodeCipher = codeCipher return nil } } diff --git a/internal/auth/options_test.go b/internal/auth/options_test.go deleted file mode 100644 index 1d392757..00000000 --- a/internal/auth/options_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package auth - -import ( - "fmt" - "strings" - "testing" - "time" - - "github.com/buzzfeed/sso/internal/pkg/testutil" -) - -func testOptions(t *testing.T) *Options { - o, err := NewOptions() - if err != nil { - t.Fatalf("error while instantiating config options: %s", err.Error()) - } - - o.CookieSecret = "foobar" - o.ClientID = "bazquux" - o.ClientSecret = "xyzzyplugh" - o.EmailDomains = []string{"*"} - o.ProxyClientID = "abcdef" - o.ProxyClientSecret = "testtest" - o.ProxyRootDomains = []string{"*"} - o.StatsdHost = "statsdhost" - o.StatsdPort = 12344 - o.Host = "/" - o.CookieRefresh = time.Hour - o.CookieSecret = testEncodedCookieSecret - return o -} - -func errorMsg(msgs []string) string { - result := make([]string, 0) - result = append(result, "Invalid configuration:") - result = append(result, msgs...) - return strings.Join(result, "\n ") -} - -func TestNewOptions(t *testing.T) { - o, _ := NewOptions() - o.EmailDomains = []string{"*"} - err := o.Validate() - testutil.NotEqual(t, nil, err) - - expected := errorMsg([]string{ - "missing setting: cookie-secret", - "missing setting: client-id", - "missing setting: client-secret", - "missing setting: proxy-root-domain", - "missing setting: proxy-client-id", - "missing setting: proxy-client-secret", - "missing setting: required-host-header", - "Invalid value for COOKIE_SECRET; must decode to 32 or 64 bytes, but decoded to 0 bytes", - "missing setting: no host specified for statsd metrics collections", - "missing setting: no port specified for statsd metrics collections", - }) - testutil.Equal(t, expected, err.Error()) -} - -func TestInitializedOptions(t *testing.T) { - o := testOptions(t) - testutil.Equal(t, nil, o.Validate()) -} - -func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) { - o := testOptions(t) - testutil.Equal(t, nil, o.Validate()) - - o.CookieSecret = testEncodedCookieSecret - o.CookieRefresh = o.CookieExpire - testutil.NotEqual(t, nil, o.Validate()) - - o.CookieRefresh -= time.Duration(1) - testutil.Equal(t, nil, o.Validate()) -} - -func TestBase64CookieSecret(t *testing.T) { - o := testOptions(t) - testutil.Equal(t, nil, o.Validate()) - - // 32 byte, base64 (urlsafe) encoded key - o.CookieSecret = testEncodedCookieSecret - testutil.Equal(t, nil, o.Validate()) - - // 32 byte, base64 (urlsafe) encoded key, w/o padding - o.CookieSecret = testEncodedCookieSecret - testutil.Equal(t, nil, o.Validate()) -} - -func TestValidateCookie(t *testing.T) { - o := testOptions(t) - o.CookieName = "_valid_cookie_name" - testutil.Equal(t, nil, o.Validate()) -} - -func TestValidateCookieBadName(t *testing.T) { - o := testOptions(t) - o.CookieName = "_bad_cookie_name{}" - err := o.Validate() - testutil.Equal(t, err.Error(), "Invalid configuration:\n"+ - fmt.Sprintf(" invalid cookie name: %q", o.CookieName)) -} diff --git a/internal/auth/providers/google.go b/internal/auth/providers/google.go index c3e85218..f8842416 100644 --- a/internal/auth/providers/google.go +++ b/internal/auth/providers/google.go @@ -31,16 +31,7 @@ type GoogleProvider struct { } // NewGoogleProvider returns a new GoogleProvider and sets the provider url endpoints. -func NewGoogleProvider(p *ProviderData, adminEmail, credsFilePath string) (*GoogleProvider, error) { - if adminEmail != "" || credsFilePath != "" { - if adminEmail == "" { - return nil, errors.New("missing setting: google-admin-email") - } - if credsFilePath == "" { - return nil, errors.New("missing setting: google-service-account-json") - } - } - +func NewGoogleProvider(p *ProviderData, impersonateUser, credsFilePath string) (*GoogleProvider, error) { p.ProviderName = "Google" p.SignInURL = &url.URL{Scheme: "https", Host: "accounts.google.com", @@ -86,8 +77,9 @@ func NewGoogleProvider(p *ProviderData, adminEmail, credsFilePath string) (*Goog if err != nil { return nil, errors.New("could not read google credentials file") } + googleProvider.AdminService = &GoogleAdminService{ - adminService: getAdminService(adminEmail, credsReader), + adminService: getAdminService(impersonateUser, credsReader), cb: googleProvider.cb, } } diff --git a/internal/auth/providers/google_admin.go b/internal/auth/providers/google_admin.go index 3699fb8d..14b319a6 100644 --- a/internal/auth/providers/google_admin.go +++ b/internal/auth/providers/google_admin.go @@ -30,24 +30,27 @@ type GoogleAdminService struct { cb *circuit.Breaker } -func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Service { +func getAdminService(impersonateUser string, credentialsReader io.Reader) *admin.Service { logger := log.NewLogEntry() data, err := ioutil.ReadAll(credentialsReader) if err != nil { logger.WithError(err).Fatal("can't read Google credentials file") } + conf, err := google.JWTConfigFromJSON(data, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope) if err != nil { logger.WithError(err).Fatal("can't load Google credentials file") } - conf.Subject = adminEmail + conf.Subject = impersonateUser client := conf.Client(oauth2.NoContext) + adminService, err := admin.New(client) if err != nil { logger.WithError(err).Fatal() } + return adminService } diff --git a/internal/auth/providers/test_provider.go b/internal/auth/providers/test_provider.go index 0c8a5ec6..1bfba01c 100644 --- a/internal/auth/providers/test_provider.go +++ b/internal/auth/providers/test_provider.go @@ -27,26 +27,12 @@ type TestProvider struct { } // NewTestProvider creates a new mock test provider. -func NewTestProvider(providerURL *url.URL) *TestProvider { +func NewTestProvider(_ *url.URL) *TestProvider { return &TestProvider{ ProviderData: &ProviderData{ ProviderName: "Test Provider", - SignInURL: &url.URL{ - Scheme: "http", - Host: providerURL.Host, - Path: "/authorize", - }, - RedeemURL: &url.URL{ - Scheme: "http", - Host: providerURL.Host, - Path: "/token", - }, - ProfileURL: &url.URL{ - Scheme: "http", - Host: providerURL.Host, - Path: "/profile", - }, - Scope: "profile.email", + ProviderSlug: "test", + Scope: "profile.email", }, } } diff --git a/internal/auth/static_files_test.go b/internal/auth/static_files_test.go index 3495a623..c7d13609 100644 --- a/internal/auth/static_files_test.go +++ b/internal/auth/static_files_test.go @@ -8,10 +8,8 @@ import ( ) func TestStaticFiles(t *testing.T) { - opts := testOpts(t, "abced", "testtest") - opts.Host = "localhost" - opts.Validate() - authMux, err := NewAuthenticatorMux(opts, nil) + config := testConfiguration(t) + authMux, err := NewAuthenticatorMux(config, nil) if err != nil { t.Fatalf("unexpected error creating auth mux: %v", err) } diff --git a/internal/proxy/options.go b/internal/proxy/options.go index d6276791..a62a06ea 100644 --- a/internal/proxy/options.go +++ b/internal/proxy/options.go @@ -41,7 +41,7 @@ import ( // CookieHTTPOnly - set HttpOnly cookie flag // PassAccessToken - send access token in the http headers // Provider - OAuth provider -// ProviderSlug - OAuth provider slug, used internally to identity a specific provider +// DefaultProviderSlug - OAuth provider slug, used internally to identity a specific provider // Scope - OAuth scope specification // SessionLifetimeTTL - time to live for a session lifetime // SessionValidTTL - time to live for a valid session @@ -82,9 +82,9 @@ type Options struct { PassAccessToken bool `envconfig:"PASS_ACCESS_TOKEN" default:"false"` - Provider string `envconfig:"PROVIDER" default:"sso"` - ProviderSlug string `envconfig:"PROVIDER_SLUG" default:"google"` - Scope string `envconfig:"SCOPE"` + Provider string `envconfig:"PROVIDER" default:"sso"` + DefaultProviderSlug string `envconfig:"DEFAULT_PROVIDER_SLUG" default:"google"` + Scope string `envconfig:"SCOPE"` SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL" default:"720h"` SessionValidTTL time.Duration `envconfig:"SESSION_VALID_TTL" default:"1m"` @@ -104,7 +104,6 @@ type Options struct { // internal values that are set after config validation upstreamConfigs []*UpstreamConfig - provider providers.Provider decodedCookieSecret []byte } @@ -182,6 +181,7 @@ func (o *Options) Validate() error { AllowedGroups: o.DefaultAllowedGroups, Timeout: o.DefaultUpstreamTimeout, ResetDeadline: o.DefaultUpstreamTCPResetDeadline, + ProviderSlug: o.DefaultProviderSlug, CookieName: o.CookieName, } @@ -237,12 +237,12 @@ func parseProviderInfo(o *Options) error { if err != nil { return err } + if providerURL.Scheme == "" || providerURL.Host == "" { return errors.New("provider-url must include scheme and host") } var providerURLInternal *url.URL - if o.ProviderURLInternalString != "" { providerURLInternal, err = url.Parse(o.ProviderURLInternalString) if err != nil { @@ -253,22 +253,37 @@ func parseProviderInfo(o *Options) error { } } + return nil +} + +func newProvider(opts *Options, upstreamConfig *UpstreamConfig) (providers.Provider, error) { + providerURL, err := url.Parse(opts.ProviderURLString) + if err != nil { + return nil, err + } + + var providerURLInternal *url.URL + if opts.ProviderURLInternalString != "" { + providerURLInternal, err = url.Parse(opts.ProviderURLInternalString) + if err != nil { + return nil, err + } + } + providerData := &providers.ProviderData{ - ClientID: o.ClientID, - ClientSecret: o.ClientSecret, + ClientID: opts.ClientID, + ClientSecret: opts.ClientSecret, ProviderURL: providerURL, ProviderURLInternal: providerURLInternal, - ProviderSlug: o.ProviderSlug, - Scope: o.Scope, - SessionLifetimeTTL: o.SessionLifetimeTTL, - SessionValidTTL: o.SessionValidTTL, - GracePeriodTTL: o.GracePeriodTTL, + ProviderSlug: upstreamConfig.ProviderSlug, + Scope: opts.Scope, + SessionLifetimeTTL: opts.SessionLifetimeTTL, + SessionValidTTL: opts.SessionValidTTL, + GracePeriodTTL: opts.GracePeriodTTL, } - p := providers.New(o.Provider, providerData, o.StatsdClient) - o.provider = providers.NewSingleFlightProvider(p, o.StatsdClient) - - return nil + p := providers.New(opts.Provider, providerData, opts.StatsdClient) + return providers.NewSingleFlightProvider(p, opts.StatsdClient), nil } func validateCookieName(o *Options, msgs []string) []string { diff --git a/internal/proxy/options_test.go b/internal/proxy/options_test.go index 39348f78..57065459 100644 --- a/internal/proxy/options_test.go +++ b/internal/proxy/options_test.go @@ -17,7 +17,7 @@ func testOptions() *Options { o.ClientID = "bazquux" o.ClientSecret = "xyzzyplugh" o.EmailDomains = []string{"*"} - o.ProviderSlug = "idp" + o.DefaultProviderSlug = "idp" o.ProviderURLString = "https://www.example.com" o.UpstreamConfigsFile = "testdata/upstream_configs.yml" o.Cluster = "sso" @@ -64,23 +64,6 @@ func TestInitializedOptions(t *testing.T) { testutil.Equal(t, nil, o.Validate()) } -func TestDefaultProviderApiSettings(t *testing.T) { - o := testOptions() - testutil.Equal(t, nil, o.Validate()) - p := o.provider.Data() - testutil.Equal(t, "https://www.example.com/idp/sign_in", - p.SignInURL.String()) - testutil.Equal(t, "https://www.example.com/idp/sign_out", - p.SignOutURL.String()) - testutil.Equal(t, "https://www.example.com/idp/redeem", - p.RedeemURL.String()) - testutil.Equal(t, "https://www.example.com/idp/validate", - p.ValidateURL.String()) - testutil.Equal(t, "https://www.example.com/idp/profile", - p.ProfileURL.String()) - testutil.Equal(t, "", p.Scope) -} - func TestProviderURLValidation(t *testing.T) { testCases := []struct { name string @@ -139,18 +122,27 @@ func TestProviderURLValidation(t *testing.T) { o.ProviderURLString = tc.providerURLString o.ProviderURLInternalString = tc.providerURLInternalString err := o.Validate() + if tc.expectedError != "" { if err == nil { - t.Errorf("expected error, got nil") - } else if err.Error() != tc.expectedError { - t.Errorf("expected error %q, got %q", tc.expectedError, err.Error()) + t.Fatalf("expected error, got nil") + } + if err.Error() != tc.expectedError { + t.Fatalf("expected error %q, got %q", tc.expectedError, err.Error()) } + // our errors have matched, and test has passed + return + } + + provider, err := newProvider(o, &UpstreamConfig{ProviderSlug: "idp"}) + if err != nil { + t.Fatalf("unexpected err creating provider: %v", err) } if tc.expectedSignInURL != "" { - testutil.Equal(t, o.provider.Data().SignInURL.String(), tc.expectedSignInURL) + testutil.Equal(t, provider.Data().SignInURL.String(), tc.expectedSignInURL) } if tc.expectedProviderURLInternalString != "" { - testutil.Equal(t, o.provider.Data().ProviderURLInternal.String(), tc.expectedProviderURLInternalString) + testutil.Equal(t, provider.Data().ProviderURLInternal.String(), tc.expectedProviderURLInternalString) } }) } diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index f09e5e95..64062a7c 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -33,13 +33,18 @@ func New(opts *Options) (*SSOProxy, error) { hostRouter := hostmux.NewRouter() for _, upstreamConfig := range opts.upstreamConfigs { + provider, err := newProvider(opts, upstreamConfig) + if err != nil { + return nil, err + } + handler, err := NewUpstreamReverseProxy(upstreamConfig, requestSigner) if err != nil { return nil, err } optFuncs = append(optFuncs, - SetProvider(opts.provider), + SetProvider(provider), SetCookieStore(opts), SetUpstreamConfig(upstreamConfig), SetProxyHandler(handler), diff --git a/internal/proxy/proxy_config.go b/internal/proxy/proxy_config.go index b040ed85..120eb1f9 100644 --- a/internal/proxy/proxy_config.go +++ b/internal/proxy/proxy_config.go @@ -61,6 +61,7 @@ type UpstreamConfig struct { HeaderOverrides map[string]string SkipRequestSigning bool CookieName string + ProviderSlug string } // RouteConfig maps to the yaml config fields, @@ -95,6 +96,7 @@ type OptionsConfig struct { ResetDeadline time.Duration `yaml:"reset_deadline"` FlushInterval time.Duration `yaml:"flush_interval"` SkipRequestSigning bool `yaml:"skip_request_signing"` + ProviderSlug string `yaml:"provider_slug"` // CookieName is still set globally, so we do not provide override behavior CookieName string @@ -400,6 +402,7 @@ func parseOptionsConfig(proxy *UpstreamConfig, defaultOpts *OptionsConfig) error proxy.PreserveHost = dst.PreserveHost proxy.SkipRequestSigning = dst.SkipRequestSigning proxy.CookieName = dst.CookieName + proxy.ProviderSlug = dst.ProviderSlug proxy.RouteConfig.Options = nil