From ba3c663c49c7556c250c6eefbd468bd89057850e Mon Sep 17 00:00:00 2001 From: JP Robinson Date: Mon, 5 Nov 2018 12:58:33 -0500 Subject: [PATCH] Adding "auth" and "auth/gcp" packages (#160) --- README.md | 15 ++ auth/gcp/iam.go | 344 ++++++++++++++++++++++++++++++++++++++ auth/gcp/iam_test.go | 186 +++++++++++++++++++++ auth/gcp/identity.go | 201 ++++++++++++++++++++++ auth/gcp/identity_test.go | 213 +++++++++++++++++++++++ auth/keys.go | 160 ++++++++++++++++++ auth/keys_test.go | 217 ++++++++++++++++++++++++ auth/verify.go | 131 +++++++++++++++ auth/verify_test.go | 332 ++++++++++++++++++++++++++++++++++++ doc.go | 221 +----------------------- go.mod | 38 ++++- go.sum | 68 +++++++- 12 files changed, 1899 insertions(+), 227 deletions(-) create mode 100644 auth/gcp/iam.go create mode 100644 auth/gcp/iam_test.go create mode 100644 auth/gcp/identity.go create mode 100644 auth/gcp/identity_test.go create mode 100644 auth/keys.go create mode 100644 auth/keys_test.go create mode 100644 auth/verify.go create mode 100644 auth/verify_test.go diff --git a/README.md b/README.md index fc094e68f..9e0170de5 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,21 @@ This is an experimental package in Gizmo! * Services using this package are meant for deploy to GCP with GKE and Cloud Endpoints. +#### [`auth`](https://godoc.org/github.com/NYTimes/gizmo/auth) + +The `auth` package provides primitives for verifying inbound authentication tokens: + +* The `PublicKeySource` interface is meant to provide `*rsa.PublicKeys` from JSON Web Key Sets. +* The `Verifier` struct composes key source implementations with custom decoders and verifier functions to streamline server side token verification. + +#### [`auth/gcp`](https://godoc.org/github.com/NYTimes/gizmo/auth/gcp) + +The `auth/gcp` package provides 2 Google Cloud Platform based `auth.PublicKeySource` and `oauth2.TokenSource` implementations: + +* The "Identity" key source and token source rely on GCP's [identity JWT mechanism for asserting instance identities](https://cloud.google.com/compute/docs/instances/verifying-instance-identity). This is the preferred method for asserting instance identity on GCP. +* The "IAM" key source and token source rely on GCP's IAM services for [signing](https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt) and [verifying JWTs](https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts.keys/get). This method can be used outside of GCP, if needed and can provide a bridge for users transitioning from the 1st generation App Engine (where Identity tokens are not available) runtime to the 2nd. + + #### [`config`](https://godoc.org/github.com/NYTimes/gizmo/config) The `config` package contains a handful of useful functions to load to configuration structs from JSON files, JSON blobs in Consul k/v, or environment variables. diff --git a/auth/gcp/iam.go b/auth/gcp/iam.go new file mode 100644 index 000000000..650056d86 --- /dev/null +++ b/auth/gcp/iam.go @@ -0,0 +1,344 @@ +package gcp + +import ( + "context" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "net/http" + "path" + "sync" + "time" + + "github.com/NYTimes/gizmo/auth" + "github.com/pkg/errors" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jws" + iam "google.golang.org/api/iam/v1" +) + +var ( + timeNow = func() time.Time { return time.Now() } + + // docs say up to 1 hour, this plays it safe? + // https://cloud.google.com/compute/docs/instances/verifying-instance-identity#verify_signature + defaultTokenTTL = time.Minute * 20 +) + +// IAMClaimSet contains just an email for service account identification. +type IAMClaimSet struct { + jws.ClaimSet + + // Email address of the default service account + Email string `json:"email"` +} + +// NewDefaultIAMVerifier will verify tokens that have the same default service account as +// the server running this verifier. +func NewDefaultIAMVerifier(ctx context.Context, cfg IAMConfig, clientFunc func(context.Context) *http.Client) (*auth.Verifier, error) { + ks, err := NewIAMPublicKeySource(ctx, cfg, clientFunc) + if err != nil { + return nil, err + } + + eml, err := GetDefaultEmail(ctx, IdentityConfig{Client: clientFunc(ctx)}) + if err != nil { + return nil, errors.Wrap(err, "unable to get default email") + } + + return auth.NewVerifier(ks, + IAMClaimsDecoderFunc, VerifyIAMEmails(ctx, []string{eml}, cfg.Audience)), nil +} + +// BaseClaims implements the auth.ClaimSetter interface. +func (s IAMClaimSet) BaseClaims() *jws.ClaimSet { + return &s.ClaimSet +} + +// IAMClaimsDecoderFunc is an auth.ClaimsDecoderFunc for GCP identity tokens. +func IAMClaimsDecoderFunc(_ context.Context, b []byte) (auth.ClaimSetter, error) { + var cs IAMClaimSet + err := json.Unmarshal(b, &cs) + return cs, err +} + +// IAMVerifyFunc auth.VerifyFunc wrapper around the IAMClaimSet. +func IAMVerifyFunc(vf func(ctx context.Context, cs IAMClaimSet) bool) auth.VerifyFunc { + return func(ctx context.Context, c interface{}) bool { + ics, ok := c.(IAMClaimSet) + if !ok { + return false + } + return vf(ctx, ics) + } +} + +// ValidIAMClaims ensures the token audience issuers matches expectations. +func ValidIAMClaims(cs IAMClaimSet, audience string) bool { + return cs.Aud != audience +} + +// VerifyIAMEmails is an auth.VerifyFunc that ensures IAMClaimSets are valid +// and have the expected email and audience in their payload. +func VerifyIAMEmails(ctx context.Context, emails []string, audience string) auth.VerifyFunc { + emls := map[string]bool{} + for _, e := range emails { + emls[e] = true + } + return IAMVerifyFunc(func(ctx context.Context, cs IAMClaimSet) bool { + if !ValidIAMClaims(cs, audience) { + return false + } + return emls[cs.Email] + }) +} + +type iamKeySource struct { + cf func(context.Context) *http.Client + cfg IAMConfig +} + +// NewIAMPublicKeySource returns a PublicKeySource that uses the Google IAM service +// for fetching public keys of a given service account. The function for returning an +// HTTP client is to allow 1st generation App Engine users to lean on urlfetch. +func NewIAMPublicKeySource(ctx context.Context, cfg IAMConfig, clientFunc func(context.Context) *http.Client) (auth.PublicKeySource, error) { + src := iamKeySource{cf: clientFunc, cfg: cfg} + + ks, err := src.Get(ctx) + if err != nil { + return nil, err + } + + return auth.NewReusePublicKeySource(ks, src), nil +} + +func (s iamKeySource) Get(ctx context.Context) (auth.PublicKeySet, error) { + var ks auth.PublicKeySet + + // for the sake of GAE standard users who have to use a different *http.Client on + // each request, we're going to init a new iam.Service on each fetch. + // since this is cached, it should hopefully not be a huge issue + svc, err := iam.New(s.cf(ctx)) + if err != nil { + return ks, errors.Wrap(err, "unable to init iam client") + } + + if s.cfg.IAMAddress != "" { + svc.BasePath = s.cfg.IAMAddress + } + + name := fmt.Sprintf("projects/%s/serviceAccounts/%s", + s.cfg.Project, s.cfg.ServiceAccountEmail) + resp, err := svc.Projects.ServiceAccounts.Keys.List(name).Context(ctx).Do() + if err != nil { + return ks, errors.Wrap(err, "unable to list service account keys") + } + + keys := map[string]*rsa.PublicKey{} + for _, keyData := range resp.Keys { + // we need to fetch each key's PublicKey data since List only returns metadata. + key, err := svc.Projects.ServiceAccounts.Keys.Get(keyData.Name). + PublicKeyType("TYPE_X509_PEM_FILE").Context(ctx).Do() + if err != nil { + return ks, errors.Wrap(err, "unable to get public key data") + } + + pemBytes, err := base64.StdEncoding.DecodeString(key.PublicKeyData) + if err != nil { + return ks, err + } + + block, _ := pem.Decode(pemBytes) + if block == nil { + return ks, errors.New("Unable to find pem block in key") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return ks, errors.Wrap(err, "unable to parse x509 certificate") + } + + pkey, ok := cert.PublicKey.(*rsa.PublicKey) + if !ok { + return ks, errors.Errorf("unexpected public key type: %T", cert.PublicKey) + } + + _, name := path.Split(key.Name) + keys[name] = pkey + } + + return auth.PublicKeySet{Keys: keys, Expiry: timeNow().Add(20 * time.Minute)}, nil +} + +// IAMConfig contains the information required for generating or verifying IAM JWTs. +type IAMConfig struct { + IAMAddress string `envconfig:"IAM_ADDR"` // optional, for testing + + Audience string `envconfig:"IAM_AUDIENCE"` + Project string `envconfig:"IAM_PROJECT"` + ServiceAccountEmail string `envconfig:"IAM_SERVICE_ACCOUNT_EMAIL"` +} + +// NewIAMTokenSource returns an oauth2.TokenSource that uses Google's IAM services +// to sign a JWT with the default service account and the given audience. +// Users should use the Identity token source if they can. This client is meant to be +// used as a bridge for users as they transition from the 1st generation App Engine +// runtime to the 2nd generation. +// This implementation can be used in the 2nd gen runtime as it can reuse an http.Client. +func NewIAMTokenSource(ctx context.Context, cfg IAMConfig) (oauth2.TokenSource, error) { + tknSrc, err := defaultTokenSource(ctx, iam.CloudPlatformScope) + if err != nil { + return nil, err + } + svc, err := iam.New(oauth2.NewClient(ctx, tknSrc)) + if err != nil { + return nil, err + } + + if cfg.IAMAddress != "" { + svc.BasePath = cfg.IAMAddress + } + + src := &iamTokenSource{ + cfg: cfg, + svc: svc, + } + + tkn, err := src.Token() + if err != nil { + return nil, errors.Wrap(err, "unable to create initial token") + } + + return oauth2.ReuseTokenSource(tkn, src), nil +} + +// NewContextIAMTokenSource returns an oauth2.TokenSource that uses Google's IAM services +// to sign a JWT with the default service account and the given audience. +// Users should use the Identity token source if they can. This client is meant to be +// used as a bridge for users as they transition from the 1st generation App Engine +// runtime to the 2nd generation. +// This implementation can be used in the 1st gen runtime as it allows users to pass a +// context.Context while fetching the token. The context allows the implementation to +// reuse clients while changing out the HTTP client under the hood. +func NewContextIAMTokenSource(ctx context.Context, cfg IAMConfig) (ContextTokenSource, error) { + src := &iamTokenSource{cfg: cfg} + + tkn, err := src.ContextToken(ctx) + if err != nil { + return nil, errors.Wrap(err, "unable to create initial token") + } + + return &reuseTokenSource{t: tkn, new: src}, nil +} + +// ContextTokenSource is an oauth2.TokenSource that is capable of running on the 1st +// generation App Engine environment because it can create a urlfetch.Client from the +// given context. +type ContextTokenSource interface { + ContextToken(context.Context) (*oauth2.Token, error) +} + +type iamTokenSource struct { + cfg IAMConfig + + svc *iam.Service +} + +var defaultTokenSource = google.DefaultTokenSource + +func (s iamTokenSource) ContextToken(ctx context.Context) (*oauth2.Token, error) { + tknSrc, err := defaultTokenSource(ctx, iam.CloudPlatformScope) + if err != nil { + return nil, err + } + svc, err := iam.New(oauth2.NewClient(ctx, tknSrc)) + if err != nil { + return nil, err + } + + if s.cfg.IAMAddress != "" { + svc.BasePath = s.cfg.IAMAddress + } + + tkn, exp, err := s.newIAMToken(ctx, svc) + if err != nil { + return nil, err + } + + return &oauth2.Token{ + AccessToken: tkn, + TokenType: "Bearer", + Expiry: exp, + }, nil +} + +func (s iamTokenSource) Token() (*oauth2.Token, error) { + tkn, exp, err := s.newIAMToken(context.Background(), s.svc) + if err != nil { + return nil, err + } + + return &oauth2.Token{ + AccessToken: tkn, + TokenType: "Bearer", + Expiry: exp, + }, nil +} + +func (s iamTokenSource) newIAMToken(ctx context.Context, svc *iam.Service) (string, time.Time, error) { + iss := timeNow() + exp := iss.Add(defaultTokenTTL) + payload, err := json.Marshal(IAMClaimSet{ + ClaimSet: jws.ClaimSet{ + Aud: s.cfg.Audience, + Exp: exp.Unix(), + Iat: iss.Unix(), + }, + Email: s.cfg.ServiceAccountEmail, + }) + if err != nil { + return "", exp, errors.Wrap(err, "unable to encode JWT payload") + } + + resp, err := svc.Projects.ServiceAccounts.SignJwt( + fmt.Sprintf("projects/%s/serviceAccounts/%s", + s.cfg.Project, s.cfg.ServiceAccountEmail), + &iam.SignJwtRequest{Payload: string(payload)}).Context(ctx).Do() + if err != nil { + return "", exp, errors.Wrap(err, "unable to sign JWT") + } + return resp.SignedJwt, exp, nil +} + +// TAKEN FROM golang.org/x/oauth2 so we can add context bc GAE 1st gen + urlfetch. +// reuseCtxTokenSource is a TokenSource that holds a single token in memory +// and validates its expiry before each call to retrieve it with +// Token. If it's expired, it will be auto-refreshed using the +// new TokenSource. +type reuseTokenSource struct { + new ContextTokenSource // called when t is expired. + + mu sync.Mutex // guards t + t *oauth2.Token +} + +// Token returns the current token if it's still valid, else will +// refresh the current token (using r.Context for HTTP client +// information) and return the new one. +func (s *reuseTokenSource) ContextToken(ctx context.Context) (*oauth2.Token, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.t.Valid() { + return s.t, nil + } + t, err := s.new.ContextToken(ctx) + if err != nil { + return nil, err + } + s.t = t + return t, nil +} diff --git a/auth/gcp/iam_test.go b/auth/gcp/iam_test.go new file mode 100644 index 000000000..793199c64 --- /dev/null +++ b/auth/gcp/iam_test.go @@ -0,0 +1,186 @@ +package gcp + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + iam "google.golang.org/api/iam/v1" +) + +func TestIAMKeySource(t *testing.T) { + + tests := []struct { + name string + givenIAMErr bool + + wantErr bool + }{ + { + name: "normal success", + }, + { + name: "iam error", + givenIAMErr: true, + + wantErr: true, + }, + } + + for _, test := range tests { + const tokenValue = "iam-signed-jwt" + t.Run(test.name, func(t *testing.T) { + iamSvr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Log(r.URL.Path) + if strings.Contains(r.URL.Path, "keys") { + json.NewEncoder(w).Encode(iam.ListServiceAccountKeysResponse{ + Keys: []*iam.ServiceAccountKey{ + {Name: "8289d54280b76712de41cd2ef95972b123be9ac0"}, + }, + }) + } else { + json.NewEncoder(w).Encode(iam.ServiceAccountKey{ + PublicKeyData: pubKey, + }) + } + })) + if test.givenIAMErr { + iamSvr.Close() + } else { + defer iamSvr.Close() + } + + defaultTokenSource = func(ctx context.Context, scopes ...string) (oauth2.TokenSource, error) { + return nil, nil + } + defer func() { + defaultTokenSource = google.DefaultTokenSource + }() + + cfg := IAMConfig{ + IAMAddress: iamSvr.URL, + } + + ctx := context.Background() + hc := func(_ context.Context) *http.Client { + return http.DefaultClient + } + src, err := NewIAMPublicKeySource(ctx, cfg, hc) + if (err != nil) != test.wantErr { + t.Errorf("expected error? %t but got %s", test.wantErr, err) + } + + if src == nil { + return + } + + got, err := src.Get(ctx) + if (err != nil) != test.wantErr { + t.Errorf("expected error? %t but got %s", test.wantErr, err) + } + + if len(got.Keys) == 0 { + t.Errorf("expected keys to be generated but got none") + } + }) + } +} + +func TestIAMTokenSource(t *testing.T) { + tests := []struct { + name string + givenIAMErr bool + + wantErr bool + }{ + { + name: "normal success", + }, + { + name: "iam error", + givenIAMErr: true, + + wantErr: true, + }, + } + + for _, test := range tests { + const tokenValue = "iam-signed-jwt" + t.Run(test.name, func(t *testing.T) { + iamSvr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(iam.SignJwtResponse{ + SignedJwt: tokenValue, + }) + })) + if test.givenIAMErr { + iamSvr.Close() + } else { + defer iamSvr.Close() + } + + defaultTokenSource = func(ctx context.Context, scopes ...string) (oauth2.TokenSource, error) { + return nil, nil + } + defer func() { + defaultTokenSource = google.DefaultTokenSource + }() + + cfg := IAMConfig{ + IAMAddress: iamSvr.URL, + } + + ctx := context.Background() + src, err := NewIAMTokenSource(ctx, cfg) + if (err != nil) != test.wantErr { + t.Errorf("expected error? %t but got %s", test.wantErr, err) + } + + if src == nil { + return + } + + got, err := src.Token() + if (err != nil) != test.wantErr { + t.Errorf("expected error? %t but got %s", test.wantErr, err) + } + + if got.AccessToken != tokenValue { + t.Errorf("expected access token value of %s, got %s", + tokenValue, got.AccessToken) + } + + csrc, err := NewContextIAMTokenSource(ctx, cfg) + if (err != nil) != test.wantErr { + t.Errorf("expected error? %t but got %s", test.wantErr, err) + } + + if csrc == nil { + return + } + + got, err = csrc.ContextToken(ctx) + if (err != nil) != test.wantErr { + t.Errorf("expected error? %t but got %s", test.wantErr, err) + } + + if got.AccessToken != tokenValue { + t.Errorf("expected access token value of %s, got %s", + tokenValue, got.AccessToken) + } + }) + } + +} + +type testTokenSource struct{} + +func (t testTokenSource) Token() (*oauth2.Token, error) { + return &oauth2.Token{}, nil +} + +const pubKey = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJakNDQWdxZ0F3SUJBZ0lJU3VCcXowQ0FMbk13RFFZSktvWklodmNOQVFFRkJRQXdOREV5TURBR0ExVUUKQXhNcGJubDBMV2RoYldWekxXUmxkaTVoY0hCemNHOTBMbWR6WlhKMmFXTmxZV05qYjNWdWRDNWpiMjB3SGhjTgpNVGd4TURJeU1EVXdNVEl3V2hjTk1UZ3hNVEEzTVRjeE5qSXdXakEwTVRJd01BWURWUVFERXlsdWVYUXRaMkZ0ClpYTXRaR1YyTG1Gd2NITndiM1F1WjNObGNuWnBZMlZoWTJOdmRXNTBMbU52YlRDQ0FTSXdEUVlKS29aSWh2Y04KQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUtQRmRZWkZUaUR1RmhWMDdKTHZuVjFhamZOL1hUdlZUbC9tUDNVbgpQVjdkakNVSTFRWFh2K2NhVmp4djVzOHc1WG9TWVpSOXpVYytlNXFpRHdndm5yQjMyeWkwajBhTEJXaHZiTmN3Ci9ZdFRBd1l3ZVhoVzM0ZzlsOFl5TkFHb0xJTDVVSy9ubXp3NVVDYm15V2pCZkdmYmdwNm5SRUI0dWhQWGM0MnoKZ1Y0cUJtS2pUclhvNk85OVVhbC9COU1rMFIzWWExMnBPclAyN0drZkxNZmFGSWlJSlhROW94aVdLMUw0U0pDdwpudDc0OEpoVFJHREtHWGtGZkxEMEFVTjAra2JsOU5hbXpVaVcvTmhXczJkd1FXOGN2YUxuWG11NENraEM1aVo1CmU4RHJvSEswMDFCd21HNmhrU1ZxN2laNnFZU2J6ekwrc3NpUTB5KzVwZDBkcXZrQ0F3RUFBYU00TURZd0RBWUQKVlIwVEFRSC9CQUl3QURBT0JnTlZIUThCQWY4RUJBTUNCNEF3RmdZRFZSMGxBUUgvQkF3d0NnWUlLd1lCQlFVSApBd0l3RFFZSktvWklodmNOQVFFRkJRQURnZ0VCQUk2eHExTzloRm4wN1lDbzhJV3E1Mk1PZVFnenN5YXBKbXVHCmhaUWF3Q1l1SmMwSFo3RXJRYkxSejdYRDBNbzRwQlFHSGl2SGtuMW1GcUxna2c5eHdISlhHVnQzbC9RWlczeWQKMDhXa0RvMlhjbEkwaE1pT3gxSHBtREVsT1FheE5Gd2NlV1VlN09hck5Da0dHVGsySEZsK3QzQkxWcDVYWnFEaQpiQlZpY0tSTTREczF6dURtQzhtUWppbjVxR0VYM1IrZ2hacGhDcnRjdC9yTWF6eW5iUHdDbnFzRDZVMFNRMXZ6CjF2RnBPRDd4cnlVZ0VuZTh4SnlEVy9aeHkzTXBIeThMTWtiSjZjVEZjaGVzOFlvaTVFYkVyYTk2NEU4SVg4dlcKdHgwU2VUdmhoT0ltQ2VsdE9UTkZmMmkxSitIY2Y0V3JaNzN4cFQ4YnV3SjZyeEkwdDRnPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" diff --git a/auth/gcp/identity.go b/auth/gcp/identity.go new file mode 100644 index 000000000..b9e8d6e51 --- /dev/null +++ b/auth/gcp/identity.go @@ -0,0 +1,201 @@ +// +build !appengine + +package gcp + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/NYTimes/gizmo/auth" + "github.com/pkg/errors" + "golang.org/x/oauth2" + "golang.org/x/oauth2/jws" +) + +// IdentityConfig contains the information required for generating or verifying identity +// JWTs. +type IdentityConfig struct { + Audience string `envconfig:"ID_AUDIENCE"` + + CertURL string `envconfig:"ID_CERT_URL"` // optional override for public key source + + Client *http.Client // optional override + + MetadataAddress string `envconfig:"ID_METADATA_ADDR"` // optional override for token and email retrieval +} + +type idKeySource struct { + cfg IdentityConfig +} + +// NewIdentityPublicKeySource fetches Google's public oauth2 certificates to be used with +// the auth.Verifier tool. +func NewIdentityPublicKeySource(ctx context.Context, cfg IdentityConfig) (auth.PublicKeySource, error) { + if cfg.Client == nil { + cfg.Client = &http.Client{ + Timeout: 5 * time.Second, + } + } + if cfg.CertURL == "" { + cfg.CertURL = "https://www.googleapis.com/oauth2/v3/certs" + } + + src := idKeySource{cfg: cfg} + + ks, err := src.Get(ctx) + if err != nil { + return nil, err + } + + return auth.NewReusePublicKeySource(ks, src), nil +} + +func (s idKeySource) Get(ctx context.Context) (auth.PublicKeySet, error) { + return auth.NewPublicKeySetFromURL(s.cfg.Client, s.cfg.CertURL, time.Hour*2) +} + +// NewIdentityTokenSource will use the GCP metadata services to generate GCP Identity +// tokens. More information on asserting GCP identities can be found here: +// https://cloud.google.com/compute/docs/instances/verifying-instance-identity +func NewIdentityTokenSource(cfg IdentityConfig) (oauth2.TokenSource, error) { + if cfg.Client == nil { + cfg.Client = &http.Client{ + Timeout: 5 * time.Second, + } + } + if cfg.MetadataAddress == "" { + cfg.MetadataAddress = "http://metadata/computeMetadata/v1/" + } + + ts := &idTokenSource{cfg: cfg} + + tok, err := ts.Token() + if err != nil { + return nil, err + } + return oauth2.ReuseTokenSource(tok, ts), nil +} + +type idTokenSource struct { + cfg IdentityConfig +} + +func (c *idTokenSource) Token() (*oauth2.Token, error) { + suffix := fmt.Sprintf("instance/service-accounts/default/identity?audience=%s&format=full", + c.cfg.Audience) + + tkn, err := metadataGet(context.Background(), c.cfg, suffix) + if err != nil { + return nil, errors.Wrap(err, "unable to get token") + } + + return &oauth2.Token{ + AccessToken: tkn, + TokenType: "Bearer", + Expiry: timeNow().Add(defaultTokenTTL), + }, nil +} + +// IdentityClaimSet holds all the expected values for the various versions of the GCP +// identity token. +// More details: +// https://cloud.google.com/compute/docs/instances/verifying-instance-identity#payload +// https://developers.google.com/identity/sign-in/web/backend-auth#calling-the-tokeninfo-endpoint +type IdentityClaimSet struct { + jws.ClaimSet + + // Email address of the default service account (only exists on GAE 2nd gen?) + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + + // Google metadata info (appears to only exist on GCE?) + Google map[string]interface{} `json:"google"` +} + +// BaseClaims implements the auth.ClaimSetter interface. +func (s IdentityClaimSet) BaseClaims() *jws.ClaimSet { + return &s.ClaimSet +} + +// IdentityClaimsDecoderFunc is an auth.ClaimsDecoderFunc for GCP identity tokens. +func IdentityClaimsDecoderFunc(_ context.Context, b []byte) (auth.ClaimSetter, error) { + var cs IdentityClaimSet + err := json.Unmarshal(b, &cs) + return cs, err +} + +// IdentityVerifyFunc auth.VerifyFunc wrapper around the IdentityClaimSet. +func IdentityVerifyFunc(vf func(ctx context.Context, cs IdentityClaimSet) bool) auth.VerifyFunc { + return func(ctx context.Context, c interface{}) bool { + ics, ok := c.(IdentityClaimSet) + if !ok { + return false + } + return vf(ctx, ics) + } +} + +// Issuers contains the known Google account issuers for identity tokens. +var Issuers = map[string]bool{ + "accounts.google.com": true, + "https://accounts.google.com": true, +} + +// ValidIdentityClaims ensures the token audience and issuers match expectations. +func ValidIdentityClaims(cs IdentityClaimSet, audience string) bool { + if cs.Aud != audience { + return false + } + return Issuers[cs.Iss] +} + +// VerifyIdentityEmails is an auth.VerifyFunc that ensures IdentityClaimSets are valid +// and have the expected email and audience in their payload. +func VerifyIdentityEmails(ctx context.Context, emails []string, audience string) auth.VerifyFunc { + emls := map[string]bool{} + for _, e := range emails { + emls[e] = true + } + return IdentityVerifyFunc(func(ctx context.Context, cs IdentityClaimSet) bool { + if !ValidIdentityClaims(cs, audience) { + return false + } + if !cs.EmailVerified { + return false + } + return emls[cs.Email] + }) +} + +// GetDefaultEmail is a helper method for users on GCE or the 2nd generation GAE +// environment. +func GetDefaultEmail(ctx context.Context, cfg IdentityConfig) (string, error) { + email, err := metadataGet(ctx, cfg, "instance/service-accounts/default/email") + return email, errors.Wrap(err, "unable to get default email from metadata") +} + +func metadataGet(ctx context.Context, cfg IdentityConfig, suffix string) (string, error) { + req, err := http.NewRequest(http.MethodGet, cfg.MetadataAddress+suffix, nil) + if err != nil { + return "", errors.Wrap(err, "unable to create metadata request") + } + req.Header.Set("Metadata-Flavor", "Google") + + resp, err := cfg.Client.Do(req) + if err != nil { + return "", errors.Wrap(err, "unable to send request to metadata") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", errors.Errorf("metadata service returned a non-200 response: %d", + resp.StatusCode) + } + + tkn, err := ioutil.ReadAll(resp.Body) + return string(tkn), errors.Wrap(err, "unable to read metadata response") +} diff --git a/auth/gcp/identity_test.go b/auth/gcp/identity_test.go new file mode 100644 index 000000000..70f0b9bb7 --- /dev/null +++ b/auth/gcp/identity_test.go @@ -0,0 +1,213 @@ +package gcp + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/oauth2" + "golang.org/x/oauth2/jws" +) + +func TestVerifyIdentity(t *testing.T) { + tests := []struct { + name string + + givenEmails []string + givenAudience string + + givenClaims interface{} + + wantVerified bool + }{ + { + name: "normal success", + + givenEmails: []string{"jp@example.com"}, + givenAudience: "example.com", + + givenClaims: IdentityClaimSet{ + ClaimSet: jws.ClaimSet{ + Aud: "example.com", + Iss: "https://accounts.google.com", + }, + Email: "jp@example.com", + EmailVerified: true, + }, + + wantVerified: true, + }, + { + name: "invalid issuer", + + givenEmails: []string{"jp@example.com"}, + givenAudience: "example.com", + + givenClaims: IdentityClaimSet{ + ClaimSet: jws.ClaimSet{ + Aud: "example.com", + Iss: "https://google.com", + }, + Email: "jp@example.com", + }, + + wantVerified: false, + }, + { + name: "unverified email", + + givenEmails: []string{"jp@example.com"}, + givenAudience: "example.com", + + givenClaims: IdentityClaimSet{ + ClaimSet: jws.ClaimSet{ + Aud: "example.com", + Iss: "https://accounts.google.com", + }, + Email: "jp@example.com", + }, + + wantVerified: false, + }, + { + name: "invalid claims type", + + givenEmails: []string{"jp@example.com"}, + givenAudience: "example.com", + + givenClaims: jws.ClaimSet{ + Aud: "google.com", + Iss: "https://accounts.google.com", + }, + + wantVerified: false, + }, + { + name: "invalid audience", + + givenEmails: []string{"jp@example.com"}, + givenAudience: "example.com", + + givenClaims: IdentityClaimSet{ + ClaimSet: jws.ClaimSet{ + Aud: "google.com", + Iss: "https://accounts.google.com", + }, + Email: "jp@example.com", + EmailVerified: true, + }, + + wantVerified: false, + }, + { + name: "bad email", + + givenEmails: []string{"jape@example.com"}, + givenAudience: "example.com", + + givenClaims: IdentityClaimSet{ + ClaimSet: jws.ClaimSet{ + Aud: "example.com", + Iss: "https://accounts.google.com", + }, + Email: "jp@example.com", + EmailVerified: true, + }, + + wantVerified: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + vf := VerifyIdentityEmails(context.Background(), test.givenEmails, test.givenAudience) + + got := vf(context.Background(), test.givenClaims) + if got != test.wantVerified { + t.Errorf("expected verfied? %t, got %t", test.wantVerified, got) + } + }) + } + +} + +func TestIdentityTokenSource(t *testing.T) { + tests := []struct { + name string + + givenMetaStatus int + givenMetaErr bool + + wantErr bool + }{ + { + name: "success", + givenMetaStatus: http.StatusOK, + }, + { + name: "bad status from metadata", + givenMetaStatus: http.StatusNotFound, + + wantErr: true, + }, + { + name: "bad connection to metadata", + givenMetaErr: true, + + wantErr: true, + }, + } + + for _, test := range tests { + + testTokenString := "blargityblarg" + testToken := oauth2.Token{ + AccessToken: testTokenString, + TokenType: "Bearer", + Expiry: timeNow().Add(20 * time.Minute), + } + t.Run(test.name, func(t *testing.T) { + metaSvr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.URL.String(), "audience=my-aud&format") { + t.Errorf("expected 'my-aud' audience in metadata call but did not see it: %s", + r.URL.String()) + } + w.WriteHeader(test.givenMetaStatus) + w.Write([]byte(testTokenString)) + })) + defer metaSvr.Close() + if test.givenMetaErr { + metaSvr.Close() + } + + cfg := IdentityConfig{ + MetadataAddress: metaSvr.URL + "/", + Audience: "my-aud", + } + + src, err := NewIdentityTokenSource(cfg) + + if (err != nil) != test.wantErr { + t.Errorf("expected error? %t, but got %s", test.wantErr, err) + } + + if src == nil { + return + } + + got, err := src.Token() + if (err != nil) != test.wantErr { + t.Errorf("expected error? %t, but got %s", test.wantErr, err) + return + } + + if cmp.Equal(got, testToken) { + t.Errorf("unpexpected token returned: %s", cmp.Diff(got, testToken)) + } + }) + } +} diff --git a/auth/keys.go b/auth/keys.go new file mode 100644 index 000000000..95057d8c1 --- /dev/null +++ b/auth/keys.go @@ -0,0 +1,160 @@ +package auth + +import ( + "context" + "crypto/rsa" + "encoding/base64" + "encoding/json" + "io/ioutil" + "math/big" + "net/http" + "regexp" + "strconv" + "sync" + "time" + + "github.com/pkg/errors" +) + +// PublicKeySource is to be used by servers who need to acquire public key sets for +// verifying inbound request's JWTs. +type PublicKeySource interface { + Get(context.Context) (PublicKeySet, error) +} + +// NewReusePublicKeySource is a wrapper around PublicKeySources to only fetch a new key +// set once the current key cache has expired. +func NewReusePublicKeySource(ks PublicKeySet, src PublicKeySource) PublicKeySource { + return &reuseKeySource{ks: ks, src: src} +} + +type reuseKeySource struct { + src PublicKeySource + + mu sync.Mutex + ks PublicKeySet +} + +func (r *reuseKeySource) Get(ctx context.Context) (PublicKeySet, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.ks.Expired() { + var err error + r.ks, err = r.src.Get(ctx) + return r.ks, err + } + return r.ks, nil +} + +// PublicKeySet contains a set of keys acquired from a JWKS that has an expiration. +type PublicKeySet struct { + Expiry time.Time + Keys map[string]*rsa.PublicKey +} + +// Expired will return true if the current key set is expire according to its Expiry +// field. +func (ks PublicKeySet) Expired() bool { + return timeNow().After(ks.Expiry) +} + +// GetKey will look for the given key ID in the key set and return it, if it exists. +func (ks PublicKeySet) GetKey(id string) (*rsa.PublicKey, error) { + key, ok := ks.Keys[id] + if !ok { + return nil, errors.Errorf("key [%s] not found in set of size %d", id, len(ks.Keys)) + } + return key, nil +} + +// JSONKey represents a public or private key in JWK format. +type JSONKey struct { + Kty string `json:"kty"` + Alg string `json:"alg"` + Use string `json:"use"` + Kid string `json:"kid"` + N string `json:"n"` + E string `json:"e"` +} + +// JSONKeyResponse represents a JWK Set object. +type JSONKeyResponse struct { + Keys []*JSONKey `json:"keys"` +} + +var reMaxAge = regexp.MustCompile("max-age=([0-9]*)") + +// NewPublicKeySetFromURL will attempt to fetch a JWKS from the given URL and parse it +// into a PublicKeySet. The endpoint the URL points to must return the same format as the +// JSONKeyResponse struct. +func NewPublicKeySetFromURL(hc *http.Client, url string, defaultTTL time.Duration) (PublicKeySet, error) { + var ks PublicKeySet + r, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return ks, errors.Wrap(err, "unable to create request") + } + + resp, err := hc.Do(r) + if err != nil { + return ks, err + } + defer resp.Body.Close() + + ttl := defaultTTL + if ccHeader := resp.Header.Get("cache-control"); ccHeader != "" { + if match := reMaxAge.FindStringSubmatch(ccHeader); len(match) > 1 { + maxAgeSeconds, err := strconv.ParseInt(match[1], 10, 64) + if err != nil { + return ks, errors.Wrap(err, "unable to parse cache-control max age") + } + ttl = time.Second * time.Duration(maxAgeSeconds) + } + } + + payload, err := ioutil.ReadAll(resp.Body) + if err != nil { + return ks, errors.Wrap(err, "unable to read response") + } + + return NewPublicKeySetFromJSON(payload, ttl) +} + +// NewPublicKeySetFromJSON will accept a JSON payload in the format of the +// JSONKeyResponse and parse it into a PublicKeySet. +func NewPublicKeySetFromJSON(payload []byte, ttl time.Duration) (PublicKeySet, error) { + var ( + ks PublicKeySet + keys JSONKeyResponse + ) + err := json.Unmarshal(payload, &keys) + if err != nil { + return ks, err + } + + ks = PublicKeySet{ + Expiry: timeNow().Add(ttl), + Keys: map[string]*rsa.PublicKey{}, + } + + for _, key := range keys.Keys { + // we only plan on using RSA + if key.Use == "sig" && key.Kty == "RSA" { + n, err := base64.RawURLEncoding.DecodeString(key.N) + if err != nil { + return ks, err + } + e, err := base64.RawURLEncoding.DecodeString(key.E) + if err != nil { + return ks, err + } + ei := big.NewInt(0).SetBytes(e).Int64() + ks.Keys[key.Kid] = &rsa.PublicKey{ + N: big.NewInt(0).SetBytes(n), + E: int(ei), + } + } + } + return ks, nil +} + +var timeNow = func() time.Time { return time.Now() } diff --git a/auth/keys_test.go b/auth/keys_test.go new file mode 100644 index 000000000..5942dfe40 --- /dev/null +++ b/auth/keys_test.go @@ -0,0 +1,217 @@ +package auth + +import ( + "context" + "crypto/rsa" + "math/big" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestResuseKeySource(t *testing.T) { + testTime := time.Date(2018, 10, 29, 12, 0, 0, 0, time.UTC) + + timeNow = func() time.Time { return testTime } + + firstKeys, err := NewPublicKeySetFromJSON([]byte(testGoogleCerts), 1*time.Second) + if err != nil { + t.Errorf("unexpected error creating key set: %s", err) + return + } + + nextKeys := PublicKeySet{ + Expiry: testTime.Add(2 * time.Second), + Keys: map[string]*rsa.PublicKey{ + "8289d54280b76712de41cd2ef95972b123be9ac0": &rsa.PublicKey{N: testGoogle2, E: 65537}, + }, + } + + reuser := NewReusePublicKeySource(firstKeys, testKeySource{keys: nextKeys}) + + // first get, firstKeys are not expired and should be returned + + gotKeys, err := reuser.Get(context.Background()) + if err != nil { + t.Errorf("unexpected error getting keys: %s", err) + return + } + if !cmp.Equal(gotKeys, firstKeys, cmpopts.IgnoreUnexported(big.Int{})) { + t.Errorf("first keys did not match expectations: %s", cmp.Diff(gotKeys, firstKeys, + cmpopts.IgnoreUnexported(big.Int{}))) + return + } + + // move time forward, expire the first keys + timeNow = func() time.Time { return testTime.Add(1500 * time.Millisecond) } + + gotKeys, err = reuser.Get(context.Background()) + if err != nil { + t.Errorf("unexpected error getting keys: %s", err) + return + } + if !cmp.Equal(gotKeys, nextKeys, cmpopts.IgnoreUnexported(big.Int{})) { + t.Errorf("next keys did not match expectations: %s", cmp.Diff(gotKeys, nextKeys, + cmpopts.IgnoreUnexported(big.Int{}))) + return + } + + // verify get works + k, err := gotKeys.GetKey("8289d54280b76712de41cd2ef95972b123be9ac0") + if err != nil { + t.Errorf("unexpected error getting key: %s", err) + } + if !cmp.Equal(k, nextKeys.Keys["8289d54280b76712de41cd2ef95972b123be9ac0"], + cmpopts.IgnoreUnexported(big.Int{})) { + t.Errorf("next keys did not match expectations: %s", + cmp.Diff(k, nextKeys.Keys["8289d54280b76712de41cd2ef95972b123be9ac0"], + cmpopts.IgnoreUnexported(big.Int{}))) + return + } +} + +type testKeySource struct { + keys PublicKeySet + err error +} + +func (t testKeySource) Get(ctx context.Context) (PublicKeySet, error) { + return t.keys, t.err +} + +func TestKeySetFromURL(t *testing.T) { + testTime := time.Date(2018, 10, 29, 12, 0, 0, 0, time.UTC) + tests := []struct { + name string + + givenCacheHeader string + givenPayload string + givenBadServer bool + + wantError bool + wantKeys PublicKeySet + }{ + { + name: "normal google certs w/ cache", + + givenCacheHeader: "max-age=1", + givenPayload: testGoogleCerts, + + wantKeys: PublicKeySet{ + Expiry: testTime.Add(1 * time.Second), + Keys: map[string]*rsa.PublicKey{ + "728f4016652079b9ed99861bb09bafc5a45baa86": &rsa.PublicKey{N: testGoogle1, E: 65537}, + "8289d54280b76712de41cd2ef95972b123be9ac0": &rsa.PublicKey{N: testGoogle2, E: 65537}, + }, + }, + }, + { + name: "normal google certs w/o cache", + + givenPayload: testGoogleCerts, + + wantKeys: PublicKeySet{ + Expiry: testTime.Add(5 * time.Second), + Keys: map[string]*rsa.PublicKey{ + "728f4016652079b9ed99861bb09bafc5a45baa86": &rsa.PublicKey{N: testGoogle1, E: 65537}, + "8289d54280b76712de41cd2ef95972b123be9ac0": &rsa.PublicKey{N: testGoogle2, E: 65537}, + }, + }, + }, + { + name: "bad response, want error", + givenPayload: "some kind of angry robot", + + wantError: true, + }, + { + name: "bad server, want error", + + givenPayload: testGoogleCerts, + givenBadServer: true, + + wantError: true, + }, + { + name: "bad cache header, want success", + + givenCacheHeader: "BLAARB! ANGRY COMPUTER!", + givenPayload: testGoogleCerts, + + wantKeys: PublicKeySet{ + Expiry: testTime.Add(5 * time.Second), + Keys: map[string]*rsa.PublicKey{ + "728f4016652079b9ed99861bb09bafc5a45baa86": &rsa.PublicKey{N: testGoogle1, E: 65537}, + "8289d54280b76712de41cd2ef95972b123be9ac0": &rsa.PublicKey{N: testGoogle2, E: 65537}, + }, + }, + }, + } + + for _, test := range tests { + timeNow = func() time.Time { return testTime } + + t.Run(test.name, func(t *testing.T) { + srvr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if test.givenCacheHeader != "" { + w.Header().Add("cache-control", test.givenCacheHeader) + } + w.Write([]byte(test.givenPayload)) + })) + defer srvr.Close() + if test.givenBadServer { + srvr.Close() + } + + got, gotErr := NewPublicKeySetFromURL(http.DefaultClient, srvr.URL, 5*time.Second) + if (gotErr != nil) != test.wantError { + t.Errorf("expected error? %t and got %s", test.wantError, gotErr) + return + } + + if !cmp.Equal(got, test.wantKeys, cmpopts.IgnoreUnexported(big.Int{})) { + t.Errorf("keys did not match expectations: %s", cmp.Diff(got, test.wantKeys, cmpopts.IgnoreUnexported(big.Int{}))) + } + }) + } +} + +var ( + testGoogleKey1 = "18112684417237113466774220553948287658642275536612278117358654328223325254239855900914880208928002193971790755769647369251185307648390933383803629253833792935549104394492595490970480288704258432536877269087694080352968836583401030682357884420432445619092471675752640354212779048186101852385524325549753366939320751885000360016238872619721767196169731422128756698973826778639560486979276112061913581353475855995717107174242233057925781337843224898645582603363390951368105740797845693907662079988391116580563176804122832211438500322243675724500523751141979116987975024595515232643410130766424608026731615327022863391377" + testGoogleKey2 = "22433090823316839640339489484457787676134304275873755218133861343920545237994470293495919014803004482856016084553850209153845425382613518932089311310596313310600424737736088033780907099977873221447195709312051528384355479077579673777886481089832045696620374920724411025483234264634539593436130076854768802102666090698524255278976644754677212286402099970599598264338136458077064875129043902522602870213617296706363155049264877048351659848686562003749244021217935734825983116131356048732262346697829165992404416525006735905763408678841171079087251498194471555953631250995421460080193870950448459655537601409828979901677" + testGoogle1 = func() *big.Int { + b := big.NewInt(0) + b.SetBytes([]byte(testGoogleKey1)) + return b + }() + testGoogle2 = func() *big.Int { + b := big.NewInt(0) + b.SetBytes([]byte(testGoogleKey2)) + return b + }() +) + +const testGoogleCerts = `{ + "keys": [ + { + "alg": "RS256", + "n": "j3rnumgAbXUMwqwL31lNVYuvWGbriT7uy7CgTeqwfNLf6Q9TMQlDidrFFSLgSe2BifjEswS6B4qpsXMlrMoSozIwbHuSkoQZdY2m5vFEZRkyHB4mAKZuzUi5XH5LVllbv2TBp6KsjJWSSW5Bnyen9pIeamHdODoX_PEdBhmqknDURRuuq3Bb3IVnudGP8JCbHTZ86ZS2aS2hpYK3eA8dvp155K1jKMAG9WH89jhkeFR67Oq9mD-yGZDaNCN8nOZR5Iyw-WQJo5-ijEAckHXn1SdYGjQgm_2fvEBsf0gJEjmx2DrNeacZLyDUA_dB9JIy5ZrfFZ0H9l0IkSgggUSakQ", + "use": "sig", + "kid": "8289d54280b76712de41cd2ef95972b123be9ac0", + "e": "AQAB", + "kty": "RSA" + }, + { + "use": "sig", + "kid": "728f4016652079b9ed99861bb09bafc5a45baa86", + "e": "AQAB", + "kty": "RSA", + "alg": "RS256", + "n": "sbRNoYaZX3w2Iosb6uzfykt-uJh_NRVQ0h_98Gptkpq3r-xgdaq9i-mmZEYZtrNUmIqOEDvtIJ36-CVnDZI2p_eARFkmedHC14QX5SHdFb2qr0a5DuqC5qLoyOMXSNJyfRHK8ULjozLxO7t_P0EsdlLPOUQjcbpTiIo9p-L9iskMCKpQdDfQ4CrzHKQjfYN3KJdehsChguffue-VBUkoDaRRUA50h6DiFe-loC_dzycoNGYJEJvAM5DC3zuHr6dfc5saHLUi4upgR2_jchA6kwSOVBC05qUgY4E3UdYTWciTqkSowiAErDx21g-oB6QzIr8MRMzKa89-g2Ine-qE7Q" + } + ] +}` diff --git a/auth/verify.go b/auth/verify.go new file mode 100644 index 000000000..f9536c848 --- /dev/null +++ b/auth/verify.go @@ -0,0 +1,131 @@ +package auth + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "strings" + "time" + + httptransport "github.com/go-kit/kit/transport/http" + "golang.org/x/oauth2/jws" +) + +// Verifier is a generic tool for verifying JWT tokens. +type Verifier struct { + ks PublicKeySource + df ClaimsDecoderFunc + vf VerifyFunc + + skewAllowance int64 +} + +var defaultSkewAllowance = time.Minute * 5 + +// ClaimSetter is an interface for all incoming claims to implement. This ensures the +// basic format used by the `jws` package. +type ClaimSetter interface { + BaseClaims() *jws.ClaimSet +} + +// ClaimsDecoderFunc will expect to convert a JSON payload into the appropriate claims +// type. +type ClaimsDecoderFunc func(context.Context, []byte) (ClaimSetter, error) + +// VerifyFunc will be called by the Verify if all other checks on the token pass. +// Developers should use this to encapsulate any business logic involved with token +// verification. +type VerifyFunc func(context.Context, interface{}) bool + +// NewVerifier returns a genric Verifier that will use the given funcs and key source. +func NewVerifier(ks PublicKeySource, df ClaimsDecoderFunc, vf VerifyFunc) *Verifier { + return &Verifier{ + ks: ks, + df: df, + vf: vf, + skewAllowance: int64(defaultSkewAllowance.Seconds()), + } +} + +// VerifyInboundKitContext is meant to be used within a go-kit stack that has populated +// the context with common headers, specficially +// kit/transport/http.ContextKeyRequestAuthorization. +func (c Verifier) VerifyInboundKitContext(ctx context.Context) (bool, error) { + authHdr, ok := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) + if !ok { + return false, errors.New("auth header did not exist") + } + + auths := strings.Split(authHdr, " ") + if len(auths) != 2 { + return false, errors.New("auth header invalid format") + } + + return c.Verify(ctx, auths[1]) +} + +// Verify will accept an opaque JWT token, decode it and verify it. +func (c Verifier) Verify(ctx context.Context, token string) (bool, error) { + hdr, rawPayload, err := decodeToken(token) + if err != nil { + return false, err + } + + keys, err := c.ks.Get(ctx) + if err != nil { + return false, err + } + + key, err := keys.GetKey(hdr.KeyID) + if err != nil { + return false, err + } + + err = jws.Verify(token, key) + if err != nil { + return false, err + } + + // use claims decoder func + clmstr, err := c.df(ctx, rawPayload) + if err != nil { + return false, err + } + + claims := clmstr.BaseClaims() + nowUnix := timeNow().Unix() + + if nowUnix < (claims.Iat - c.skewAllowance) { + return false, errors.New("invalid issue time") + } + + if nowUnix > (claims.Exp + c.skewAllowance) { + return false, errors.New("invalid expiration time") + } + + return c.vf(ctx, clmstr), nil +} + +func decodeToken(token string) (*jws.Header, []byte, error) { + s := strings.Split(token, ".") + if len(s) != 3 { + return nil, nil, errors.New("invalid token") + } + + dh, err := base64.RawURLEncoding.DecodeString(s[0]) + if err != nil { + return nil, nil, err + } + var h jws.Header + err = json.Unmarshal(dh, &h) + if err != nil { + return nil, nil, err + } + + dcs, err := base64.RawURLEncoding.DecodeString(s[1]) + if err != nil { + return nil, nil, err + } + return &h, dcs, nil +} diff --git a/auth/verify_test.go b/auth/verify_test.go new file mode 100644 index 000000000..eae607ce4 --- /dev/null +++ b/auth/verify_test.go @@ -0,0 +1,332 @@ +package auth + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + httptransport "github.com/go-kit/kit/transport/http" + "github.com/google/go-cmp/cmp" + "golang.org/x/oauth2/jws" +) + +func TestVerifyInboundKit(t *testing.T) { + tests := []struct { + name string + + givenBadToken bool + givenBadContext bool + + wantVerified bool + wantErr bool + }{ + { + name: "normal route, success", + + wantVerified: true, + }, + { + name: "bad token", + + givenBadToken: true, + + wantErr: true, + }, + { + name: "bad context", + + givenBadContext: true, + + wantErr: true, + }, + } + + prv, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + t.Fatalf("unable to generate private key: %s", err) + } + + keyID := "the-key" + testTime := time.Date(2018, 10, 29, 12, 0, 0, 0, time.UTC) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + timeNow = func() time.Time { return testTime } + + token, err := encode( + &jws.Header{Algorithm: "RS256", Typ: "JWT", KeyID: keyID}, + testClaims{ + ClaimSet: jws.ClaimSet{ + Iss: "example.com", + Iat: testTime.Add(-5 * time.Second).Unix(), + Exp: testTime.Add(5 * time.Second).Unix(), + Aud: "example.com", + }, + }, prv) + if err != nil { + t.Fatalf("unable to encode token: %s", err) + } + + decoderFunc := func(_ context.Context, b []byte) (ClaimSetter, error) { + var c testClaims + err := json.Unmarshal(b, &c) + return c, err + } + + verifyFunc := func(_ context.Context, c interface{}) bool { + return true + } + + ks := testKeySource{ + keys: PublicKeySet{ + Expiry: timeNow().Add(time.Hour), + Keys: map[string]*rsa.PublicKey{ + keyID: &prv.PublicKey, + }, + }, + } + + vrfy := NewVerifier(ks, decoderFunc, verifyFunc) + + ctx := context.Background() + + token = "Bearer " + token + if test.givenBadToken { + token = "ASDFLKANSDFLKJ" + } + + if !test.givenBadContext { + ctx = context.WithValue(ctx, httptransport.ContextKeyRequestAuthorization, token) + } + + verified, err := vrfy.VerifyInboundKitContext(ctx) + if (err != nil) != test.wantErr { + t.Errorf("unexpected error? %t, got %s", test.wantErr, err) + } + + if verified != test.wantVerified { + t.Errorf("wanted verified? %t, got %t", test.wantVerified, verified) + } + }) + } + +} + +func TestVerify(t *testing.T) { + testTime := time.Date(2018, 10, 29, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + + givenClaims testClaims + givenBadToken bool + givenTime time.Time + givenKeyError error + givenBadKeyID bool + + wantVerified bool + wantErr bool + }{ + { + name: "normal JWT, success", + givenClaims: testClaims{ + ClaimSet: jws.ClaimSet{ + Iss: "example.com", + Iat: testTime.Add(-5 * time.Second).Unix(), + Exp: testTime.Add(5 * time.Second).Unix(), + Aud: "example.com", + }, + }, + givenTime: testTime, + + wantVerified: true, + }, + { + name: "invalid Iat", + givenClaims: testClaims{ + ClaimSet: jws.ClaimSet{ + Iss: "example.com", + Iat: testTime.Add(10 * time.Minute).Unix(), + Exp: testTime.Add(5 * time.Second).Unix(), + Aud: "example.com", + }, + }, + givenTime: testTime, + + wantErr: true, + }, + { + name: "expired claims", + givenClaims: testClaims{ + ClaimSet: jws.ClaimSet{ + Iss: "example.com", + Iat: testTime.Add(-15 * time.Minute).Unix(), + Exp: testTime.Add(-10 * time.Minute).Unix(), + Aud: "example.com", + }, + }, + givenTime: testTime, + + wantErr: true, + }, + { + name: "unable to get keys", + givenClaims: testClaims{ + ClaimSet: jws.ClaimSet{ + Iss: "example.com", + Iat: testTime.Add(-15 * time.Minute).Unix(), + Exp: testTime.Add(-10 * time.Minute).Unix(), + Aud: "example.com", + }, + }, + givenTime: testTime, + givenKeyError: errors.New("angry computer"), + + wantErr: true, + }, + { + name: "invalid key id", + givenClaims: testClaims{ + ClaimSet: jws.ClaimSet{ + Iss: "example.com", + Iat: testTime.Add(-15 * time.Minute).Unix(), + Exp: testTime.Add(-10 * time.Minute).Unix(), + Aud: "example.com", + }, + }, + givenTime: testTime, + givenBadKeyID: true, + + wantErr: true, + }, + { + name: "invalid token", + givenClaims: testClaims{ + ClaimSet: jws.ClaimSet{ + Iss: "example.com", + Iat: testTime.Add(-15 * time.Minute).Unix(), + Exp: testTime.Add(-10 * time.Minute).Unix(), + Aud: "example.com", + }, + }, + givenTime: testTime, + givenBadToken: true, + + wantErr: true, + }, + } + + prv, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + t.Fatalf("unable to generate private key: %s", err) + } + + keyID := "the-key" + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + timeNow = func() time.Time { return testTime } + + token, err := encode( + &jws.Header{Algorithm: "RS256", Typ: "JWT", KeyID: keyID}, + test.givenClaims, prv) + if err != nil { + t.Fatalf("unable to encode token: %s", err) + } + + decoderFunc := func(_ context.Context, b []byte) (ClaimSetter, error) { + var c testClaims + err := json.Unmarshal(b, &c) + return c, err + } + + verifyFunc := func(_ context.Context, c interface{}) bool { + tc, ok := c.(testClaims) + if !ok { + t.Errorf("expected testClaims type, got %T", c) + } + if !cmp.Equal(tc, test.givenClaims) { + t.Errorf("claims were not what we expected: %s", cmp.Diff(tc, test.givenClaims)) + } + return true + } + + kid := keyID + if test.givenBadKeyID { + kid = "blah" + } + + ks := testKeySource{ + keys: PublicKeySet{ + Expiry: timeNow().Add(time.Hour), + Keys: map[string]*rsa.PublicKey{ + kid: &prv.PublicKey, + }, + }, + err: test.givenKeyError, + } + + vrfy := NewVerifier(ks, decoderFunc, verifyFunc) + + if test.givenBadToken { + token = "ASDFLKANSDFLKJ" + } + + verified, err := vrfy.Verify(context.Background(), token) + if (err != nil) != test.wantErr { + t.Errorf("unexpected error? %t, got %s", test.wantErr, err) + } + + if verified != test.wantVerified { + t.Errorf("wanted verified? %t, got %t", test.wantVerified, verified) + } + }) + } + +} + +// taken from jws package bc we just have an interface type for signing +func encodeWithSigner(header *jws.Header, c interface{}, sg jws.Signer) (string, error) { + bh, err := json.Marshal(header) + if err != nil { + return "", err + } + head := base64.RawURLEncoding.EncodeToString(bh) + + b, err := json.Marshal(c) + if err != nil { + return "", err + } + cs := base64.RawURLEncoding.EncodeToString(b) + ss := fmt.Sprintf("%s.%s", head, cs) + sig, err := sg([]byte(ss)) + if err != nil { + return "", err + } + return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil +} + +func encode(header *jws.Header, c interface{}, key *rsa.PrivateKey) (string, error) { + sg := func(data []byte) (sig []byte, err error) { + h := sha256.New() + h.Write(data) + return rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil)) + } + return encodeWithSigner(header, c, sg) +} + +type testClaims struct { + jws.ClaimSet +} + +func (t testClaims) BaseClaims() *jws.ClaimSet { + return &t.ClaimSet +} diff --git a/doc.go b/doc.go index 87d4a89f1..f68a5f5b2 100644 --- a/doc.go +++ b/doc.go @@ -1,221 +1,4 @@ -/* -Package gizmo is a toolkit that provides packages to put together server and pubsub daemons with the following features: - - * standardized configuration and logging - * health check endpoints with configurable strategies - * configuration for managing pprof endpoints and log levels - * structured logging containing basic request information - * useful metrics for endpoints - * graceful shutdowns - * basic interfaces to define our expectations and vocabulary - -## The `config` packages - -The `config` package contains a handful of useful functions to load to configuration structs from JSON files, JSON blobs in Consul k/v or environment variables. - -The subpackages contain structs meant for managing common configuration options and credentials. There are currently configs for: - - * Go Kit Metrics - * MySQL - * MongoDB - * Oracle - * AWS (S3, DynamoDB, ElastiCache) - * GCP - * Gorilla's `securecookie` - -The package also has a generic `Config` type in the `config/combined` subpackage that contains all of the above types. It's meant to be a 'catch all' convenience struct that many applications should be able to use. -The `server` package - -This package is the bulk of the toolkit and relies on `server.Config` for any managing `Server` implementations. A server must implement the following interface: - - // Server is the basic interface that defines what expect from any server. - type Server interface { - Register(Service) error - Start() error - Stop() error - } - -The package offers 2 server implementations: - -`SimpleServer`, which is capable of handling basic HTTP and JSON requests via 3 of the available `Service` implementations: `SimpleService`, `JSONService`, `ContextService`, `MixedService` and `MixedContextService`. A service and these implenetations will be defined below. - -`RPCServer`, which is capable of serving a gRPC server on one port and JSON endpoints on another. This kind of server can only handle the `RPCService` implementation. - -The `Service` interface is minimal to allow for maximum flexibility: - - type Service interface { - Prefix() string - - // Middleware provides a hook for service-wide middleware - Middleware(http.Handler) http.Handler - } - -The 3 service types that are accepted and hostable on the `SimpleServer`: - - type SimpleService interface { - Service - - // router - method - func - Endpoints() map[string]map[string]http.HandlerFunc - } - - type JSONService interface { - Service - - // Ensure that the route syntax is compatible with the router - // implementation chosen in cfg.RouterType. - // route - method - func - JSONEndpoints() map[string]map[string]JSONEndpoint - // JSONMiddleware provides a hook for service-wide middleware around JSONEndpoints. - JSONMiddleware(JSONEndpoint) JSONEndpoint - } - - type MixedService interface { - Service - - // route - method - func - Endpoints() map[string]map[string]http.HandlerFunc - - // Ensure that the route syntax is compatible with the router - // implementation chosen in cfg.RouterType. - // route - method - func - JSONEndpoints() map[string]map[string]JSONEndpoint - // JSONMiddleware provides a hook for service-wide middleware around JSONEndpoints. - JSONMiddleware(JSONEndpoint) JSONEndpoint - } - - type ContextService interface { - Service - - // route - method - func - ContextEndpoints() map[string]map[string]ContextHandlerFunc - // ContextMiddleware provides a hook for service-wide middleware around ContextHandler - ContextMiddleware(ContextHandler) ContextHandler - } - - type MixedContextService interface { - ContextService - - // route - method - func - JSONEndpoints() map[string]map[string]JSONContextEndpoint - JSONContextMiddleware(JSONContextEndpoint) JSONContextEndpoint - } - -Where `JSONEndpoint`, `JSONContextEndpoint`, `ContextHandler` and `ContextHandlerFunc` are defined as: - - type JSONEndpoint func(*http.Request) (int, interface{}, error) - - type JSONContextEndpoint func(context.Context, *http.Request) (int, interface{}, error) - - type ContextHandler interface { - ServeHTTPContext(context.Context, http.ResponseWriter, *http.Request) - } - - type ContextHandlerFunc func(context.Context, http.ResponseWriter, *http.Request) - -Also, the one service type that works with an `RPCServer`: - - type RPCService interface { - Service - - Service() (grpc.ServiceDesc, interface{}) - - // Ensure that the route syntax is compatible with the router - // implementation chosen in cfg.RouterType. - // route - method - func - JSONEndpoints() map[string]map[string]JSONContextEndpoint - // JSONMiddleware provides a hook for service-wide middleware around JSONContextEndpoints. - JSONMiddleware(JSONContextEndpoint) JSONContextEndpoint - } - -The `Middleware(..)` functions offer each service a 'hook' to wrap each of its endpoints. This may be handy for adding additional headers or context to the request. This is also the point where other, third-party middleware could be easily be plugged in (ie. oauth, tracing, metrics, logging, etc.) - -The `pubsub` package - -This package contains two generic interfaces for publishing data to queues and subscribing and consuming data from those queues. - - // Publisher is a generic interface to encapsulate how we want our publishers - // to behave. Until we find reason to change, we're forcing all publishers - // to emit protobufs. - type Publisher interface { - // Publish will publish a message. - Publish(ctx context.Context, key string, msg proto.Message) error - // Publish will publish a []byte message. - PublishRaw(ctx context.Context, key string, msg []byte) error - } - - // Subscriber is a generic interface to encapsulate how we want our subscribers - // to behave. For now the system will auto stop if it encounters any errors. If - // a user encounters a closed channel, they should check the Err() method to see - // what happened. - type Subscriber interface { - // Start will return a channel of raw messages - Start() <-chan SubscriberMessage - // Err will contain any errors returned from the consumer connection. - Err() error - // Stop will initiate a graceful shutdown of the subscriber connection - Stop() error - } - -Where a `SubscriberMessage` is an interface that gives implementations a hook for acknowledging/delete messages. Take a look at the docs for each implementation in `pubsub` to see how they behave. - -There are currently 3 implementations of each type of `pubsub` interfaces: - -For pubsub via Amazon's SNS/SQS, you can use the `pubsub/aws` package. - -For pubsub via Google's Pubsub, you can use the `pubsub/gcp` package. - -For pubsub via Kafka topics, you can use the `pubsub/kafka` package. - -For publishing via HTTP, you can use the `pubsub/http` package. - -The `pubsub/pubsubtest` package - -This package contains 'test' implementations of the `pubsub.Publisher` and `pubsub.Subscriber` interfaces that will allow developers to easily mock out and test their `pubsub` implementations: - - type TestPublisher struct { - // Published will contain a list of all messages that have been published. - Published []TestPublishMsg - - // GivenError will be returned by the TestPublisher on publish. - // Good for testing error scenarios. - GivenError error - - // FoundError will contain any errors encountered while marshalling - // the protobuf struct. - FoundError error - } - - type TestSubscriber struct { - // ProtoMessages will be marshalled into []byte used to mock out - // a feed if it is populated. - ProtoMessages []proto.Message - - // JSONMEssages will be marshalled into []byte and used to mock out - // a feed if it is populated. - JSONMessages []interface{} - - // GivenErrError will be returned by the TestSubscriber on Err(). - // Good for testing error scenarios. - GivenErrError error - - // GivenStopError will be returned by the TestSubscriber on Stop(). - // Good for testing error scenarios. - GivenStopError error - - // FoundError will contain any errors encountered while marshalling - // the JSON and protobuf struct. - FoundError error - } - -The `web` package - -This package contains a handful of very useful functions for parsing types from request queries and payloads. - -Examples - -For examples of how to use the gizmo `server` and `pubsub` packages, take a look at the 'examples' subdirectory. - -The Gizmo logo was based on the Go mascot designed by Renée French and copyrighted under the Creative Commons Attribution 3.0 license. +/* +Package gizmo is a toolkit that provides packages to put together server and pubsub daemons. */ package gizmo diff --git a/go.mod b/go.mod index c11aadfd7..ca5fa8a40 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,19 @@ module github.com/NYTimes/gizmo require ( cloud.google.com/go v0.26.0 contrib.go.opencensus.io/exporter/stackdriver v0.6.0 // indirect + github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895 // indirect github.com/NYTimes/gziphandler v1.0.1 github.com/NYTimes/logrotate v0.0.0-20170824154650-2b6e866fd507 github.com/NYTimes/sqliface v0.0.0-20180310195202-f8e6c8b78d37 github.com/Shopify/sarama v1.17.0 + github.com/Shopify/toxiproxy v2.1.3+incompatible // indirect github.com/VividCortex/gohistogram v1.0.0 // indirect + github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect github.com/aws/aws-sdk-go v1.15.21 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 + github.com/circonus-labs/circonus-gometrics v2.2.4+incompatible // indirect + github.com/circonus-labs/circonusllhist v0.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/eapache/go-resiliency v1.1.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect @@ -19,9 +24,13 @@ require ( github.com/go-logfmt/logfmt v0.3.0 // indirect github.com/go-sql-driver/mysql v1.4.0 github.com/go-stack/stack v1.8.0 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/protobuf v1.2.0 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/google/go-cmp v0.2.0 + github.com/google/martian v2.1.0+incompatible // indirect github.com/googleapis/gax-go v2.0.0+incompatible // indirect + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/gorilla/context v1.1.1 github.com/gorilla/handlers v1.4.0 github.com/gorilla/mux v1.6.2 @@ -29,34 +38,55 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 github.com/hashicorp/consul v1.2.2 github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 // indirect + github.com/hashicorp/go-immutable-radix v1.0.0 // indirect + github.com/hashicorp/go-msgpack v0.0.0-20150518234257-fa3f63826f7c // indirect + github.com/hashicorp/go-multierror v1.0.0 // indirect + github.com/hashicorp/go-retryablehttp v0.0.0-20180718195005-e651d75abec6 // indirect github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 // indirect + github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect + github.com/hashicorp/memberlist v0.1.0 // indirect github.com/hashicorp/serf v0.8.1 // indirect + github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/influxdata/influxdb v1.6.1 // indirect + github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d github.com/kelseyhightower/envconfig v1.3.0 + github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/miekg/dns v1.0.14 // indirect github.com/mitchellh/go-homedir v1.0.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/mapstructure v1.0.0 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d + github.com/onsi/gomega v1.4.2 // indirect + github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c // indirect github.com/pierrec/lz4 v2.0.3+incompatible // indirect github.com/pkg/errors v0.8.0 + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v0.8.0 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 // indirect github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 // indirect + github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/sirupsen/logrus v1.0.6 + github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect + github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect + github.com/stretchr/testify v1.2.2 // indirect + github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect go.opencensus.io v0.15.0 // indirect golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac // indirect - golang.org/x/net v0.0.0-20180826012351-8a410e7b638d + golang.org/x/net v0.0.0-20180906233101-161cd47e91fd golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be - golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect - golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87 // indirect - golang.org/x/text v0.3.0 // indirect golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect google.golang.org/api v0.0.0-20180826000528-7954115fcf34 google.golang.org/appengine v1.1.0 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 google.golang.org/grpc v1.14.0 + gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce + gopkg.in/vmihailenco/msgpack.v2 v2.9.1 // indirect ) diff --git a/go.sum b/go.sum index c3cc4b9d1..7aaa7c7af 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= contrib.go.opencensus.io/exporter/stackdriver v0.6.0 h1:U0FQWsZU3aO8W+BrZc88T8fdd24qe3Phawa9V9oaVUE= contrib.go.opencensus.io/exporter/stackdriver v0.6.0/go.mod h1:QeFzMJDAw8TXt5+aRaSuE8l5BwaMIOIlaVkBOPRuMuw= +github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/NYTimes/gziphandler v1.0.1 h1:iLrQrdwjDd52kHDA5op2UBJFjmOb9g+7scBan4RN8F0= github.com/NYTimes/gziphandler v1.0.1/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/logrotate v0.0.0-20170824154650-2b6e866fd507 h1:S0NXkQpfmcwUz5kHr2G8fqepsknsnNr9j6uQqdTYEck= @@ -10,14 +11,20 @@ github.com/NYTimes/sqliface v0.0.0-20180310195202-f8e6c8b78d37 h1:w+JsDnKEY/IBBR github.com/NYTimes/sqliface v0.0.0-20180310195202-f8e6c8b78d37/go.mod h1:kyL8Cd2vQCEDjsCaFWSARXwRfyQfXv9/mHvr59mHxD8= github.com/Shopify/sarama v1.17.0 h1:Y2/FBwElFVwt7aLKL3fDG6hh+rrlywR6uLgTgKObwTc= github.com/Shopify/sarama v1.17.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.3+incompatible h1:awiJqUYH4q4OmoBiRccJykjd7B+w0loJi2keSna4X/M= +github.com/Shopify/toxiproxy v2.1.3+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/aws/aws-sdk-go v1.15.21 h1:STLvc6RrpycslC1NRtTvt/YSgDkIGCTrB9K9vE5R2oQ= github.com/aws/aws-sdk-go v1.15.21/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g= github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/circonus-labs/circonus-gometrics v2.2.4+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.0 h1:3s0i9irZZhzwHAqAbx4BqbnOCVti+XiuoSiTpysNAuE= +github.com/circonus-labs/circonusllhist v0.1.0/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 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/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= @@ -26,6 +33,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +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/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.7.0 h1:ApufNmWF1H6/wUbAG81hZOHmqwd0zRf8mNfLjYj/064= @@ -36,12 +45,17 @@ github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 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/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA= @@ -54,32 +68,59 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:BWIsLfhgKhV5g/oF34aRjniBH github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/hashicorp/consul v1.2.2 h1:C5FurAZWLQ+XAjmL9g6rXbPlwxyyz8DvTL0WCAxTLAo= github.com/hashicorp/consul v1.2.2/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 h1:URgjUo+bs1KwatoNbwG0uCO4dHN4r1jsp4a5AGgHRjo= github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.0.0-20150518234257-fa3f63826f7c/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.0.0-20180718195005-e651d75abec6/go.mod h1:fXcdFsQoipQa7mwORhKad5jmDCeSy/RCGzWA08PO0lM= github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 h1:9HVkPxOpo+yO93Ah4yrO67d/qh0fbLLWbKqhYjyHq9A= github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg= +github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE= github.com/hashicorp/serf v0.8.1 h1:mYs6SMzu72+90OcPa5wr3nfznA4Dw9UyR791ZFNOIf4= github.com/hashicorp/serf v0.8.1/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/influxdata/influxdb v1.6.1 h1:OseoBlzI5ftNI/bczyxSWq6PKRCNEeiXvyWP/wS5fB0= github.com/influxdata/influxdb v1.6.1/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d h1:of6+TpypLAaiv4JxgH5aplBZnt0b65B4v4c8q5oy+Sk= github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d/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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +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/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= github.com/mitchellh/go-homedir v1.0.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/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I= github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= +github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pierrec/lz4 v2.0.3+incompatible h1:h0ipQUMRrnr+/HHhxhceftyXk4QcZsmxSNliSG75Bi0= github.com/pierrec/lz4 v2.0.3+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.8.0 h1:1921Yw9Gc3iSc4VQh3PIoOqgPCZS7G/4xQNVUp8Mda8= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= @@ -90,20 +131,26 @@ github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 h1:agujYaXJSxSo1 github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk= github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= go.opencensus.io v0.15.0 h1:r1SzcjSm4ybA0qZs3B4QYX072f8gK61Kh0qtwyFpfdk= go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac h1:7d7lG9fHOLdL6jZPtnV4LpI41SbohIJ1Atq7U991dMg= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= -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/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87 h1:GqwDwfvIpC33dK9bA1fD+JiDUNsuAiQiEkpHqUKze4o= -golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87/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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= @@ -116,5 +163,18 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.14.0 h1:ArxJuB1NWfPY6r9Gp9gqwplT0Ge7nqv9msgu03lHLmo= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=