Skip to content

Commit

Permalink
cleanup: introduce DoerFunc, DoerFactory, ConfigMap, and an auth plug…
Browse files Browse the repository at this point in the history
…in registry
  • Loading branch information
James DeFelice committed Jul 12, 2016
1 parent af8d21d commit 7a3406d
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 129 deletions.
12 changes: 12 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
@@ -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()
}
2 changes: 1 addition & 1 deletion docs/docs/configuration-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
26 changes: 0 additions & 26 deletions httpcli/basic.go

This file was deleted.

28 changes: 28 additions & 0 deletions httpcli/basic/basic.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
60 changes: 53 additions & 7 deletions httpcli/config.go
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 29 additions & 1 deletion httpcli/doer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
76 changes: 0 additions & 76 deletions httpcli/iam.go

This file was deleted.

78 changes: 78 additions & 0 deletions httpcli/iam/iam.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func main() {

// initialize logging
logging.SetupLogs()
initAuth()

// initialize resolver
config := records.SetConfig(*cjson)
Expand Down
24 changes: 6 additions & 18 deletions records/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 7a3406d

Please sign in to comment.