From 7a3406d1cd930698cce1395532bdf380db48b08b Mon Sep 17 00:00:00 2001 From: James DeFelice Date: Tue, 12 Jul 2016 12:53:44 +0000 Subject: [PATCH] cleanup: introduce DoerFunc, DoerFactory, ConfigMap, and an auth plugin registry --- auth.go | 12 +++++ docs/docs/configuration-parameters.md | 2 +- httpcli/basic.go | 26 --------- httpcli/basic/basic.go | 28 ++++++++++ httpcli/config.go | 60 ++++++++++++++++++--- httpcli/doer.go | 30 ++++++++++- httpcli/iam.go | 76 -------------------------- httpcli/iam/iam.go | 78 +++++++++++++++++++++++++++ main.go | 1 + records/generator.go | 24 +++------ 10 files changed, 208 insertions(+), 129 deletions(-) create mode 100644 auth.go delete mode 100644 httpcli/basic.go create mode 100644 httpcli/basic/basic.go delete mode 100644 httpcli/iam.go create mode 100644 httpcli/iam/iam.go diff --git a/auth.go b/auth.go new file mode 100644 index 00000000..46f615ba --- /dev/null +++ b/auth.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/mesosphere/mesos-dns/httpcli/basic" + "github.com/mesosphere/mesos-dns/httpcli/iam" +) + +// initAuth registers HTTP client factories for supported authentication types +func initAuth() { + basic.Register() + iam.Register() +} diff --git a/docs/docs/configuration-parameters.md b/docs/docs/configuration-parameters.md index 1f602f1f..4e5bc4a8 100644 --- a/docs/docs/configuration-parameters.md +++ b/docs/docs/configuration-parameters.md @@ -56,7 +56,7 @@ Defaults to `30` seconds. It is sufficient to specify just one of the `zk` or `masters` field. If both are defined, Mesos-DNS will first attempt to detect the leading master through Zookeeper. If Zookeeper is not responding, it will fall back to using the `masters` field. Both `zk` and `master` fields are static. To update them you need to restart Mesos-DNS. We recommend you use the `zk` field since this allows the dynamic addition to Mesos masters. -`mesosAuthentication` configures the authentication mechanism for talking to the Mesos cluster. Valid values are 'none', 'basic' (see `mesosCredentials`), and 'iam'. Default is 'none'. +`mesosAuthentication` configures the authentication mechanism for talking to the Mesos cluster. Valid values are '', 'basic' (see `mesosCredentials`), and 'iam'. Default is ''. `mesosCredentials` is a dictionary containing a `principal` and a `secret`, corresponding to a configured authentication principal for the Mesos masters. Starting with Mesos `1.0.0`, if the masters have `http_authentication` enabled, then Mesos-DNS must authenticate. You must specify `mesosAuthentication`: `basic` to use this configuration. diff --git a/httpcli/basic.go b/httpcli/basic.go deleted file mode 100644 index 805cfdf0..00000000 --- a/httpcli/basic.go +++ /dev/null @@ -1,26 +0,0 @@ -package httpcli - -import ( - "github.com/mesosphere/mesos-dns/httpcli/basic" - "net/http" -) - -// NewBasic wraps an HTTP transactor given basic credentials -func NewBasic(client *http.Client, credentials basic.Credentials) Doer { - return &basicAuthClient{ - client: client, - credentials: credentials, - } -} - -type basicAuthClient struct { - client *http.Client - credentials basic.Credentials -} - -// Do implements Doer for iamAuthClient -func (a *basicAuthClient) Do(req *http.Request) (*http.Response, error) { - req.SetBasicAuth(a.credentials.Principal, a.credentials.Secret) - - return a.client.Do(req) -} diff --git a/httpcli/basic/basic.go b/httpcli/basic/basic.go new file mode 100644 index 00000000..f8e22d16 --- /dev/null +++ b/httpcli/basic/basic.go @@ -0,0 +1,28 @@ +package basic + +import ( + "fmt" + "net/http" + + "github.com/mesosphere/mesos-dns/httpcli" +) + +// Register registers a DoerFactory for HTTP Basic authentication +func Register() { + httpcli.Register(httpcli.AuthBasic, httpcli.DoerFactory(func(cm httpcli.ConfigMap, c *http.Client) httpcli.Doer { + obj := cm.FindOrPanic(httpcli.AuthBasic) + config, ok := obj.(Credentials) + if !ok { + panic(fmt.Sprintf("expected Credentials instead of %#+v", obj)) + } + return Doer(c, config) + })) +} + +// Doer wraps an HTTP transactor given basic credentials +func Doer(client httpcli.Doer, credentials Credentials) httpcli.Doer { + return httpcli.DoerFunc(func(req *http.Request) (*http.Response, error) { + req.SetBasicAuth(credentials.Principal, credentials.Secret) + return client.Do(req) + }) +} diff --git a/httpcli/config.go b/httpcli/config.go index cde84b83..19e98238 100644 --- a/httpcli/config.go +++ b/httpcli/config.go @@ -1,15 +1,61 @@ package httpcli +import ( + "errors" + "fmt" + "net/http" + "sync" +) + // AuthMechanism enumerates the supported authentication strategies type AuthMechanism string +// ErrDuplicateAuthRegistration signifies a configuration error in which the same +// AuthMechanism is being registered multiple times. +var ErrDuplicateAuthRegistration = errors.New("duplicate auth mechanism registration") + +// AuthNone, et al. represent the complete set of supported authentication mechanisms const ( - // AuthNone specifies no authentication mechanism - AuthNone AuthMechanism = "none" + AuthNone AuthMechanism = "" // AuthNone specifies no authentication mechanism + AuthBasic = "basic" // AuthBasic specifies to use HTTP Basic + AuthIAM = "iam" // AuthIAM specifies to use IAM / JDK authentication +) - // AuthBasic specifies to use HTTP Basic - AuthBasic AuthMechanism = "basic" +var registry = struct { + sync.Mutex + factories map[AuthMechanism]DoerFactory +}{ + factories: map[AuthMechanism]DoerFactory{ + AuthNone: DoerFactory(func(_ ConfigMap, client *http.Client) Doer { return client }), + }, +} - // AuthIAM specifies to use IAM / JDK authentication - AuthIAM AuthMechanism = "iam" -) +// ConfigMap maps authentication configuration types to values +type ConfigMap map[AuthMechanism]interface{} + +// FindOrPanic returns the mapped configuration for the given auth mechanism or else panics +func (cm ConfigMap) FindOrPanic(am AuthMechanism) interface{} { + obj, ok := cm[am] + if !ok { + panic(fmt.Sprintf("missing configuration for auth mechanism %q", am)) + } + return obj +} + +// Register associates an AuthMechanism with a DoerFactory +func Register(am AuthMechanism, df DoerFactory) { + registry.Lock() + defer registry.Unlock() + + if _, ok := registry.factories[am]; ok { + panic(ErrDuplicateAuthRegistration) + } + registry.factories[am] = df +} + +func factoryFor(am AuthMechanism) (df DoerFactory, ok bool) { + registry.Lock() + defer registry.Unlock() + df, ok = registry.factories[am] + return +} diff --git a/httpcli/doer.go b/httpcli/doer.go index edc99819..65afd29e 100644 --- a/httpcli/doer.go +++ b/httpcli/doer.go @@ -4,9 +4,11 @@ import ( "crypto/tls" "crypto/x509" "errors" - "github.com/mesosphere/mesos-dns/urls" + "fmt" "net/http" "time" + + "github.com/mesosphere/mesos-dns/urls" ) // ErrAuthFailed is returned for any type of IAM authentication failure @@ -18,9 +20,35 @@ type Doer interface { Do(req *http.Request) (resp *http.Response, err error) } +// DoerFunc is the functional adaptation of Doer +type DoerFunc func(req *http.Request) (resp *http.Response, err error) + +// Do implements Doer for DoerFunc +func (df DoerFunc) Do(req *http.Request) (*http.Response, error) { return df(req) } + +// DoerFactory generates a Doer +type DoerFactory func(ConfigMap, *http.Client) Doer + // Option is a functional option type type Option func(*http.Client) +// New generates and returns an HTTP transactor given an optional IAM configuration and some set of +// functional options. +func New(am AuthMechanism, cm ConfigMap, options ...Option) Doer { + defaultClient := &http.Client{} + for i := range options { + if options[i] != nil { + options[i](defaultClient) + } + } + + df, ok := factoryFor(am) + if !ok { + panic(fmt.Sprintf("unregistered auth mechanism %q", am)) + } + return df(cm, defaultClient) +} + // Timeout returns an Option that configures client timeout func Timeout(timeout time.Duration) Option { return func(client *http.Client) { diff --git a/httpcli/iam.go b/httpcli/iam.go deleted file mode 100644 index b56c52c3..00000000 --- a/httpcli/iam.go +++ /dev/null @@ -1,76 +0,0 @@ -package httpcli - -import ( - "bytes" - "encoding/json" - "net/http" - "time" - - "github.com/dgrijalva/jwt-go" - "github.com/mesosphere/mesos-dns/errorutil" - "github.com/mesosphere/mesos-dns/httpcli/iam" -) - -// NewIAM wraps an HTTP transactor given an IAM configuration -func NewIAM(client *http.Client, config *iam.Config) Doer { - return &iamAuthClient{ - client: client, - config: config, - } -} - -type iamAuthClient struct { - client *http.Client - config *iam.Config -} - -// Do implements Doer for iamAuthClient -func (a *iamAuthClient) Do(req *http.Request) (*http.Response, error) { - // TODO if we still have a valid token, try using it first - token := jwt.New(jwt.SigningMethodRS256) - token.Claims["uid"] = a.config.ID - token.Claims["exp"] = time.Now().Add(time.Hour).Unix() - // SignedString will treat secret as PEM-encoded key - tokenStr, err := token.SignedString([]byte(a.config.Secret)) - if err != nil { - return nil, err - } - - authReq := struct { - UID string `json:"uid"` - Token string `json:"token,omitempty"` - }{ - UID: a.config.ID, - Token: tokenStr, - } - - b, err := json.Marshal(authReq) - if err != nil { - return nil, err - } - - authBody := bytes.NewBuffer(b) - resp, err := a.client.Post(a.config.LoginEndpoint, "application/json", authBody) - if err != nil { - return nil, err - } - defer errorutil.Ignore(resp.Body.Close) - if resp.StatusCode != 200 { - return nil, ErrAuthFailed - } - - var authResp struct { - Token string `json:"token"` - } - err = json.NewDecoder(resp.Body).Decode(&authResp) - if err != nil { - return nil, err - } - - if req.Header == nil { - req.Header = make(http.Header) - } - req.Header.Set("Authorization", "token="+authResp.Token) - - return a.client.Do(req) -} diff --git a/httpcli/iam/iam.go b/httpcli/iam/iam.go new file mode 100644 index 00000000..8a3c1ceb --- /dev/null +++ b/httpcli/iam/iam.go @@ -0,0 +1,78 @@ +package iam + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/mesosphere/mesos-dns/errorutil" + "github.com/mesosphere/mesos-dns/httpcli" +) + +// Register registers a DoerFactory for IAM (JWT-based) authentication +func Register() { + httpcli.Register(httpcli.AuthIAM, httpcli.DoerFactory(func(cm httpcli.ConfigMap, c *http.Client) httpcli.Doer { + obj := cm.FindOrPanic(httpcli.AuthIAM) + config, ok := obj.(Config) + if !ok { + panic(fmt.Sprintf("expected Config instead of %#+v", obj)) + } + return Doer(c, config) + })) +} + +// Doer wraps an HTTP transactor given an IAM configuration +func Doer(client *http.Client, config Config) httpcli.Doer { + return httpcli.DoerFunc(func(req *http.Request) (*http.Response, error) { + // TODO if we still have a valid token, try using it first + token := jwt.New(jwt.SigningMethodRS256) + token.Claims["uid"] = config.ID + token.Claims["exp"] = time.Now().Add(time.Hour).Unix() + // SignedString will treat secret as PEM-encoded key + tokenStr, err := token.SignedString([]byte(config.Secret)) + if err != nil { + return nil, err + } + + authReq := struct { + UID string `json:"uid"` + Token string `json:"token,omitempty"` + }{ + UID: config.ID, + Token: tokenStr, + } + + b, err := json.Marshal(authReq) + if err != nil { + return nil, err + } + + authBody := bytes.NewBuffer(b) + resp, err := client.Post(config.LoginEndpoint, "application/json", authBody) + if err != nil { + return nil, err + } + defer errorutil.Ignore(resp.Body.Close) + if resp.StatusCode != 200 { + return nil, httpcli.ErrAuthFailed + } + + var authResp struct { + Token string `json:"token"` + } + err = json.NewDecoder(resp.Body).Decode(&authResp) + if err != nil { + return nil, err + } + + if req.Header == nil { + req.Header = make(http.Header) + } + req.Header.Set("Authorization", "token="+authResp.Token) + + return client.Do(req) + }) +} diff --git a/main.go b/main.go index 39eaff15..8b747a2f 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ func main() { // initialize logging logging.SetupLogs() + initAuth() // initialize resolver config := records.SetConfig(*cjson) diff --git a/records/generator.go b/records/generator.go index 5530e515..40a3cd25 100644 --- a/records/generator.go +++ b/records/generator.go @@ -141,25 +141,13 @@ func WithConfig(config Config) Option { MaxIdleConnsPerHost: 2, TLSClientConfig: tlsClientConfig, }) - timeout = httpcli.Timeout(time.Duration(config.StateTimeoutSeconds) * time.Second) - defaultClient = &http.Client{} - doer httpcli.Doer + timeout = httpcli.Timeout(time.Duration(config.StateTimeoutSeconds) * time.Second) + configMap = httpcli.ConfigMap(map[httpcli.AuthMechanism]interface{}{ + httpcli.AuthBasic: config.MesosCredentials, + httpcli.AuthIAM: config.MesosAuthentication, + }) + doer = httpcli.New(config.MesosAuthentication, configMap, transport, timeout) ) - - transport(defaultClient) - timeout(defaultClient) - - switch config.MesosAuthentication { - case httpcli.AuthNone, "": - doer = defaultClient - case httpcli.AuthBasic: - doer = httpcli.NewBasic(defaultClient, config.MesosCredentials) - case httpcli.AuthIAM: - doer = httpcli.NewIAM(defaultClient, config.iamConfig) - default: - panic("I don't know about " + config.MesosAuthentication) - } - return func(rg *RecordGenerator) { rg.httpClient = doer rg.stateEndpoint = rg.stateEndpoint.With(