diff --git a/.gitignore b/.gitignore index 231611b..69dfc9b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.dll *.so *.dylib +*.swp # Example binaries examples/acquirer/acquirer diff --git a/acquire/auth.go b/acquire/auth.go deleted file mode 100644 index 97153e1..0000000 --- a/acquire/auth.go +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -// Package acquire is used for getting Auths to pass in http requests. -package acquire - -import ( - "errors" - "fmt" - "net/http" -) - -// ErrEmptyCredentials is returned whenever an Acquirer is attempted to -// be built with empty credentials. -// Use DefaultAcquirer for such no-op use case. -var ErrEmptyCredentials = errors.New("Empty credentials are not valid") - -// Acquirer gets an Authorization value that can be added to an http request. -// The format of the string returned should be the key, a space, and then the -// auth string: '[AuthType] [AuthValue]' -type Acquirer interface { - Acquire() (string, error) -} - -// DefaultAcquirer is a no-op Acquirer. -type DefaultAcquirer struct{} - -// Acquire returns the zero values of the return types. -func (d *DefaultAcquirer) Acquire() (string, error) { - return "", nil -} - -// AddAuth adds an auth value to the Authorization header of an http request. -func AddAuth(r *http.Request, acquirer Acquirer) error { - if r == nil { - return errors.New("can't add authorization to nil request") - } - - if acquirer == nil { - return errors.New("acquirer is undefined") - } - - auth, err := acquirer.Acquire() - - if err != nil { - return fmt.Errorf("failed to acquire auth for request: %w", err) - } - - if auth != "" { - r.Header.Set("Authorization", auth) - } - - return nil -} - -// FixedValueAcquirer implements Acquirer with a constant authorization value. -type FixedValueAcquirer struct { - authValue string -} - -func (f *FixedValueAcquirer) Acquire() (string, error) { - return f.authValue, nil -} - -// NewFixedAuthAcquirer returns a FixedValueAcquirer with the given authValue. -func NewFixedAuthAcquirer(authValue string) (*FixedValueAcquirer, error) { - if authValue != "" { - return &FixedValueAcquirer{ - authValue: authValue}, nil - } - return nil, ErrEmptyCredentials -} diff --git a/acquire/auth_test.go b/acquire/auth_test.go deleted file mode 100644 index e24ac23..0000000 --- a/acquire/auth_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package acquire - -import ( - "errors" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAddAuth(t *testing.T) { - fixedAcquirer, _ := NewFixedAuthAcquirer("Basic abc==") - tests := []struct { - name string - request *http.Request - acquirer Acquirer - shouldError bool - authValue string - }{ - { - name: "RequestIsNil", - acquirer: &DefaultAcquirer{}, - shouldError: true, - }, - { - name: "AcquirerIsNil", - request: httptest.NewRequest(http.MethodGet, "/", nil), - shouldError: true, - }, - { - name: "AcquirerFails", - acquirer: &failingAcquirer{}, - shouldError: true, - }, - { - name: "HappyPath", - request: httptest.NewRequest(http.MethodGet, "/", nil), - acquirer: fixedAcquirer, - shouldError: false, - authValue: "Basic abc==", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - - if test.shouldError { - assert.NotNil(AddAuth(test.request, test.acquirer)) - } else { - assert.Nil(AddAuth(test.request, test.acquirer)) - assert.Equal(test.authValue, test.request.Header.Get("Authorization")) - } - }) - } -} - -func TestFixedAuthAcquirer(t *testing.T) { - t.Run("HappyPath", func(t *testing.T) { - assert := assert.New(t) - - acquirer, err := NewFixedAuthAcquirer("Basic xyz==") - assert.NotNil(acquirer) - assert.NoError(err) - - authValue, _ := acquirer.Acquire() - assert.Equal("Basic xyz==", authValue) - }) - - t.Run("EmptyCredentials", func(t *testing.T) { - assert := assert.New(t) - - acquirer, err := NewFixedAuthAcquirer("") - assert.Equal(ErrEmptyCredentials, err) - assert.Nil(acquirer) - }) -} - -func TestDefaultAcquirer(t *testing.T) { - assert := assert.New(t) - acquirer := &DefaultAcquirer{} - authValue, err := acquirer.Acquire() - assert.Empty(authValue) - assert.Empty(err) -} - -type failingAcquirer struct{} - -func (f *failingAcquirer) Acquire() (string, error) { - return "", errors.New("always fails") -} diff --git a/acquire/bearer.go b/acquire/bearer.go deleted file mode 100644 index 6f1429a..0000000 --- a/acquire/bearer.go +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package acquire - -import ( - "fmt" - "io" - "net/http" - "sync" - "time" -) - -// RemoteBearerTokenAcquirerOptions provides configuration for the RemoteBearerTokenAcquirer. -type RemoteBearerTokenAcquirerOptions struct { - AuthURL string `json:"authURL"` - Timeout time.Duration `json:"timeout"` - Buffer time.Duration `json:"buffer"` - RequestHeaders map[string]string `json:"requestHeaders"` - - GetToken TokenParser - GetExpiration ParseExpiration -} - -// RemoteBearerTokenAcquirer implements Acquirer and fetches the tokens from a remote location with caching strategy. -type RemoteBearerTokenAcquirer struct { - options RemoteBearerTokenAcquirerOptions - authValue string - authValueExpiration time.Time - httpClient *http.Client - nonExpiringSpecialCase time.Time - lock sync.RWMutex -} - -// SimpleBearer defines the field name mappings used by the default bearer token and expiration parsers. -type SimpleBearer struct { - ExpiresInSeconds float64 `json:"expires_in"` - Token string `json:"serviceAccessToken"` -} - -// NewRemoteBearerTokenAcquirer returns a RemoteBearerTokenAcquirer configured with the given options. -func NewRemoteBearerTokenAcquirer(options RemoteBearerTokenAcquirerOptions) (*RemoteBearerTokenAcquirer, error) { - if options.GetToken == nil { - options.GetToken = DefaultTokenParser - } - - if options.GetExpiration == nil { - options.GetExpiration = DefaultExpirationParser - } - - // TODO: we should inject timeout and buffer defaults values as well. - - return &RemoteBearerTokenAcquirer{ - options: options, - authValueExpiration: time.Now(), - httpClient: &http.Client{ - Timeout: options.Timeout, - }, - nonExpiringSpecialCase: time.Unix(0, 0), - }, nil -} - -// Acquire provides the cached token or, if it's near its expiry time, contacts -// the server for a new token to cache. -func (acquirer *RemoteBearerTokenAcquirer) Acquire() (string, error) { - acquirer.lock.RLock() - if time.Now().Add(acquirer.options.Buffer).Before(acquirer.authValueExpiration) { - defer acquirer.lock.RUnlock() - return acquirer.authValue, nil - } - acquirer.lock.RUnlock() - acquirer.lock.Lock() - defer acquirer.lock.Unlock() - - req, err := http.NewRequest("GET", acquirer.options.AuthURL, nil) - if err != nil { - return "", fmt.Errorf("failed to create new request for Bearer: %v", err) - } - - for key, value := range acquirer.options.RequestHeaders { - req.Header.Set(key, value) - } - - resp, errHTTP := acquirer.httpClient.Do(req) - if errHTTP != nil { - return "", fmt.Errorf("error making request to '%v' to acquire bearer token: %v", - acquirer.options.AuthURL, errHTTP) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("received non 200 code acquiring Bearer: code %v", resp.Status) - } - - respBody, errRead := io.ReadAll(resp.Body) - if errRead != nil { - return "", fmt.Errorf("error reading HTTP response body: %v", errRead) - } - - token, err := acquirer.options.GetToken(respBody) - if err != nil { - return "", fmt.Errorf("error parsing bearer token from http response body: %v", err) - } - expiration, err := acquirer.options.GetExpiration(respBody) - if err != nil { - return "", fmt.Errorf("error parsing bearer token expiration from http response body: %v", err) - } - - acquirer.authValue, acquirer.authValueExpiration = "Bearer "+token, expiration - return acquirer.authValue, nil -} diff --git a/acquire/bearer_test.go b/acquire/bearer_test.go deleted file mode 100644 index 9505c2b..0000000 --- a/acquire/bearer_test.go +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package acquire - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestRemoteBearerTokenAcquirer(t *testing.T) { - goodAuth := SimpleBearer{ - Token: "test-token", - } - goodToken := "Bearer test-token" - - tests := []struct { - description string - authToken interface{} - authURL string - returnUnauthorized bool - expectedToken string - expectedErr error - }{ - { - description: "Success", - authToken: goodAuth, - expectedToken: goodToken, - expectedErr: nil, - }, - { - description: "HTTP Do Error", - authToken: goodAuth, - expectedToken: "", - authURL: "/", - expectedErr: errors.New("error making request to '/' to acquire bearer"), - }, - { - description: "HTTP Make Request Error", - authToken: goodAuth, - expectedToken: "", - authURL: "/\b", - expectedErr: errors.New("failed to create new request"), - }, - { - description: "HTTP Unauthorized Error", - authToken: goodAuth, - returnUnauthorized: true, - expectedToken: "", - expectedErr: errors.New("received non 200 code"), - }, - { - description: "Unmarshal Error", - authToken: []byte("{token:5555}"), - expectedToken: "", - expectedErr: errors.New("unable to parse bearer token"), - }, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - - // Start a local HTTP server - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - - // Test optional headers - assert.Equal("v0", req.Header.Get("k0")) - assert.Equal("v1", req.Header.Get("k1")) - - // Send response to be tested - if tc.returnUnauthorized { - rw.WriteHeader(http.StatusUnauthorized) - return - } - marshaledAuth, err := json.Marshal(tc.authToken) - assert.NoError(err) - rw.Write(marshaledAuth) - })) - // Close the server when test finishes - defer server.Close() - - url := server.URL - if tc.authURL != "" { - url = tc.authURL - } - - // Use Client & URL from our local test server - auth, errConstructor := NewRemoteBearerTokenAcquirer(RemoteBearerTokenAcquirerOptions{ - AuthURL: url, - Timeout: 5 * time.Second, - RequestHeaders: map[string]string{"k0": "v0", "k1": "v1"}, - }) - - assert.NoError(errConstructor) - - token, err := auth.Acquire() - - if tc.expectedErr == nil || err == nil { - assert.Equal(tc.expectedErr, err) - } else { - assert.Contains(err.Error(), tc.expectedErr.Error()) - } - assert.Equal(tc.expectedToken, token) - }) - } -} - -func TestRemoteBearerTokenAcquirerCaching(t *testing.T) { - assert := assert.New(t) - - count := 0 - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - auth := SimpleBearer{ - Token: fmt.Sprintf("gopher%v", count), - ExpiresInSeconds: 3600, //1 hour - } - count++ - - marshaledAuth, err := json.Marshal(&auth) - assert.NoError(err) - rw.Write(marshaledAuth) - })) - defer server.Close() - - // Use Client & URL from our local test server - auth, errConstructor := NewRemoteBearerTokenAcquirer(RemoteBearerTokenAcquirerOptions{ - AuthURL: server.URL, - Timeout: time.Duration(5) * time.Second, - Buffer: time.Microsecond, - }) - assert.NoError(errConstructor) - token, err := auth.Acquire() - assert.NoError(err) - - cachedToken, err := auth.Acquire() - assert.NoError(err) - assert.Equal(token, cachedToken) - assert.Equal(1, count) -} - -func TestRemoteBearerTokenAcquirerExiting(t *testing.T) { - assert := assert.New(t) - - count := 0 - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - auth := SimpleBearer{ - Token: fmt.Sprintf("gopher%v", count), - ExpiresInSeconds: 1, //1 second - } - count++ - - marshaledAuth, err := json.Marshal(&auth) - assert.NoError(err) - rw.Write(marshaledAuth) - })) - defer server.Close() - - // Use Client & URL from our local test server - auth, errConstructor := NewRemoteBearerTokenAcquirer(RemoteBearerTokenAcquirerOptions{ - AuthURL: server.URL, - Timeout: time.Duration(5) * time.Second, - Buffer: time.Second, - }) - assert.NoError(errConstructor) - token, err := auth.Acquire() - assert.NoError(err) - var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - _, err := auth.Acquire() - assert.NoError(err) - wg.Done() - }() - } - wg.Wait() - cachedToken, err := auth.Acquire() - assert.NoError(err) - assert.NotEqual(token, cachedToken) -} diff --git a/acquire/parsers.go b/acquire/parsers.go deleted file mode 100644 index d3c338a..0000000 --- a/acquire/parsers.go +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package acquire - -import ( - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/golang-jwt/jwt" - "github.com/spf13/cast" -) - -var ( - errMissingExpClaim = errors.New("missing exp claim in jwt") - errUnexpectedCasting = errors.New("unexpected casting error") -) - -// TokenParser defines the function signature of a bearer token extractor from a payload. -type TokenParser func([]byte) (string, error) - -// ParseExpiration defines the function signature of a bearer token expiration date extractor. -type ParseExpiration func([]byte) (time.Time, error) - -// DefaultTokenParser extracts a bearer token as defined by a SimpleBearer in a payload. -func DefaultTokenParser(data []byte) (string, error) { - var bearer SimpleBearer - - if errUnmarshal := json.Unmarshal(data, &bearer); errUnmarshal != nil { - return "", fmt.Errorf("unable to parse bearer token: %w", errUnmarshal) - } - return bearer.Token, nil -} - -// DefaultExpirationParser extracts a bearer token expiration date as defined by a SimpleBearer in a payload. -func DefaultExpirationParser(data []byte) (time.Time, error) { - var bearer SimpleBearer - - if errUnmarshal := json.Unmarshal(data, &bearer); errUnmarshal != nil { - return time.Time{}, fmt.Errorf("unable to parse bearer token expiration: %w", errUnmarshal) - } - return time.Now().Add(time.Duration(bearer.ExpiresInSeconds) * time.Second), nil -} - -func RawTokenParser(data []byte) (string, error) { - return string(data), nil -} - -func RawTokenExpirationParser(data []byte) (time.Time, error) { - p := jwt.Parser{SkipClaimsValidation: true} - token, _, err := p.ParseUnverified(string(data), jwt.MapClaims{}) - if err != nil { - return time.Time{}, err - } - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - return time.Time{}, errUnexpectedCasting - } - expVal, ok := claims["exp"] - if !ok { - return time.Time{}, errMissingExpClaim - } - - exp, err := cast.ToInt64E(expVal) - if err != nil { - return time.Time{}, err - } - return time.Unix(exp, 0), nil -} diff --git a/acquire/parsers_test.go b/acquire/parsers_test.go deleted file mode 100644 index 34b098b..0000000 --- a/acquire/parsers_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package acquire - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestRawTokenParser(t *testing.T) { - assert := assert.New(t) - payload := []byte("eyJhbGciOiJSUzI1NiIsImtpZCI6ImRldmVsb3BtZW50IiwidHlwIjoiSldUIn0.eyJhbGxvd2VkUmVzb3VyY2VzIjp7ImFsbG93ZWRQYXJ0bmVycyI6WyJjb21jYXN0Il19LCJhdWQiOiJYTWlEVCIsImNhcGFiaWxpdGllcyI6WyJ4MTppc3N1ZXI6dGVzdDouKjphbGwiLCJ4MTppc3N1ZXI6dWk6YWxsIl0sImV4cCI6MTYyMjE1Nzk4MSwiaWF0IjoxNjIyMDcxNTgxLCJpc3MiOiJkZXZlbG9wbWVudCIsImp0aSI6ImN4ZmkybTZDWnJjaFNoZ1Nzdi1EM3ciLCJuYmYiOjE2MjIwNzE1NjYsInBhcnRuZXItaWQiOiJjb21jYXN0Iiwic3ViIjoiY2xpZW50LXN1cHBsaWVkIiwidHJ1c3QiOjEwMDB9.7QzRWJgxGs1cEZunMOewYCnEDiq2CTDh5R5F47PYhkMVb2KxSf06PRRGN-rQSWPhhBbev1fGgu63mr3yp_VDmdVvHR2oYiKyxP2skJTSzfQmiRyLMYY5LcLn3BObyQxU8EnLhnqGIjpORW0L5Dd4QsaZmXRnkC73yGnJx4XCx0I") - token, err := RawTokenParser(payload) - assert.Equal(string(payload), token) - assert.Nil(err) -} - -func TestRawExpirationParser(t *testing.T) { - tcs := []struct { - Description string - Payload []byte - ShouldFail bool - ExpectedTime time.Time - }{ - { - Description: "Not a JWT", - Payload: []byte("xyz==abcNotAJWT"), - ShouldFail: true, - }, - { - Description: "A jwt", - Payload: []byte("eyJhbGciOiJSUzI1NiIsImtpZCI6ImRldmVsb3BtZW50IiwidHlwIjoiSldUIn0.eyJhbGxvd2VkUmVzb3VyY2VzIjp7ImFsbG93ZWRQYXJ0bmVycyI6WyJjb21jYXN0Il19LCJhdWQiOiJYTWlEVCIsImNhcGFiaWxpdGllcyI6WyJ4MTppc3N1ZXI6dGVzdDouKjphbGwiLCJ4MTppc3N1ZXI6dWk6YWxsIl0sImV4cCI6MTYyMjE1Nzk4MSwiaWF0IjoxNjIyMDcxNTgxLCJpc3MiOiJkZXZlbG9wbWVudCIsImp0aSI6ImN4ZmkybTZDWnJjaFNoZ1Nzdi1EM3ciLCJuYmYiOjE2MjIwNzE1NjYsInBhcnRuZXItaWQiOiJjb21jYXN0Iiwic3ViIjoiY2xpZW50LXN1cHBsaWVkIiwidHJ1c3QiOjEwMDB9.7QzRWJgxGs1cEZunMOewYCnEDiq2CTDh5R5F47PYhkMVb2KxSf06PRRGN-rQSWPhhBbev1fGgu63mr3yp_VDmdVvHR2oYiKyxP2skJTSzfQmiRyLMYY5LcLn3BObyQxU8EnLhnqGIjpORW0L5Dd4QsaZmXRnkC73yGnJx4XCx0I"), - ExpectedTime: time.Unix(1622157981, 0), - }, - } - - for _, tc := range tcs { - assert := assert.New(t) - exp, err := RawTokenExpirationParser(tc.Payload) - if tc.ShouldFail { - assert.NotNil(err) - assert.Empty(exp) - } else { - assert.Nil(err) - assert.Equal(tc.ExpectedTime, exp) - } - } -} diff --git a/attributes.go b/attributes.go index b5bda36..d33ad38 100644 --- a/attributes.go +++ b/attributes.go @@ -3,49 +3,45 @@ package bascule -type BasicAttributes map[string]interface{} - -func (a BasicAttributes) Get(key string) (interface{}, bool) { - v, ok := a[key] - return v, ok -} - -// NewAttributes builds an Attributes instance with -// the given map as datasource. -func NewAttributes(m map[string]interface{}) Attributes { - return BasicAttributes(m) +// Attributes is an optional interface that a Token may implement +// that provides access to arbitrary key/value pairs. +type Attributes interface { + // Get returns the value of an attribute, if it exists. + Get(key string) (any, bool) } -// GetNestedAttribute uses multiple keys in order to obtain an attribute. -func GetNestedAttribute(attributes Attributes, keys ...string) (interface{}, bool) { - // need at least one key. +// GetAttribute provides a typesafe way of obtaining attribute values. +// This function will return false if either the attribute doesn't exist +// or if the attribute's value of not of type T. +// +// Multiple keys may be passed to this function, in which case the keys will +// be traversed to find the nested key. If any intervening keys are not of +// type map[string]any or Attributes, this function will return false. +// +// If no keys are supplied, this function returns the zero value for T and false. +func GetAttribute[T any](a Attributes, keys ...string) (v T, ok bool) { if len(keys) == 0 { - return nil, false + return } - var ( - result interface{} - ok bool - ) - result = attributes - for _, k := range keys { - var a Attributes - switch r := result.(type) { - case BasicAttributes: - a = r - case Attributes: - a = r - case map[string]interface{}: - a = BasicAttributes(r) - default: - return nil, false + var raw any + raw, ok = a.Get(keys[0]) + for i := 1; ok && i < len(keys); i++ { + var m map[string]any + if m, ok = raw.(map[string]any); ok { + raw, ok = m[keys[i]] + continue } - result, ok = a.Get(k) - if !ok { - return nil, false + var a Attributes + if a, ok = raw.(Attributes); ok { + raw, ok = a.Get(keys[i]) } } - return result, ok + if ok { + v, ok = raw.(T) + } + + return } diff --git a/attributes_test.go b/attributes_test.go index c910461..f437620 100644 --- a/attributes_test.go +++ b/attributes_test.go @@ -4,91 +4,126 @@ package bascule import ( + "fmt" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -func TestGet(t *testing.T) { - assert := assert.New(t) - attributes := attrs +type testAttributes map[string]any - val, ok := attributes.Get("testkey") - assert.Equal("testval", val) - assert.True(ok) +func (ta testAttributes) Get(key string) (v any, ok bool) { + v, ok = ta[key] + return +} - val, ok = attributes.Get("noval") - assert.Empty(val) - assert.False(ok) +type AttributesTestSuite struct { + suite.Suite +} - emptyAttributes := NewAttributes(map[string]interface{}{}) - val, ok = emptyAttributes.Get("test") - assert.Nil(val) - assert.False(ok) +func (suite *AttributesTestSuite) testAttributes() Attributes { + return testAttributes{ + "value": 123, + "untypedNil": nil, + "emptyMap": map[string]any{}, + "nestedMap": map[string]any{ + "value": 123, + "nestedMap": map[string]any{ + "value": 123, + }, + "nestedAttributes": Attributes(testAttributes{ + "value": 123, + }), + }, + "nestedAttributes": Attributes(testAttributes{ + "value": 123, + "nestedMap": map[string]any{ + "value": 123, + }, + "nestedAttributes": Attributes(testAttributes{ + "value": 123, + }), + }), + } } -func TestGetNestedAttribute(t *testing.T) { - attributes := NewAttributes(map[string]interface{}{ - "a": map[string]interface{}{"b": map[string]interface{}{"c": "answer"}}, - "one level": "yay", - "bad": nil, - }) - tests := []struct { - description string - keys []string - expectedResult interface{} - expectedOK bool +func (suite *AttributesTestSuite) TestGetAttribute() { + testCases := []struct { + keys []string + expectedValue int + expectedOK bool }{ - // Success test is failing. ): getting nil, false { - description: "Success", - keys: []string{"a", "b", "c"}, - expectedResult: "answer", - expectedOK: true, + keys: nil, + }, + { + keys: []string{"missing"}, }, { - description: "Success single key", - keys: []string{"one level"}, - expectedResult: "yay", - expectedOK: true, + keys: []string{"untypedNil"}, }, { - description: "Success nil", - keys: []string{"bad"}, - expectedResult: nil, - expectedOK: true, + keys: []string{"untypedNil", "value"}, }, { - description: "Nil Keys Error", - keys: nil, + keys: []string{"value"}, + expectedValue: 123, + expectedOK: true, }, { - description: "No Keys Error", - keys: []string{}, + keys: []string{"emptyMap"}, }, { - description: "Non Attribute Value Error", - keys: []string{"one level", "test"}, + keys: []string{"emptyMap", "value"}, }, { - description: "Nil Attributes Error", - keys: []string{"bad", "more bad"}, + keys: []string{"nestedMap"}, }, { - description: "Missing Key Error", - keys: []string{"c", "b", "a"}, + keys: []string{"nestedMap", "missing"}, }, { - description: "Wrong Key Case Error", - keys: []string{"A", "B", "C"}, + keys: []string{"nestedMap", "value"}, + expectedValue: 123, + expectedOK: true, + }, + { + keys: []string{"nestedMap", "nestedMap", "missing"}, + }, + { + keys: []string{"nestedMap", "nestedMap", "value"}, + expectedValue: 123, + expectedOK: true, + }, + { + keys: []string{"nestedMap", "nestedAttributes", "value"}, + expectedValue: 123, + expectedOK: true, + }, + { + keys: []string{"nestedAttributes", "nestedMap", "missing"}, + }, + { + keys: []string{"nestedAttributes", "nestedMap", "value"}, + expectedValue: 123, + expectedOK: true, + }, + { + keys: []string{"nestedAttributes", "nestedAttributes", "value"}, + expectedValue: 123, + expectedOK: true, }, } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - val, ok := GetNestedAttribute(attributes, tc.keys...) - assert.Equal(tc.expectedResult, val) - assert.Equal(tc.expectedOK, ok) + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("%v", testCase.keys), func() { + actual, ok := GetAttribute[int](suite.testAttributes(), testCase.keys...) + suite.Equal(testCase.expectedValue, actual) + suite.Equal(testCase.expectedOK, ok) }) } } + +func TestAttributes(t *testing.T) { + suite.Run(t, new(AttributesTestSuite)) +} diff --git a/authorizer.go b/authorizer.go new file mode 100644 index 0000000..9818416 --- /dev/null +++ b/authorizer.go @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +import ( + "context" + + "go.uber.org/multierr" +) + +// Authorizer is a strategy for determining if a given token represents +// adequate permissions to access a resource. +type Authorizer[R any] interface { + // Authorize tests if a given token holds the correct permissions to + // access a given resource. If this method needs to access external + // systems, it should pass the supplied context to honor context + // cancelation semantics. + // + // If this method doesn't support the given token, it should return nil. + Authorize(ctx context.Context, token Token, resource R) error +} + +// AuthorizerFunc is a closure type that implements Authorizer. +type AuthorizerFunc[R any] func(context.Context, Token, R) error + +func (af AuthorizerFunc[R]) Authorize(ctx context.Context, token Token, resource R) error { + return af(ctx, token, resource) +} + +// Authorizers is a collection of Authorizers. +type Authorizers[R any] []Authorizer[R] + +// Add appends authorizers to this aggregate Authorizers. +func (as *Authorizers[R]) Add(a ...Authorizer[R]) { + if *as == nil { + *as = make(Authorizers[R], 0, len(a)) + } + + *as = append(*as, a...) +} + +// Authorize requires all authorizers in this sequence to allow access. This +// method supplies a logical AND. +// +// Because authorization can be arbitrarily expensive, execution halts at the first failed +// authorization attempt. +func (as Authorizers[R]) Authorize(ctx context.Context, token Token, resource R) error { + for _, a := range as { + if err := a.Authorize(ctx, token, resource); err != nil { + return err + } + } + + return nil +} + +type requireAny[R any] struct { + a Authorizers[R] +} + +// Authorize returns nil at the first authorizer that returns nil, i.e. accepts the access. +// Otherwise, this method returns an aggregate error of all the authorization errors. +func (ra requireAny[R]) Authorize(ctx context.Context, token Token, resource R) error { + var err error + for _, a := range ra.a { + authErr := a.Authorize(ctx, token, resource) + if authErr == nil { + return nil + } + + err = multierr.Append(err, authErr) + } + + return err +} + +// Any returns an Authorizer which is a logical OR: each authorizer is executed in +// order, and any authorizer that allows access results in an immediate return. The +// returned Authorizer's state is distinct and is unaffected by subsequent changes +// to the Authorizers set. +// +// Any error returns from the returned Authorizer will be an aggregate of all the errors +// returned from each element. +func (as Authorizers[R]) Any() Authorizer[R] { + return requireAny[R]{ + a: append( + make(Authorizers[R], 0, len(as)), + as..., + ), + } +} diff --git a/authorizer_test.go b/authorizer_test.go new file mode 100644 index 0000000..a77ba48 --- /dev/null +++ b/authorizer_test.go @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/suite" +) + +type AuthorizersTestSuite struct { + TestSuite +} + +func (suite *AuthorizersTestSuite) TestAuthorize() { + const placeholderResource = "placeholder resource" + authorizeErr := errors.New("expected Authorize error") + + testCases := []struct { + name string + results []error + expectedErr error + }{ + { + name: "EmptyAuthorizers", + results: nil, + }, + { + name: "OneSuccess", + results: []error{nil}, + }, + { + name: "OneFailure", + results: []error{authorizeErr}, + expectedErr: authorizeErr, + }, + { + name: "FirstFailure", + results: []error{authorizeErr, errors.New("should not be called")}, + expectedErr: authorizeErr, + }, + { + name: "MiddleFailure", + results: []error{nil, authorizeErr, errors.New("should not be called")}, + expectedErr: authorizeErr, + }, + { + name: "AllSuccess", + results: []error{nil, nil, nil}, + }, + } + + for _, testCase := range testCases { + suite.Run(testCase.name, func() { + var ( + testCtx = suite.testContext() + testToken = suite.testToken() + as Authorizers[string] + ) + + for _, err := range testCase.results { + err := err + as.Add( + AuthorizerFunc[string](func(ctx context.Context, token Token, resource string) error { + suite.Same(testCtx, ctx) + suite.Same(testToken, token) + suite.Equal(placeholderResource, resource) + return err + }), + ) + } + + suite.Equal( + testCase.expectedErr, + as.Authorize(testCtx, testToken, placeholderResource), + ) + }) + } +} + +func (suite *AuthorizersTestSuite) TestAny() { + const placeholderResource = "placeholder resource" + authorizeErr := errors.New("expected Authorize error") + + testCases := []struct { + name string + results []error + expectedErr error + }{ + { + name: "EmptyAuthorizers", + results: nil, + }, + { + name: "OneSuccess", + results: []error{nil, errors.New("should not be called")}, + }, + { + name: "OnlyFailure", + results: []error{authorizeErr}, + expectedErr: authorizeErr, + }, + { + name: "FirstFailure", + results: []error{authorizeErr, nil}, + }, + { + name: "LastSuccess", + results: []error{authorizeErr, authorizeErr, nil}, + }, + } + + for _, testCase := range testCases { + suite.Run(testCase.name, func() { + var ( + testCtx = suite.testContext() + testToken = suite.testToken() + as Authorizers[string] + ) + + for _, err := range testCase.results { + err := err + as.Add( + AuthorizerFunc[string](func(ctx context.Context, token Token, resource string) error { + suite.Same(testCtx, ctx) + suite.Same(testToken, token) + suite.Equal(placeholderResource, resource) + return err + }), + ) + } + + anyAs := as.Any() + suite.Equal( + testCase.expectedErr, + anyAs.Authorize(testCtx, testToken, placeholderResource), + ) + + if len(as) > 0 { + // the any instance should be distinct + as[0] = AuthorizerFunc[string]( + func(context.Context, Token, string) error { + suite.Fail("should not have been called") + return nil + }, + ) + + suite.Equal( + testCase.expectedErr, + anyAs.Authorize(testCtx, testToken, placeholderResource), + ) + } + }) + } +} + +func TestAuthorizers(t *testing.T) { + suite.Run(t, new(AuthorizersTestSuite)) +} diff --git a/basculechecks/capabilitiesmap.go b/basculechecks/capabilitiesmap.go deleted file mode 100644 index 6b6a98c..0000000 --- a/basculechecks/capabilitiesmap.go +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "errors" - "fmt" - "regexp" - - "github.com/xmidt-org/bascule" -) - -var ( - ErrNilDefaultChecker = errors.New("default checker cannot be nil") - ErrEmptyEndpoint = errWithReason{ - err: errors.New("endpoint provided is empty"), - reason: EmptyParsedURL, - } - errRegexCompileFail = errors.New("failed to compile regexp") -) - -// CapabilitiesMapConfig includes the values needed to set up a map capability -// checker. The checker will verify that one of the capabilities in a provided -// JWT match the string meant for that endpoint exactly. A CapabilitiesMap set -// up with this will use the default KeyPath. -type CapabilitiesMapConfig struct { - Endpoints map[string]string `json:"endpoints" yaml:"endpoints"` - Default string `json:"default" yaml:"default"` -} - -// CapabilitiesMap runs a capability check based on the value of the parsedURL, -// which is the key to the CapabilitiesMap's map. The parsedURL is expected to -// be some regex values, allowing for bucketing of urls that contain some kind -// of ID or otherwise variable portion of a URL. -type CapabilitiesMap struct { - Checkers map[string]EndpointChecker - DefaultChecker EndpointChecker - KeyPath []string -} - -// CheckAuthentication uses the parsed endpoint value to determine which EndpointChecker to -// run against the capabilities in the auth provided. If there is no -// EndpointChecker for the endpoint, the default is used. As long as one -// capability is found to be authorized by the EndpointChecker, no error is -// returned. -func (c CapabilitiesMap) CheckAuthentication(auth bascule.Authentication, vs ParsedValues) error { - if auth.Token == nil { - return ErrNoToken - } - - if auth.Request.URL == nil { - return ErrNoURL - } - - if vs.Endpoint == "" { - return ErrEmptyEndpoint - } - - capabilities, err := getCapabilities(auth.Token.Attributes(), c.KeyPath) - if err != nil { - return err - } - - // determine which EndpointChecker to use. - checker, ok := c.Checkers[vs.Endpoint] - if !ok || checker == nil { - checker = c.DefaultChecker - } - reqURL := auth.Request.URL.EscapedPath() - method := auth.Request.Method - - // if the checker is nil, we treat it like a checker that always returns - // false. - if checker == nil { - // ErrNoValidCapabilityFound is a Reasoner. - return fmt.Errorf("%w in [%v] with nil endpoint checker", - ErrNoValidCapabilityFound, capabilities) - } - - // if one of the capabilities is good, then the request is authorized - // for this endpoint. - for _, capability := range capabilities { - if checker.Authorized(capability, reqURL, method) { - return nil - } - } - - return fmt.Errorf("%w in [%v] with %v endpoint checker", - ErrNoValidCapabilityFound, capabilities, checker.Name()) -} - -// NewCapabilitiesMap parses the CapabilitiesMapConfig provided into a -// CapabilitiesMap. The same regular expression provided for the map are also -// needed for labels for a MetricValidator, so an option to be used for that is -// also created. -func NewCapabilitiesMap(config CapabilitiesMapConfig) (CapabilitiesCheckerOut, error) { - // if we don't get a capability value, a nil default checker means always - // returning false. - var defaultChecker EndpointChecker - if config.Default != "" { - defaultChecker = ConstEndpointCheck(config.Default) - } - - i := 0 - rs := make([]*regexp.Regexp, len(config.Endpoints)) - endpointMap := map[string]EndpointChecker{} - for r, checkVal := range config.Endpoints { - regex, err := regexp.Compile(r) - if err != nil { - return CapabilitiesCheckerOut{}, fmt.Errorf("%w [%v]: %v", errRegexCompileFail, r, err) - } - // because rs is the length of config.Endpoints, i never overflows. - rs[i] = regex - i++ - endpointMap[r] = ConstEndpointCheck(checkVal) - } - - cc := CapabilitiesMap{ - Checkers: endpointMap, - DefaultChecker: defaultChecker, - } - - return CapabilitiesCheckerOut{ - Checker: cc, - Options: []MetricOption{WithEndpoints(rs)}, - }, nil -} diff --git a/basculechecks/capabilitiesmap_test.go b/basculechecks/capabilitiesmap_test.go deleted file mode 100644 index 1912e0e..0000000 --- a/basculechecks/capabilitiesmap_test.go +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "errors" - "fmt" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/xmidt-org/bascule" -) - -func TestCapabilitiesMapCheck(t *testing.T) { - goodDefault := ConstEndpointCheck("default checker") - checkersMap := map[string]EndpointChecker{ - "a": ConstEndpointCheck("meh"), - "bcedef": ConstEndpointCheck("yay"), - "all": ConstEndpointCheck("good"), - "fallback": nil, - } - cm := CapabilitiesMap{ - Checkers: checkersMap, - DefaultChecker: goodDefault, - } - nilCM := CapabilitiesMap{} - goodCapabilities := []string{ - "test", - "", - "yay", - "...", - } - goodToken := bascule.NewToken("test", "princ", - bascule.NewAttributes( - buildDummyAttributes(CapabilityKeys(), goodCapabilities))) - defaultCapabilities := []string{ - "test", - "", - "default checker", - "...", - } - defaultToken := bascule.NewToken("test", "princ", - bascule.NewAttributes( - buildDummyAttributes(CapabilityKeys(), defaultCapabilities))) - badToken := bascule.NewToken("", "", nil) - tests := []struct { - description string - cm CapabilitiesMap - token bascule.Token - includeURL bool - endpoint string - expectedErr error - }{ - { - description: "Success", - cm: cm, - token: goodToken, - includeURL: true, - endpoint: "bcedef", - }, - { - description: "Success Not in Map", - cm: cm, - token: defaultToken, - includeURL: true, - endpoint: "b", - }, - { - description: "Success Nil Map Value", - cm: cm, - token: defaultToken, - includeURL: true, - endpoint: "fallback", - }, - { - description: "No Match Error", - cm: cm, - token: goodToken, - includeURL: true, - endpoint: "b", - expectedErr: ErrNoValidCapabilityFound, - }, - { - description: "No Match with Default Checker Error", - cm: cm, - token: defaultToken, - includeURL: true, - endpoint: "bcedef", - expectedErr: ErrNoValidCapabilityFound, - }, - { - description: "No Match Nil Default Checker Error", - cm: nilCM, - token: defaultToken, - includeURL: true, - endpoint: "bcedef", - expectedErr: ErrNoValidCapabilityFound, - }, - { - description: "No Token Error", - cm: cm, - token: nil, - includeURL: true, - expectedErr: ErrNoToken, - }, - { - description: "No Request URL Error", - cm: cm, - token: goodToken, - includeURL: false, - expectedErr: ErrNoURL, - }, - { - description: "Empty Endpoint Error", - cm: cm, - token: goodToken, - includeURL: true, - endpoint: "", - expectedErr: ErrEmptyEndpoint, - }, - { - description: "Get Capabilities Error", - cm: cm, - token: badToken, - includeURL: true, - endpoint: "b", - expectedErr: ErrNilAttributes, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - auth := bascule.Authentication{ - Token: tc.token, - } - if tc.includeURL { - goodURL, err := url.Parse("/test") - require.Nil(err) - auth.Request = bascule.Request{ - URL: goodURL, - Method: "GET", - } - } - err := tc.cm.CheckAuthentication(auth, ParsedValues{Endpoint: tc.endpoint}) - if tc.expectedErr == nil { - assert.NoError(err) - return - } - assert.True(errors.Is(err, tc.expectedErr), - fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", - err, tc.expectedErr), - ) - // every error should be a reasoner. - var r Reasoner - assert.True(errors.As(err, &r), "expected error to be a Reasoner") - }) - } -} - -func TestNewCapabilitiesMap(t *testing.T) { - a := ".*" - b := "aaaaa+" - c1 := "yup" - c2 := "nope" - es := map[string]string{a: c1, b: c2} - m := map[string]EndpointChecker{ - a: ConstEndpointCheck(c1), - b: ConstEndpointCheck(c2), - } - - tests := []struct { - description string - config CapabilitiesMapConfig - expectedChecker CapabilitiesChecker - expectedErr error - }{ - { - description: "Success", - config: CapabilitiesMapConfig{ - Endpoints: es, - }, - expectedChecker: CapabilitiesMap{ - Checkers: m, - }, - }, - { - description: "Success with default", - config: CapabilitiesMapConfig{ - Endpoints: es, - Default: "pls", - }, - expectedChecker: CapabilitiesMap{ - Checkers: m, - DefaultChecker: ConstEndpointCheck("pls"), - }, - }, - { - description: "Regex fail", - config: CapabilitiesMapConfig{ - Endpoints: map[string]string{ - `\m\n\b\v`: "test", - }, - }, - expectedErr: errRegexCompileFail, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - c, err := NewCapabilitiesMap(tc.config) - if tc.expectedErr != nil { - assert.Empty(c) - require.Error(t, err) - assert.True(errors.Is(err, tc.expectedErr), - fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", - err, tc.expectedErr), - ) - return - } - assert.NoError(err) - assert.NotEmpty(c) - assert.Equal(tc.expectedChecker, c.Checker) - assert.NotNil(c.Options) - }) - } -} diff --git a/basculechecks/capabilitiesvalidator.go b/basculechecks/capabilitiesvalidator.go deleted file mode 100644 index 46073b9..0000000 --- a/basculechecks/capabilitiesvalidator.go +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "context" - "errors" - "fmt" - "regexp" - - "github.com/spf13/cast" - "github.com/xmidt-org/bascule" -) - -var ( - ErrNoAuth = errors.New("couldn't get request info: authorization not found") - ErrNoVals = errWithReason{ - err: errors.New("expected at least one value"), - reason: EmptyCapabilitiesList, - } - ErrNoToken = errWithReason{ - err: errors.New("no token found in Auth"), - reason: MissingValues, - } - ErrNoValidCapabilityFound = errWithReason{ - err: errors.New("no valid capability for endpoint"), - reason: NoCapabilitiesMatch, - } - ErrNilAttributes = errWithReason{ - err: errors.New("nil attributes interface"), - reason: MissingValues, - } - ErrNoMethod = errWithReason{ - err: errors.New("no method found in Auth"), - reason: MissingValues, - } - ErrNoURL = errWithReason{ - err: errors.New("invalid URL found in Auth"), - reason: MissingValues, - } - ErrGettingCapabilities = errWithReason{ - err: errors.New("couldn't get capabilities from attributes"), - reason: UndeterminedCapabilities, - } - ErrCapabilityNotStringSlice = errWithReason{ - err: errors.New("expected a string slice"), - reason: UndeterminedCapabilities, - } -) - -// EndpointChecker is an object that can determine if a value provides -// authorization to the endpoint. -type EndpointChecker interface { - Authorized(value string, reqURL string, method string) bool - Name() string -} - -// CapabilitiesValidatorConfig is input that can be used to build a -// CapabilitiesValidator and some metric options for a MetricValidator. A -// CapabilitiesValidator set up with this will use the default KeyPath and an -// EndpointRegexCheck. -type CapabilitiesValidatorConfig struct { - Type string `json:"type" yaml:"type"` - Prefix string `json:"prefix" yaml:"prefix"` - AcceptAllMethod string `json:"acceptAllMethod" yaml:"acceptAllMethod"` - EndpointBuckets []string `json:"endpointBuckets" yaml:"endpointBuckets"` -} - -// CapabilitiesValidator checks the capabilities provided in a -// bascule.Authentication object to determine if a request is authorized. It -// can also provide a function to be used in authorization middleware that -// pulls the Authentication object from a context before checking it. -type CapabilitiesValidator struct { - Checker EndpointChecker - KeyPath []string - ErrorOut bool -} - -// Check determines whether or not a client is authorized to make a request to -// an endpoint. It uses the bascule.Authentication from the context to get the -// information needed by the EndpointChecker to determine authorization. -func (c CapabilitiesValidator) Check(ctx context.Context, _ bascule.Token) error { - auth, ok := bascule.FromContext(ctx) - if !ok { - if c.ErrorOut { - return ErrNoAuth - } - return nil - } - - err := c.CheckAuthentication(auth, ParsedValues{}) - if err != nil && c.ErrorOut { - return fmt.Errorf("endpoint auth for %v on %v failed: %v", - auth.Request.Method, auth.Request.URL.EscapedPath(), err) - } - - return nil -} - -// CheckAuthentication takes the needed values out of the given Authentication object in -// order to determine if a request is authorized. It determines this through -// iterating through each capability and calling the EndpointChecker. If no -// capability authorizes the client for the given endpoint and method, it is -// unauthorized. -func (c CapabilitiesValidator) CheckAuthentication(auth bascule.Authentication, _ ParsedValues) error { - if auth.Token == nil { - return ErrNoToken - } - if len(auth.Request.Method) == 0 { - return ErrNoMethod - } - vals, err := getCapabilities(auth.Token.Attributes(), c.KeyPath) - if err != nil { - return err - } - - if auth.Request.URL == nil { - return ErrNoURL - } - reqURL := auth.Request.URL.EscapedPath() - method := auth.Request.Method - return c.checkCapabilities(vals, reqURL, method) -} - -// checkCapabilities uses a EndpointChecker to check if each capability -// provided is authorized. If an authorized capability is found, no error is -// returned. -func (c CapabilitiesValidator) checkCapabilities(capabilities []string, reqURL string, method string) error { - for _, val := range capabilities { - if c.Checker.Authorized(val, reqURL, method) { - return nil - } - } - return fmt.Errorf("%w in [%v] with %v endpoint checker", - ErrNoValidCapabilityFound, capabilities, c.Checker.Name()) - -} - -// getCapabilities runs some error checks while getting the list of -// capabilities from the attributes. -func getCapabilities(attributes bascule.Attributes, keyPath []string) ([]string, error) { - if attributes == nil { - return []string{}, ErrNilAttributes - } - - if len(keyPath) == 0 { - keyPath = CapabilityKeys() - } - - val, ok := bascule.GetNestedAttribute(attributes, keyPath...) - if !ok { - return []string{}, fmt.Errorf("%w using key path %v", - ErrGettingCapabilities, keyPath) - } - - vals, err := cast.ToStringSliceE(val) - if err != nil { - return []string{}, fmt.Errorf("%w for capabilities \"%v\": %v", - ErrCapabilityNotStringSlice, val, err) - } - - if len(vals) == 0 { - return []string{}, ErrNoVals - } - - return vals, nil - -} - -// NewCapabilitiesValidator uses the provided config to create an -// RegexEndpointCheck and wrap it in a CapabilitiesValidator. Metric Options -// are also created for a Metric Validator by parsing the type to determine if -// the metric validator should only monitor and compiling endpoints into Regexps. -func NewCapabilitiesValidator(config CapabilitiesValidatorConfig) (CapabilitiesCheckerOut, error) { - var out CapabilitiesCheckerOut - if config.Type != "enforce" && config.Type != "monitor" { - // unsupported capability check type. CapabilityCheck disabled. - return out, nil - } - c, err := NewRegexEndpointCheck(config.Prefix, config.AcceptAllMethod) - if err != nil { - return out, fmt.Errorf("error initializing endpointRegexCheck: %w", err) - } - - endpoints := make([]*regexp.Regexp, 0, len(config.EndpointBuckets)) - for _, e := range config.EndpointBuckets { - r, err := regexp.Compile(e) - if err != nil { - continue - } - endpoints = append(endpoints, r) - } - - os := []MetricOption{WithEndpoints(endpoints)} - if config.Type == "monitor" { - os = append(os, MonitorOnly()) - } - - out = CapabilitiesCheckerOut{ - Checker: CapabilitiesValidator{Checker: c}, - Options: os, - } - return out, nil -} diff --git a/basculechecks/capabilitiesvalidator_test.go b/basculechecks/capabilitiesvalidator_test.go deleted file mode 100644 index 0a5f288..0000000 --- a/basculechecks/capabilitiesvalidator_test.go +++ /dev/null @@ -1,419 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "context" - "errors" - "fmt" - "net/url" - "regexp" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/xmidt-org/bascule" -) - -var _ CapabilitiesChecker = CapabilitiesValidator{} - -func TestCapabilitiesValidatorCheck(t *testing.T) { - capabilities := []string{ - "test", - "a", - "joweiafuoiuoiwauf", - "it's a match", - } - goodURL, err := url.Parse("/test") - require.Nil(t, err) - goodRequest := bascule.Request{ - URL: goodURL, - Method: "GET", - } - tests := []struct { - description string - includeAuth bool - includeToken bool - errorOut bool - errExpected bool - }{ - { - description: "Success", - includeAuth: true, - includeToken: true, - errorOut: true, - }, - { - description: "No Auth Error", - errorOut: true, - errExpected: true, - }, - { - description: "No Auth Suppressed Error", - }, - { - description: "Check Error", - includeAuth: true, - errorOut: true, - errExpected: true, - }, - { - description: "Check Suppressed Error", - includeAuth: true, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - ctx := context.Background() - auth := bascule.Authentication{ - Request: goodRequest, - } - if tc.includeToken { - auth.Token = bascule.NewToken("test", "princ", - bascule.NewAttributes( - buildDummyAttributes(CapabilityKeys(), capabilities))) - } - if tc.includeAuth { - ctx = bascule.WithAuthentication(ctx, auth) - } - c := CapabilitiesValidator{ - Checker: ConstEndpointCheck("it's a match"), - ErrorOut: tc.errorOut, - } - err := c.Check(ctx, bascule.NewToken("", "", nil)) - if tc.errExpected { - assert.NotNil(err) - return - } - assert.Nil(err) - }) - } -} - -func TestCapabilitiesValidatorCheckAuthentication(t *testing.T) { - capabilities := []string{ - "test", - "a", - "joweiafuoiuoiwauf", - "it's a match", - } - pv := ParsedValues{} - tests := []struct { - description string - includeToken bool - includeMethod bool - includeAttributes bool - includeURL bool - checker EndpointChecker - expectedErr error - }{ - { - description: "Success", - includeMethod: true, - includeAttributes: true, - includeURL: true, - checker: ConstEndpointCheck("it's a match"), - expectedErr: nil, - }, - { - description: "No Token Error", - expectedErr: ErrNoToken, - }, - { - description: "No Method Error", - includeToken: true, - expectedErr: ErrNoMethod, - }, - { - description: "Get Capabilities Error", - includeToken: true, - includeMethod: true, - expectedErr: ErrNilAttributes, - }, - { - description: "No URL Error", - includeAttributes: true, - includeMethod: true, - expectedErr: ErrNoURL, - }, - { - description: "Check Capabilities Error", - includeAttributes: true, - includeMethod: true, - includeURL: true, - checker: AlwaysEndpointCheck(false), - expectedErr: ErrNoValidCapabilityFound, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - c := CapabilitiesValidator{ - Checker: tc.checker, - } - a := bascule.Authentication{} - if tc.includeToken { - a.Token = bascule.NewToken("", "", nil) - } - if tc.includeAttributes { - a.Token = bascule.NewToken("test", "princ", - bascule.NewAttributes( - buildDummyAttributes(CapabilityKeys(), capabilities))) - } - if tc.includeURL { - goodURL, err := url.Parse("/test") - require.Nil(err) - a.Request = bascule.Request{ - URL: goodURL, - } - } - if tc.includeMethod { - a.Request.Method = "GET" - } - err := c.CheckAuthentication(a, pv) - if tc.expectedErr == nil { - assert.NoError(err) - return - } - assert.True(errors.Is(err, tc.expectedErr), - fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", - err, tc.expectedErr), - ) - // every error should be a reasoner. - var r Reasoner - assert.True(errors.As(err, &r), "expected error to be a Reasoner") - }) - } -} - -func TestCheckCapabilities(t *testing.T) { - capabilities := []string{ - "test", - "a", - "joweiafuoiuoiwauf", - "it's a match", - } - - tests := []struct { - description string - goodCapability string - expectedErr error - }{ - { - description: "Success", - goodCapability: "it's a match", - }, - { - description: "No Capability Found Error", - expectedErr: ErrNoValidCapabilityFound, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - c := CapabilitiesValidator{ - Checker: ConstEndpointCheck(tc.goodCapability), - } - err := c.checkCapabilities(capabilities, "", "") - if tc.expectedErr == nil { - assert.NoError(err) - return - } - assert.True(errors.Is(err, tc.expectedErr), - fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", - err, tc.expectedErr), - ) - // every error should be a reasoner. - var r Reasoner - assert.True(errors.As(err, &r), "expected error to be a Reasoner") - }) - } -} - -func TestGetCapabilities(t *testing.T) { - type badInt int - goodKeyVal := []string{"cap1", "cap2"} - emptyVal := []string{} - tests := []struct { - description string - nilAttributes bool - missingAttribute bool - key []string - keyValue interface{} - expectedVals []string - expectedErr error - }{ - { - description: "Success", - key: []string{"test", "a", "b"}, - keyValue: goodKeyVal, - expectedVals: goodKeyVal, - expectedErr: nil, - }, - { - description: "Success with default key", - keyValue: goodKeyVal, - expectedVals: goodKeyVal, - expectedErr: nil, - }, - { - description: "Nil Attributes Error", - nilAttributes: true, - expectedVals: emptyVal, - expectedErr: ErrNilAttributes, - }, - { - description: "No Attribute Error", - missingAttribute: true, - expectedVals: emptyVal, - expectedErr: ErrGettingCapabilities, - }, - { - description: "Nil Capabilities Error", - keyValue: nil, - expectedVals: emptyVal, - expectedErr: ErrCapabilityNotStringSlice, - }, - { - description: "Non List Capabilities Error", - keyValue: struct{ string }{"abcd"}, - expectedVals: emptyVal, - expectedErr: ErrCapabilityNotStringSlice, - }, - { - description: "Non String List Capabilities Error", - keyValue: []badInt{0, 1, 2}, - expectedVals: emptyVal, - expectedErr: ErrCapabilityNotStringSlice, - }, - { - description: "Empty Capabilities Error", - keyValue: emptyVal, - expectedVals: emptyVal, - expectedErr: ErrNoVals, - }, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - if tc.key == nil { - tc.key = CapabilityKeys() - } - m := buildDummyAttributes(tc.key, tc.keyValue) - if tc.missingAttribute { - m = map[string]interface{}{} - } - attributes := bascule.NewAttributes(m) - if tc.nilAttributes { - attributes = nil - } - vals, err := getCapabilities(attributes, tc.key) - assert.Equal(tc.expectedVals, vals) - if tc.expectedErr == nil { - assert.NoError(err) - return - } - assert.True(errors.Is(err, tc.expectedErr), - fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", - err, tc.expectedErr), - ) - // every error should be a reasoner. - var r Reasoner - assert.True(errors.As(err, &r), "expected error to be a Reasoner") - }) - } -} - -func TestNewCapabilitiesValidator(t *testing.T) { - require := require.New(t) - goodCheck, err := NewRegexEndpointCheck("", "") - require.Nil(err) - es := []string{"abc", "def", `\M`, "adbecf"} - goodEndpoints := []*regexp.Regexp{ - regexp.MustCompile(es[0]), - regexp.MustCompile(es[1]), - regexp.MustCompile(es[3]), - } - _, err = regexp.Compile(es[2]) - require.Error(err) - - tests := []struct { - description string - config CapabilitiesValidatorConfig - expectedOut CapabilitiesCheckerOut - expectedErr error - }{ - { - description: "Success", - config: CapabilitiesValidatorConfig{ - Type: "enforce", - EndpointBuckets: es, - }, - expectedOut: CapabilitiesCheckerOut{ - Checker: CapabilitiesValidator{Checker: goodCheck}, - Options: []MetricOption{ - WithEndpoints(goodEndpoints), - }, - }, - }, - { - description: "Monitor success", - config: CapabilitiesValidatorConfig{ - Type: "monitor", - EndpointBuckets: es, - }, - expectedOut: CapabilitiesCheckerOut{ - Checker: CapabilitiesValidator{Checker: goodCheck}, - Options: []MetricOption{ - WithEndpoints(goodEndpoints), - MonitorOnly(), - }, - }, - }, - { - description: "Disabled success", - }, - { - description: "New check error", - config: CapabilitiesValidatorConfig{ - Type: "enforce", - Prefix: `\M`, - }, - expectedErr: errors.New("failed to compile prefix"), - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - out, err := NewCapabilitiesValidator(tc.config) - assert.Equal(tc.expectedOut.Checker, out.Checker) - assert.Equal(len(tc.expectedOut.Options), len(out.Options)) - if tc.expectedErr == nil { - assert.NoError(err) - return - } - assert.True(strings.Contains(err.Error(), tc.expectedErr.Error()), - fmt.Errorf("error [%v] doesn't contain error [%v]", - err, tc.expectedErr), - ) - }) - } -} - -func buildDummyAttributes(keyPath []string, val interface{}) map[string]interface{} { - keyLen := len(keyPath) - if keyLen == 0 { - return nil - } - m := map[string]interface{}{keyPath[keyLen-1]: val} - // we want to move out from the inner most map. - for i := keyLen - 2; i >= 0; i-- { - m = map[string]interface{}{keyPath[i]: m} - } - return m -} diff --git a/basculechecks/doc.go b/basculechecks/doc.go deleted file mode 100644 index 349d8cb..0000000 --- a/basculechecks/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -/* -Package basculechecks provides bascule validators for JWT capability checking. -*/ - -package basculechecks diff --git a/basculechecks/endpointchecks.go b/basculechecks/endpointchecks.go deleted file mode 100644 index 7d8a2dd..0000000 --- a/basculechecks/endpointchecks.go +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "fmt" - "regexp" - "strings" -) - -// AlwaysEndpointCheck is a EndpointChecker that always returns either true or false. -type AlwaysEndpointCheck bool - -// Authorized returns the saved boolean value, rather than checking the -// parameters given. -func (a AlwaysEndpointCheck) Authorized(_, _, _ string) bool { - return bool(a) -} - -// Name returns the endpoint check's name. -func (a AlwaysEndpointCheck) Name() string { - if a { - return "always true" - } - return "always false" -} - -// ConstEndpointCheck is a basic EndpointChecker that determines a capability is -// authorized if it matches the ConstCheck's string. -type ConstEndpointCheck string - -// Authorized validates the capability provided against the stored string. -func (c ConstEndpointCheck) Authorized(capability, _, _ string) bool { - return string(c) == capability -} - -// Name returns the endpoint check's name. -func (c ConstEndpointCheck) Name() string { - return "const" -} - -// RegexEndpointCheck uses a regular expression to validate an endpoint and -// method provided in a capability against the endpoint hit and method used for -// the request. -type RegexEndpointCheck struct { - prefixToMatch *regexp.Regexp - acceptAllMethod string -} - -// NewRegexEndpointCheck creates an object that implements the EndpointChecker -// interface. It takes a prefix that is expected at the beginning of a -// capability and a string that, if provided in the capability, authorizes all -// methods for that endpoint. After the prefix, the RegexEndpointCheck expects -// there to be an endpoint regular expression and an http method - separated by -// a colon. The expected format of a capability is: : -// Note, the endpoint url path and the capabilities substring (used for authorization) -// will be normalized to have a leading `/` if missing. -func NewRegexEndpointCheck(prefix string, acceptAllMethod string) (RegexEndpointCheck, error) { - matchPrefix, err := regexp.Compile("^" + prefix + "(.+):(.+?)$") - if err != nil { - return RegexEndpointCheck{}, fmt.Errorf("failed to compile prefix [%v]: %w", prefix, err) - } - - r := RegexEndpointCheck{ - prefixToMatch: matchPrefix, - acceptAllMethod: acceptAllMethod, - } - return r, nil -} - -// Authorized checks the capability against the endpoint hit and method used. If -// the capability has the correct prefix and is meant to be used with the method -// provided to access the endpoint provided, it is authorized. -func (r RegexEndpointCheck) Authorized(capability string, urlToMatch string, methodToMatch string) bool { - matches := r.prefixToMatch.FindStringSubmatch(capability) - - if matches == nil || len(matches) < 2 { - return false - } - - method := matches[2] - if method != r.acceptAllMethod && method != strings.ToLower(methodToMatch) { - return false - } - - re, err := regexp.Compile(urlPathNormalization(matches[1])) //url regex that capability grants access to - if err != nil { - return false - } - - matchIdxs := re.FindStringIndex(urlPathNormalization(urlToMatch)) - if matchIdxs == nil || matchIdxs[0] != 0 { - return false - } - - return true -} - -// Name returns the endpoint check's name. -func (e RegexEndpointCheck) Name() string { - return "regex" -} - -// urlPathNormalization returns an url path with a leading `/` if missing, -// otherwise the same unmodified url path is returned. -func urlPathNormalization(url string) string { - if url[0] == '/' { - return url - } - - return "/" + url -} diff --git a/basculechecks/endpointchecks_test.go b/basculechecks/endpointchecks_test.go deleted file mode 100644 index e4ed2d2..0000000 --- a/basculechecks/endpointchecks_test.go +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var _ EndpointChecker = ConstEndpointCheck("test") - -func TestAlwaysEndpointCheck(t *testing.T) { - assert := assert.New(t) - alwaysTrue := AlwaysEndpointCheck(true) - assert.True(alwaysTrue.Authorized("a", "b", "c")) - assert.Equal("always true", alwaysTrue.Name()) - alwaysFalse := AlwaysEndpointCheck(false) - assert.False(alwaysFalse.Authorized("a", "b", "c")) - assert.Equal("always false", alwaysFalse.Name()) -} - -func TestConstCheck(t *testing.T) { - tests := []struct { - description string - capability string - okExpected bool - }{ - { - description: "Success", - capability: "perfectmatch", - okExpected: true, - }, - { - description: "Not a Match", - capability: "meh", - okExpected: false, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - c := ConstEndpointCheck("perfectmatch") - ok := c.Authorized(tc.capability, "ignored1", "ignored2") - assert.Equal(tc.okExpected, ok) - assert.Equal("const", c.Name()) - }) - } -} - -func TestRegexEndpointCheckEndpointChecker(t *testing.T) { - assert := assert.New(t) - var v interface{} - v, err := NewRegexEndpointCheck("test", "") - assert.Nil(err) - _, ok := v.(EndpointChecker) - assert.True(ok) -} -func TestNewRegexEndpointCheck(t *testing.T) { - e, err := NewRegexEndpointCheck(`\M`, "") - assert := assert.New(t) - assert.Empty(e) - assert.NotNil(err) -} - -func TestRegexEndpointCheck(t *testing.T) { - tests := []struct { - description string - prefix string - acceptAllMethod string - capability string - url string - method string - okExpected bool - }{ - { - description: "Success", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: "a:b:c:.*:get", - url: "/test/ffff//", - method: "get", - okExpected: true, - }, - { - description: "No Match Error", - prefix: "a:b:c:", - capability: "a:.*:get", - method: "get", - }, - { - description: "Wrong Method Error", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: "a:b:c:.*:get", - method: "post", - }, - { - description: "Regex Doesn't Compile Error", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: `a:b:c:\M:get`, - method: "get", - }, - { - description: "URL Doesn't Match Capability Error", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: "a:b:c:[A..Z]+:get", - url: "1111", - method: "get", - }, - { - description: "URL Capability Match Wrong Location Error", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: "a:b:c:[A..Z]+:get", - url: "11AAAAA", - method: "get", - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - r, err := NewRegexEndpointCheck(tc.prefix, tc.acceptAllMethod) - require.Nil(err) - require.NotEmpty(r) - assert.Equal("regex", r.Name()) - ok := r.Authorized(tc.capability, tc.url, tc.method) - assert.Equal(tc.okExpected, ok) - }) - } -} diff --git a/basculechecks/errors.go b/basculechecks/errors.go deleted file mode 100644 index 9cddc9c..0000000 --- a/basculechecks/errors.go +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -// Reasoner is an error that provides a failure reason to use as a value for a -// metric label. -type Reasoner interface { - Reason() string -} - -type errWithReason struct { - err error - reason string -} - -// Error returns the error string. -func (e errWithReason) Error() string { - return e.err.Error() -} - -// Reason returns the reason string for the error. This is intended to be used -// in a metric label. -func (e errWithReason) Reason() string { - return e.reason -} - -// Unwrap returns the error stored. -func (e errWithReason) Unwrap() error { - return e.err -} diff --git a/basculechecks/errors_test.go b/basculechecks/errors_test.go deleted file mode 100644 index a828914..0000000 --- a/basculechecks/errors_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestErrorWithReason(t *testing.T) { - assert := assert.New(t) - testErr := errors.New("test err") - e := errWithReason{ - err: testErr, - reason: "who knows", - } - var r Reasoner = e - assert.Equal("who knows", r.Reason()) - - var ee error = e - assert.Equal("test err", ee.Error()) - - assert.Equal(testErr, e.Unwrap()) -} diff --git a/basculechecks/keys.go b/basculechecks/keys.go deleted file mode 100644 index c88a7e9..0000000 --- a/basculechecks/keys.go +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -var ( - capabilityKeys = []string{"capabilities"} - partnerKeys = []string{"allowedResources", "allowedPartners"} -) - -// CapabilityKeys is the default location of capabilities in a bascule Token's -// Attributes. -func CapabilityKeys() []string { - return capabilityKeys -} - -// PartnerKeys is the location of the list of allowed partners in a bascule -// Token's Attributes. -func PartnerKeys() []string { - return partnerKeys -} diff --git a/basculechecks/metricoptions.go b/basculechecks/metricoptions.go deleted file mode 100644 index ecc1eb3..0000000 --- a/basculechecks/metricoptions.go +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import "regexp" - -const ( - defaultServer = "primary" -) - -// MetricOption provides a way to configure a MetricValidator. -type MetricOption func(*MetricValidator) - -// MonitorOnly modifies the MetricValidator to never return an error when the -// Check() function is called. -func MonitorOnly() MetricOption { - return func(m *MetricValidator) { - m.errorOut = false - } -} - -// WithServer provides the server name to be used in the metric label. -func WithServer(s string) MetricOption { - return func(m *MetricValidator) { - if len(s) > 0 { - m.server = s - } - } -} - -// WithEndpoints provides the endpoint buckets to use in the endpoint metric -// label. The endpoint bucket found for a request is also passed to the -// CapabilitiesChecker. -func WithEndpoints(e []*regexp.Regexp) MetricOption { - return func(m *MetricValidator) { - if len(e) != 0 { - m.endpoints = e - } - } -} - -// NewMetricValidator creates a MetricValidator given a CapabilitiesChecker, -// measures, and options to configure it. The checker and measures cannot be -// nil. -func NewMetricValidator(checker CapabilitiesChecker, measures *AuthCapabilityCheckMeasures, options ...MetricOption) (*MetricValidator, error) { - if checker == nil { - return nil, ErrNilChecker - } - - if measures == nil { - return nil, ErrNilMeasures - } - - m := MetricValidator{ - c: checker, - measures: measures, - errorOut: true, - server: defaultServer, - } - - for _, o := range options { - if o != nil { - o(&m) - } - } - return &m, nil -} diff --git a/basculechecks/metricoptions_test.go b/basculechecks/metricoptions_test.go deleted file mode 100644 index cd42ff4..0000000 --- a/basculechecks/metricoptions_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "errors" - "fmt" - "regexp" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewMetricValidator(t *testing.T) { - c := &CapabilitiesValidator{} - m := &AuthCapabilityCheckMeasures{} - e := []*regexp.Regexp{regexp.MustCompile(".*")} - s := "testserverrr" - tests := []struct { - description string - checker CapabilitiesChecker - measures *AuthCapabilityCheckMeasures - options []MetricOption - expectedValidator *MetricValidator - expectedErr error - }{ - { - description: "Success", - checker: c, - measures: m, - options: []MetricOption{ - MonitorOnly(), - WithServer(s), - WithServer(""), - WithEndpoints(e), - WithEndpoints(nil), - }, - expectedValidator: &MetricValidator{ - c: c, - measures: m, - server: s, - endpoints: e, - errorOut: false, - }, - }, - { - description: "Success with defaults", - checker: c, - measures: m, - expectedValidator: &MetricValidator{ - c: c, - measures: m, - errorOut: true, - server: defaultServer, - }, - }, - { - description: "Nil Checker Error", - measures: m, - expectedErr: ErrNilChecker, - }, - { - description: "Nil Measures Error", - checker: c, - expectedErr: ErrNilMeasures, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - m, err := NewMetricValidator(tc.checker, tc.measures, tc.options...) - assert.Equal(tc.expectedValidator, m) - assert.True(errors.Is(err, tc.expectedErr), - fmt.Errorf("error [%v] doesn't match expected error [%v]", - err, tc.expectedErr), - ) - }) - } -} diff --git a/basculechecks/metrics.go b/basculechecks/metrics.go deleted file mode 100644 index ddcefaa..0000000 --- a/basculechecks/metrics.go +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/xmidt-org/touchstone" - "go.uber.org/fx" -) - -// Names for our metrics -const ( - AuthCapabilityCheckOutcome = "auth_capability_check" -) - -// labels -const ( - OutcomeLabel = "outcome" - ReasonLabel = "reason" - ClientIDLabel = "clientid" - EndpointLabel = "endpoint" - MethodLabel = "method" - PartnerIDLabel = "partnerid" - ServerLabel = "server" -) - -// label values -const ( - // outcomes - RejectedOutcome = "rejected" - AcceptedOutcome = "accepted" - // reasons - UnknownReason = "unknown" - TokenMissing = "auth_missing" - UndeterminedPartnerID = "undetermined_partner_ID" - UndeterminedCapabilities = "undetermined_capabilities" - EmptyCapabilitiesList = "empty_capabilities_list" - MissingValues = "auth_is_missing_values" - NoEndpointChecker = "no_capability_checker" - NoCapabilitiesMatch = "no_capabilities_match" - EmptyParsedURL = "empty_parsed_URL" - // partners - NonePartner = "none" - WildcardPartner = "wildcard" - ManyPartner = "many" - // endpoints - NoneEndpoint = "no_endpoints" - NotRecognizedEndpoint = "not_recognized" -) - -// help messages -const ( - capabilityCheckHelpMsg = "Counter for the capability checker, providing outcome information by client, partner, and endpoint" -) - -// ProvideMetrics provides the metrics relevant to this package as uber/fx -// options. -func ProvideMetrics() fx.Option { - return fx.Options( - touchstone.CounterVec(prometheus.CounterOpts{ - Name: AuthCapabilityCheckOutcome, - Help: capabilityCheckHelpMsg, - ConstLabels: nil, - }, ServerLabel, OutcomeLabel, ReasonLabel, ClientIDLabel, - PartnerIDLabel, EndpointLabel, MethodLabel), - ) -} - -// AuthCapabilityCheckMeasures describes the defined metrics that will be used -// by clients. -type AuthCapabilityCheckMeasures struct { - fx.In - - CapabilityCheckOutcome *prometheus.CounterVec `name:"auth_capability_check"` -} diff --git a/basculechecks/metricvalidator.go b/basculechecks/metricvalidator.go deleted file mode 100644 index ba12098..0000000 --- a/basculechecks/metricvalidator.go +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "context" - "errors" - "fmt" - "regexp" - - "github.com/prometheus/client_golang/prometheus" - "github.com/spf13/cast" - "github.com/xmidt-org/bascule" - "go.uber.org/fx" -) - -var ( - ErrNilChecker = errors.New("capabilities checker cannot be nil") - ErrNilMeasures = errors.New("measures cannot be nil") - ErrGettingPartnerIDs = errWithReason{ - err: errors.New("couldn't get partner IDs from attributes"), - reason: UndeterminedPartnerID, - } - ErrPartnerIDsNotStringSlice = errWithReason{ - err: errors.New("expected a string slice"), - reason: UndeterminedPartnerID, - } -) - -// CapabilitiesChecker is an object that can determine if a request is -// authorized given a bascule.Authentication object. If it's not authorized, an -// error is given for logging and metrics. -type CapabilitiesChecker interface { - CheckAuthentication(auth bascule.Authentication, vals ParsedValues) error -} - -// CapabilitiesCheckerOut is a struct returned by New() functions that help to -// create a CapabilitiesChecker and as a byproduct also create some -// MetricOptions. -type CapabilitiesCheckerOut struct { - fx.Out - Checker CapabilitiesChecker - Options []MetricOption `group:"bascule_capability_options,flatten"` -} - -// ParsedValues are values determined from the bascule Authentication. -type ParsedValues struct { - // Endpoint is the string representation of a regular expression that - // matches the URL for the request. The main benefit of this string is it - // most likely won't include values that change from one request to the next - // (ie, device ID). - Endpoint string -} - -type metricValues struct { - method string - endpoint string - partnerID string - client string -} - -// MetricValidatorIn contains the objects needed to create a MetricValidator, -// wired with uber fx. -type MetricValidatorIn struct { - fx.In - Checker CapabilitiesChecker - Measures AuthCapabilityCheckMeasures - Options []MetricOption `group:"bascule_capability_options"` -} - -// MetricValidator determines if a request is authorized and then updates a -// metric to show those results. -type MetricValidator struct { - c CapabilitiesChecker - measures *AuthCapabilityCheckMeasures - endpoints []*regexp.Regexp - errorOut bool - server string -} - -// Check is a function for authorization middleware. The function parses the -// information needed for the CapabilitiesChecker, calls it to determine if the -// request is authorized, and maintains the results in a metric. The function -// can mark the request as unauthorized or only update the metric and allow the -// request, depending on configuration. This allows for monitoring before being -// more strict with authorization. -func (m MetricValidator) Check(ctx context.Context, _ bascule.Token) error { - auth, ok := bascule.FromContext(ctx) - if !ok { - m.measures.CapabilityCheckOutcome.With(prometheus.Labels{ - ServerLabel: m.server, - OutcomeLabel: m.failureOutcome(), - ReasonLabel: TokenMissing, - ClientIDLabel: "", - PartnerIDLabel: "", - EndpointLabel: "", - MethodLabel: "", - }).Add(1) - return m.errReturn(ErrNoAuth) - } - - l, err := m.prepMetrics(auth) - labels := prometheus.Labels{ - ServerLabel: m.server, - ClientIDLabel: l.client, - PartnerIDLabel: l.partnerID, - EndpointLabel: l.endpoint, - MethodLabel: l.method, - OutcomeLabel: AcceptedOutcome, - ReasonLabel: "", - } - if err != nil { - labels[OutcomeLabel] = m.failureOutcome() - labels[ReasonLabel] = UnknownReason - var r Reasoner - if errors.As(err, &r) { - labels[ReasonLabel] = r.Reason() - } - m.measures.CapabilityCheckOutcome.With(labels).Add(1) - return m.errReturn(err) - } - - v := ParsedValues{ - Endpoint: l.endpoint, - } - - err = m.c.CheckAuthentication(auth, v) - if err != nil { - labels[OutcomeLabel] = m.failureOutcome() - labels[ReasonLabel] = UnknownReason - var r Reasoner - if errors.As(err, &r) { - labels[ReasonLabel] = r.Reason() - } - m.measures.CapabilityCheckOutcome.With(labels).Add(1) - return m.errReturn(fmt.Errorf("endpoint auth for %v on %v failed: %v", - auth.Request.Method, auth.Request.URL.EscapedPath(), err)) - } - - m.measures.CapabilityCheckOutcome.With(labels).Add(1) - return nil -} - -// prepMetrics gathers the information needed for metric label information. It -// gathers the client ID, partnerID, and endpoint (bucketed) for more information -// on the metric when a request is unauthorized. -func (m MetricValidator) prepMetrics(auth bascule.Authentication) (metricValues, error) { - v := metricValues{} - if auth.Token == nil { - return v, ErrNoToken - } - v.client = auth.Token.Principal() - if len(auth.Request.Method) == 0 { - return v, ErrNoMethod - } - v.method = auth.Request.Method - if auth.Token.Attributes() == nil { - return v, ErrNilAttributes - } - - partnerVal, ok := bascule.GetNestedAttribute(auth.Token.Attributes(), PartnerKeys()...) - if !ok { - err := fmt.Errorf("%w using keys %v", ErrGettingPartnerIDs, PartnerKeys()) - return v, err - } - partnerIDs, err := cast.ToStringSliceE(partnerVal) - if err != nil { - err = fmt.Errorf("%w for partner IDs \"%v\": %v", - ErrPartnerIDsNotStringSlice, partnerVal, err) - return v, err - } - v.partnerID = DeterminePartnerMetric(partnerIDs) - - if auth.Request.URL == nil { - return v, ErrNoURL - } - escapedURL := auth.Request.URL.EscapedPath() - v.endpoint = determineEndpointMetric(m.endpoints, escapedURL) - return v, nil -} - -func (m MetricValidator) failureOutcome() string { - // if we actually error out, the outcome is the request being rejected - if m.errorOut { - return RejectedOutcome - } - // if we're not supposed to error out, the outcome should be accepted on failure - return AcceptedOutcome -} - -func (m MetricValidator) errReturn(err error) error { - // if we actually error out, the error should be returned. - if m.errorOut { - return err - } - // if we're not supposed to error out, the error is suppressed. - return nil -} diff --git a/basculechecks/metricvalidator_test.go b/basculechecks/metricvalidator_test.go deleted file mode 100644 index c49a0c7..0000000 --- a/basculechecks/metricvalidator_test.go +++ /dev/null @@ -1,414 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "context" - "errors" - "fmt" - "net/url" - "regexp" - "testing" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/touchstone/touchtest" -) - -func TestMetricValidatorCheck(t *testing.T) { - goodURL, err := url.Parse("/test") - require.Nil(t, err) - capabilities := []string{ - "test", - "a", - "joweiafuoiuoiwauf", - "it's a match", - } - goodMap := buildDummyAttributes(CapabilityKeys(), capabilities) - goodMap["allowedResources"] = map[string]interface{}{ - "allowedPartners": []string{"meh"}, - } - goodAttributes := bascule.NewAttributes(goodMap) - cErr := errWithReason{ - err: errors.New("check test error"), - reason: NoCapabilitiesMatch, - } - - tests := []struct { - description string - includeAuth bool - attributes bascule.Attributes - checkCallExpected bool - checkErr error - errorOut bool - errExpected bool - expectedLabels prometheus.Labels - }{ - { - description: "Success", - includeAuth: true, - attributes: goodAttributes, - checkCallExpected: true, - errorOut: true, - expectedLabels: prometheus.Labels{ - ServerLabel: "testserver", - PartnerIDLabel: "meh", - OutcomeLabel: AcceptedOutcome, - ReasonLabel: "", - }, - }, - { - description: "Include Auth Error", - errorOut: true, - errExpected: true, - expectedLabels: prometheus.Labels{ - ServerLabel: "testserver", - OutcomeLabel: RejectedOutcome, - ReasonLabel: TokenMissing, - ClientIDLabel: "", - PartnerIDLabel: "", - EndpointLabel: "", - MethodLabel: "", - }, - }, - { - description: "Include Auth Suppressed Error", - errorOut: false, - expectedLabels: prometheus.Labels{ - ServerLabel: "testserver", - OutcomeLabel: AcceptedOutcome, - ReasonLabel: TokenMissing, - ClientIDLabel: "", - PartnerIDLabel: "", - EndpointLabel: "", - MethodLabel: "", - }, - }, - { - description: "Prep Metrics Error", - includeAuth: true, - attributes: nil, - errorOut: true, - errExpected: true, - expectedLabels: prometheus.Labels{ - ServerLabel: "testserver", - OutcomeLabel: RejectedOutcome, - ReasonLabel: MissingValues, - ClientIDLabel: "princ", - PartnerIDLabel: "", - EndpointLabel: "", - MethodLabel: "GET", - }, - }, - { - description: "Prep Metrics Suppressed Error", - includeAuth: true, - attributes: nil, - errorOut: false, - expectedLabels: prometheus.Labels{ - ServerLabel: "testserver", - OutcomeLabel: AcceptedOutcome, - ReasonLabel: MissingValues, - ClientIDLabel: "princ", - PartnerIDLabel: "", - EndpointLabel: "", - MethodLabel: "GET", - }, - }, - { - description: "Check Error", - includeAuth: true, - attributes: goodAttributes, - checkCallExpected: true, - checkErr: cErr, - errorOut: true, - errExpected: true, - expectedLabels: prometheus.Labels{ - ServerLabel: "testserver", - OutcomeLabel: RejectedOutcome, - ReasonLabel: NoCapabilitiesMatch, - PartnerIDLabel: "meh", - }, - }, - { - description: "Check Suppressed Error", - includeAuth: true, - attributes: goodAttributes, - checkCallExpected: true, - checkErr: cErr, - errorOut: false, - expectedLabels: prometheus.Labels{ - ServerLabel: "testserver", - OutcomeLabel: AcceptedOutcome, - ReasonLabel: NoCapabilitiesMatch, - PartnerIDLabel: "meh", - }, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - testAssert := touchtest.New(t) - expectedRegistry := prometheus.NewPedanticRegistry() - expectedCounter := prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "testCounter", - Help: "testCounter", - }, - []string{ServerLabel, OutcomeLabel, ReasonLabel, ClientIDLabel, - PartnerIDLabel, EndpointLabel, MethodLabel}, - ) - expectedRegistry.Register(expectedCounter) - actualRegistry := prometheus.NewPedanticRegistry() - - ctx := context.Background() - auth := bascule.Authentication{ - Token: bascule.NewToken("test", "princ", tc.attributes), - Request: bascule.Request{ - URL: goodURL, - Method: "GET", - }, - } - if tc.includeAuth { - ctx = bascule.WithAuthentication(ctx, auth) - } - mockCapabilitiesChecker := new(mockCapabilitiesChecker) - if tc.checkCallExpected { - tc.expectedLabels[EndpointLabel] = NoneEndpoint - tc.expectedLabels[MethodLabel] = auth.Request.Method - tc.expectedLabels[ClientIDLabel] = auth.Token.Principal() - mockCapabilitiesChecker.On("CheckAuthentication", mock.Anything, mock.Anything). - Return(tc.checkErr).Once() - } - - mockMeasures := AuthCapabilityCheckMeasures{ - CapabilityCheckOutcome: prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "testCounter", - Help: "testCounter", - }, - []string{ServerLabel, OutcomeLabel, ReasonLabel, ClientIDLabel, - PartnerIDLabel, EndpointLabel, MethodLabel}, - ), - } - actualRegistry.MustRegister(mockMeasures.CapabilityCheckOutcome) - expectedCounter.With(tc.expectedLabels).Inc() - - m := MetricValidator{ - c: mockCapabilitiesChecker, - measures: &mockMeasures, - errorOut: tc.errorOut, - server: "testserver", - } - err := m.Check(ctx, nil) - mockCapabilitiesChecker.AssertExpectations(t) - if tc.errExpected { - assert.NotNil(err) - return - } - assert.Nil(err) - testAssert.Expect(expectedRegistry) - assert.True(testAssert.GatherAndCompare(actualRegistry)) - }) - } -} - -func TestPrepMetrics(t *testing.T) { - var ( - goodURL = "/asnkfn/aefkijeoij/aiogj" - matchingURL = "/fnvvdsjkfji/mac:12345544322345334/geigosj" - client = "special" - goodEndpoint = `/fnvvdsjkfji/.*/geigosj\b` - goodRegex = regexp.MustCompile(goodEndpoint) - unusedEndpoint = `/a/b\b` - unusedRegex = regexp.MustCompile(unusedEndpoint) - ) - - type badInt int - - tests := []struct { - description string - noPartnerID bool - partnerIDs interface{} - url string - includeToken bool - includeMethod bool - includeAttributes bool - includeURL bool - expectedMetricValues metricValues - expectedErr error - }{ - { - description: "Success", - partnerIDs: []string{"partner"}, - url: goodURL, - includeToken: true, - includeMethod: true, - includeAttributes: true, - includeURL: true, - expectedMetricValues: metricValues{ - method: "get", - endpoint: NotRecognizedEndpoint, - partnerID: "partner", - client: client, - }, - expectedErr: nil, - }, - { - description: "Success Abridged URL", - partnerIDs: []string{"partner"}, - url: matchingURL, - includeToken: true, - includeMethod: true, - includeAttributes: true, - includeURL: true, - expectedMetricValues: metricValues{ - method: "get", - endpoint: goodEndpoint, - partnerID: "partner", - client: client, - }, - expectedErr: nil, - }, - { - description: "Nil Token Error", - expectedErr: ErrNoToken, - }, - { - description: "No Method Error", - includeToken: true, - expectedMetricValues: metricValues{ - client: client, - }, - expectedErr: ErrNoMethod, - }, - { - description: "Nil Token Attributes Error", - url: goodURL, - includeToken: true, - includeMethod: true, - includeURL: true, - expectedMetricValues: metricValues{ - method: "get", - client: client, - }, - expectedErr: ErrNilAttributes, - }, - { - description: "No Partner ID Error", - noPartnerID: true, - url: goodURL, - includeToken: true, - includeMethod: true, - includeAttributes: true, - includeURL: true, - expectedMetricValues: metricValues{ - method: "get", - client: client, - }, - expectedErr: ErrGettingPartnerIDs, - }, - { - description: "Non String Slice Partner ID Error", - partnerIDs: []badInt{0, 1, 2}, - url: goodURL, - includeToken: true, - includeMethod: true, - includeAttributes: true, - includeURL: true, - expectedMetricValues: metricValues{ - method: "get", - client: client, - }, - expectedErr: ErrPartnerIDsNotStringSlice, - }, - { - description: "Non Slice Partner ID Error", - partnerIDs: struct{ string }{}, - url: goodURL, - includeToken: true, - includeMethod: true, - includeAttributes: true, - includeURL: true, - expectedMetricValues: metricValues{ - method: "get", - client: client, - }, - expectedErr: ErrPartnerIDsNotStringSlice, - }, - { - description: "Nil URL Error", - partnerIDs: []string{"partner"}, - url: goodURL, - includeToken: true, - includeMethod: true, - includeAttributes: true, - expectedMetricValues: metricValues{ - method: "get", - partnerID: "partner", - client: client, - }, - expectedErr: ErrNoURL, - }, - } - - m := MetricValidator{ - endpoints: []*regexp.Regexp{unusedRegex, goodRegex}, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - // setup auth - token := bascule.NewToken("mehType", client, nil) - if tc.includeAttributes { - a := map[string]interface{}{ - "allowedResources": map[string]interface{}{ - "allowedPartners": tc.partnerIDs, - }, - } - - if tc.noPartnerID { - a["allowedResources"] = 5 - } - attributes := bascule.NewAttributes(a) - token = bascule.NewToken("mehType", client, attributes) - } - auth := bascule.Authentication{ - Authorization: "testAuth", - Request: bascule.Request{}, - } - if tc.includeToken { - auth.Token = token - } - if tc.includeURL { - u, err := url.ParseRequestURI(tc.url) - require.Nil(err) - auth.Request.URL = u - } - if tc.includeMethod { - auth.Request.Method = "get" - } - - v, err := m.prepMetrics(auth) - assert.Equal(tc.expectedMetricValues, v) - if tc.expectedErr == nil { - assert.NoError(err) - return - } - assert.True(errors.Is(err, tc.expectedErr), - fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", - err, tc.expectedErr), - ) - // every error should be a reasoner. - var r Reasoner - assert.True(errors.As(err, &r), "expected error to be a Reasoner") - }) - } -} diff --git a/basculechecks/mocks_test.go b/basculechecks/mocks_test.go deleted file mode 100644 index 8a8cf19..0000000 --- a/basculechecks/mocks_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "github.com/stretchr/testify/mock" - "github.com/xmidt-org/bascule" -) - -type mockCapabilitiesChecker struct { - mock.Mock -} - -func (m *mockCapabilitiesChecker) CheckAuthentication(auth bascule.Authentication, v ParsedValues) error { - args := m.Called(auth, v) - return args.Error(0) -} diff --git a/basculechecks/parse.go b/basculechecks/parse.go deleted file mode 100644 index 2f5a1d0..0000000 --- a/basculechecks/parse.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "regexp" - "strings" -) - -const Wildcard = "*" - -// DeterminePartnerMetric takes a list of partners and decides what the partner -// metric label should be. -func DeterminePartnerMetric(partners []string) string { - if len(partners) < 1 { - return NonePartner - } - if len(partners) == 1 { - if partners[0] == Wildcard { - return WildcardPartner - } - return partners[0] - } - for _, partner := range partners { - if partner == Wildcard { - return WildcardPartner - } - } - return ManyPartner -} - -// determineEndpointMetric takes a list of regular expressions and applies them -// to the url of the request to decide what the endpoint metric label should be. -func determineEndpointMetric(endpoints []*regexp.Regexp, urlHit string) string { - if len(endpoints) == 0 { - return NoneEndpoint - } - for _, r := range endpoints { - idxs := r.FindStringIndex(urlHit) - if len(idxs) == 0 { - continue - } - if idxs[0] == 0 { - return strings.ReplaceAll(r.String(), " ", "_") - } - } - return NotRecognizedEndpoint -} diff --git a/basculechecks/parse_test.go b/basculechecks/parse_test.go deleted file mode 100644 index 2fd5170..0000000 --- a/basculechecks/parse_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "regexp" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDeterminePartnerMetric(t *testing.T) { - tests := []struct { - description string - partnersInput []string - expectedResult string - }{ - { - description: "No Partners", - expectedResult: NonePartner, - }, - { - description: "one wildcard", - partnersInput: []string{Wildcard}, - expectedResult: WildcardPartner, - }, - { - description: "one partner", - partnersInput: []string{"TestPartner"}, - expectedResult: "TestPartner", - }, - { - description: "many partners", - partnersInput: []string{"partner1", "partner2", "partner3"}, - expectedResult: ManyPartner, - }, - { - description: "many partners with wildcard", - partnersInput: []string{"partner1", "partner2", "partner3", Wildcard}, - expectedResult: WildcardPartner, - }, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - partner := DeterminePartnerMetric(tc.partnersInput) - assert.Equal(tc.expectedResult, partner) - }) - } -} - -func TestDetermineEndpointMetric(t *testing.T) { - var ( - goodURL = "/asnkfn/aefkijeoij/aiogj" - matchingURL = "/fnvvds jkfji/mac:12345544322345334/geigosj" - matchingEndpoint = `/fnvvds jkfji/.*/geigosj\b` - matchingRegex = regexp.MustCompile(matchingEndpoint) - matchingParsed = `/fnvvds_jkfji/.*/geigosj\b` - unusedEndpoint = `/a/b\b` - unusedRegex = regexp.MustCompile(unusedEndpoint) - ) - - tests := []struct { - description string - endpoints []*regexp.Regexp - u string - expectedEndpoint string - }{ - { - description: "No Endpoints", - u: goodURL, - expectedEndpoint: NoneEndpoint, - }, - { - description: "Endpoint Not Recognized", - endpoints: []*regexp.Regexp{unusedRegex, matchingRegex}, - u: goodURL, - expectedEndpoint: NotRecognizedEndpoint, - }, - { - description: "Endpoint Matched", - endpoints: []*regexp.Regexp{unusedRegex, matchingRegex}, - u: matchingURL, - expectedEndpoint: matchingParsed, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - endpoint := determineEndpointMetric(tc.endpoints, tc.u) - assert.Equal(tc.expectedEndpoint, endpoint) - }) - } -} diff --git a/basculechecks/provide.go b/basculechecks/provide.go deleted file mode 100644 index 52b583f..0000000 --- a/basculechecks/provide.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "github.com/xmidt-org/bascule" - "go.uber.org/fx" -) - -// ProvideMetricValidator is an uber fx Provide() function that builds a -// MetricValidator given the dependencies needed. -func ProvideMetricValidator(optional bool) fx.Option { - return fx.Provide( - fx.Annotated{ - Name: "bascule_validator_capabilities", - Target: func(in MetricValidatorIn) (bascule.Validator, error) { - if optional && in.Checker == nil { - return nil, nil - } - return NewMetricValidator(in.Checker, &in.Measures, in.Options...) - }, - }, - ) -} - -// ProvideCapabilitiesMapValidator is an uber fx Provide() function that builds -// a MetricValidator that uses a CapabilitiesMap and ConstChecks, using the -// configuration found at the key provided. -func ProvideCapabilitiesMapValidator() fx.Option { - return fx.Options( - fx.Provide( - NewCapabilitiesMap, - ), - ProvideMetricValidator(false), - ) -} - -// ProvideRegexCapabilitiesValidator is an uber fx Provide() function that -// builds a MetricValidator that uses a CapabilitiesValidator and -// RegexEndpointCheck, using the configuration found at the key provided. -func ProvideRegexCapabilitiesValidator() fx.Option { - return fx.Options( - fx.Provide( - NewCapabilitiesValidator, - ), - ProvideMetricValidator(true), - ) -} diff --git a/basculechecks/provide_test.go b/basculechecks/provide_test.go deleted file mode 100644 index 8092a69..0000000 --- a/basculechecks/provide_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/touchstone" - "go.uber.org/fx" -) - -func TestProvideMetricValidator(t *testing.T) { - type In struct { - fx.In - V bascule.Validator `name:"bascule_validator_capabilities"` - } - tests := []struct { - description string - optional bool - expectedErr error - }{ - { - description: "Optional Success", - optional: true, - expectedErr: nil, - }, - { - description: "Required Failure", - optional: false, - expectedErr: ErrNilChecker, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - var result bascule.Validator - app := fx.New( - touchstone.Provide(), - ProvideMetrics(), - fx.Provide( - func() CapabilitiesChecker { - return nil - }, - ), - ProvideMetricValidator(tc.optional), - fx.Invoke( - func(in In) { - result = in.V - }, - ), - ) - app.Start(context.Background()) - defer app.Stop(context.Background()) - err := app.Err() - assert.Nil(result) - if tc.expectedErr == nil { - assert.NoError(err) - return - } - require.Error(err) - assert.True(strings.Contains(err.Error(), tc.expectedErr.Error()), - fmt.Errorf("error [%v] doesn't contain error [%v]", - err, tc.expectedErr), - ) - }) - } -} diff --git a/basculechecks/validators.go b/basculechecks/validators.go deleted file mode 100644 index 03c2a70..0000000 --- a/basculechecks/validators.go +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "context" - "errors" - "fmt" - - "github.com/xmidt-org/bascule" -) - -// AllowAll returns a Validator that never returns an error. -func AllowAll() bascule.ValidatorFunc { - return func(_ context.Context, _ bascule.Token) error { - return nil - } -} - -// ValidType returns a Validator that checks that the token's type is one of the -// given valid types. -func ValidType(validTypes []string) bascule.ValidatorFunc { - return func(_ context.Context, token bascule.Token) error { - tt := token.Type() - for _, vt := range validTypes { - if tt == vt { - return nil - } - } - return errors.New("invalid token type") - } -} - -// NonEmptyType returns a Validator that checks that the token's type isn't an -// empty string. -func NonEmptyType() bascule.ValidatorFunc { - return func(_ context.Context, token bascule.Token) error { - if token.Type() == "" { - return errors.New("empty token type") - } - return nil - } -} - -// NonEmptyPrincipal returns a Validator that checks that the token's Principal -// isn't an empty string. -func NonEmptyPrincipal() bascule.ValidatorFunc { - return func(_ context.Context, token bascule.Token) error { - if token.Principal() == "" { - return errors.New("empty token principal") - } - return nil - } -} - -// AttributeList returns a Validator that runs checks against the content found -// in the key given. It runs every check and returns all errors it finds. -func AttributeList(keys []string, checks ...func(context.Context, []interface{}) error) bascule.ValidatorFunc { - return func(ctx context.Context, token bascule.Token) error { - val, ok := bascule.GetNestedAttribute(token.Attributes(), keys...) - if !ok { - return fmt.Errorf("couldn't find attribute with keys %v", keys) - } - strVal, ok := val.([]interface{}) - if !ok { - return fmt.Errorf("unexpected attribute value, expected []interface{} type but received: %T", val) - } - errs := bascule.Errors{} - for _, check := range checks { - err := check(ctx, strVal) - if err != nil { - errs = append(errs, err) - } - } - if len(errs) == 0 { - return nil - } - return fmt.Errorf("attribute checks of keys %v failed: %v", keys, errs) - } -} diff --git a/basculechecks/validators_test.go b/basculechecks/validators_test.go deleted file mode 100644 index 9f9243f..0000000 --- a/basculechecks/validators_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculechecks - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/xmidt-org/bascule" -) - -func TestAllowAll(t *testing.T) { - assert := assert.New(t) - f := AllowAll() - err := f(context.Background(), bascule.NewToken("", "", bascule.NewAttributes(map[string]interface{}{}))) - assert.NoError(err) -} - -func TestValidType(t *testing.T) { - emptyAttributes := bascule.NewAttributes(map[string]interface{}{}) - assert := assert.New(t) - f := ValidType([]string{"valid", "type"}) - err := f(context.Background(), bascule.NewToken("valid", "", emptyAttributes)) - assert.NoError(err) - err = f(context.Background(), bascule.NewToken("invalid", "", emptyAttributes)) - assert.NotNil(err) -} - -func TestNonEmptyType(t *testing.T) { - emptyAttributes := bascule.NewAttributes(map[string]interface{}{}) - assert := assert.New(t) - f := NonEmptyType() - err := f(context.Background(), bascule.NewToken("type", "", emptyAttributes)) - assert.NoError(err) - err = f(context.Background(), bascule.NewToken("", "", emptyAttributes)) - assert.NotNil(err) -} - -func TestNonEmptyPrincipal(t *testing.T) { - emptyAttributes := bascule.NewAttributes(map[string]interface{}{}) - assert := assert.New(t) - f := NonEmptyPrincipal() - err := f(context.Background(), bascule.NewToken("", "principal", emptyAttributes)) - assert.NoError(err) - err = f(context.Background(), bascule.NewToken("", "", emptyAttributes)) - assert.NotNil(err) -} - -func TestAttributeList(t *testing.T) { - testErr := errors.New("test err") - failFunc := func(_ context.Context, _ []interface{}) error { - return testErr - } - successFunc := func(_ context.Context, _ []interface{}) error { - return nil - } - - assert := assert.New(t) - fGood := AttributeList([]string{"testkey", "subkey"}, successFunc) - f := AttributeList([]string{"testkey", "subkey"}, successFunc, failFunc) - - err := fGood(context.Background(), bascule.NewToken("", "", bascule.NewAttributes(map[string]interface{}{ - "testkey": map[string]interface{}{"subkey": []interface{}{"a", "b", "c"}}}))) - assert.NoError(err) - - err = fGood(context.Background(), bascule.NewToken("", "", bascule.NewAttributes(map[string]interface{}{}))) - assert.Error(err) - - err = fGood(context.Background(), bascule.NewToken("", "", bascule.NewAttributes(map[string]interface{}{"testkey": ""}))) - assert.Error(err) - - err = fGood(context.Background(), bascule.NewToken("", "", bascule.NewAttributes(map[string]interface{}{"testkey": map[string]interface{}{ - "subkey": 5555}}))) - assert.Error(err) - - err = f(context.Background(), bascule.NewToken("", "", bascule.NewAttributes(map[string]interface{}{"testkey": map[string]interface{}{ - "subkey": []interface{}{}}}))) - assert.Error(err) -} diff --git a/basculehelper/basculeHelper.go b/basculehelper/basculeHelper.go deleted file mode 100644 index 6d365ff..0000000 --- a/basculehelper/basculeHelper.go +++ /dev/null @@ -1,557 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehelper - -//Deprecated: this is a bascule helper package that uses older bascule functions from webpa-common in order to implement -//sallust and zap logger in scytale - -import ( - "context" - "errors" - "fmt" - "regexp" - "strings" - "time" - - "github.com/SermoDigital/jose/jwt" - "github.com/go-kit/kit/metrics" - "github.com/go-kit/kit/metrics/provider" - "github.com/spf13/cast" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/bascule/basculehttp" - - //nolint:staticcheck - "github.com/xmidt-org/webpa-common/v2/xmetrics" - "go.uber.org/multierr" -) - -var ( - ErrNoVals = errors.New("expected at least one value") - ErrNoAuth = errors.New("couldn't get request info: authorization not found") - ErrNoToken = errors.New("no token found in Auth") - ErrNoValidCapabilityFound = errors.New("no valid capability for endpoint") - ErrNilAttributes = errors.New("nil attributes interface") - ErrNoURL = errors.New("invalid URL found in Auth") - partnerKeys = []string{"allowedResources", "allowedPartners"} -) - -const ( - CapabilityKey = "capabilities" - - RejectedOutcome = "rejected" - AcceptedOutcome = "accepted" - // reasons - TokenMissing = "auth_missing" - UndeterminedPartnerID = "undetermined_partner_ID" - NoCapabilityChecker = "no_capability_checker" - EmptyParsedURL = "empty_parsed_URL" - AuthCapabilityCheckOutcome = "auth_capability_check" - capabilityCheckHelpMsg = "Counter for the capability checker, providing outcome information by client, partner, and endpoint" - EmptyCapabilitiesList = "empty_capabilities_list" - MissingValues = "auth_is_missing_values" - UndeterminedCapabilities = "undetermined_capabilities" - NoCapabilitiesMatch = "no_capabilities_match" - //nolint:gosec - TokenMissingValues = "auth_is_missing_values" - - //labels - OutcomeLabel = "outcome" - ReasonLabel = "reason" - ClientIDLabel = "clientid" - EndpointLabel = "endpoint" - PartnerIDLabel = "partnerid" - ServerLabel = "server" - - // Names for Auth Validation metrics - AuthValidationOutcome = "auth_validation" - NBFHistogram = "auth_from_nbf_seconds" - EXPHistogram = "auth_from_exp_seconds" - - // Help messages for Auth Validation metrics - authValidationOutcomeHelpMsg = "Counter for success and failure reason results through bascule" - nbfHelpMsg = "Difference (in seconds) between time of JWT validation and nbf (including leeway)" - expHelpMsg = "Difference (in seconds) between time of JWT validation and exp (including leeway)" -) - -// AuthCapabilityCheckMeasures describes the defined metrics that will be used by clients -type AuthCapabilityCheckMeasures struct { - CapabilityCheckOutcome metrics.Counter -} - -// NewAuthCapabilityCheckMeasures realizes desired metrics. It's intended to be used alongside Metrics() for -// our older non uber/fx applications. -func NewAuthCapabilityCheckMeasures(p provider.Provider) *AuthCapabilityCheckMeasures { - return &AuthCapabilityCheckMeasures{ - CapabilityCheckOutcome: p.NewCounter(AuthCapabilityCheckOutcome), - } -} - -// AuthCapabilitiesMetrics returns the Metrics relevant to this package targeting our older non uber/fx applications. -// To initialize the metrics, use NewAuthCapabilityCheckMeasures(). -func AuthCapabilitiesMetrics() []xmetrics.Metric { - return []xmetrics.Metric{ - { - Name: AuthCapabilityCheckOutcome, - Type: xmetrics.CounterType, - Help: capabilityCheckHelpMsg, - LabelNames: []string{OutcomeLabel, ReasonLabel, ClientIDLabel, PartnerIDLabel, EndpointLabel}, - }, - } -} - -// AuthValidationMeasures describes the defined metrics that will be used by clients -type AuthValidationMeasures struct { - NBFHistogram metrics.Histogram - ExpHistogram metrics.Histogram - ValidationOutcome metrics.Counter -} - -// NewAuthValidationMeasures realizes desired metrics. It's intended to be used alongside Metrics() for -// our older non uber/fx applications. -func NewAuthValidationMeasures(r xmetrics.Registry) *AuthValidationMeasures { - return &AuthValidationMeasures{ - ValidationOutcome: r.NewCounter(AuthValidationOutcome), - } -} - -// AuthValidationMetrics returns the Metrics relevant to this package targeting our older non uber/fx applications. -// To initialize the metrics, use NewAuthValidationMeasures(). -func AuthValidationMetrics() []xmetrics.Metric { - return []xmetrics.Metric{ - { - Name: AuthValidationOutcome, - Type: xmetrics.CounterType, - Help: authValidationOutcomeHelpMsg, - LabelNames: []string{OutcomeLabel}, - }, - { - Name: NBFHistogram, - Type: xmetrics.HistogramType, - Help: nbfHelpMsg, - Buckets: []float64{-61, -11, -2, -1, 0, 9, 60}, // defines the upper inclusive (<=) bounds - }, - { - Name: EXPHistogram, - Type: xmetrics.HistogramType, - Help: expHelpMsg, - Buckets: []float64{-61, -11, -2, -1, 0, 9, 60}, - }, - } -} - -// MetricValidator determines if a request is authorized and then updates a -// metric to show those results. -type MetricValidator struct { - C CapabilitiesChecker - Measures *AuthCapabilityCheckMeasures - Endpoints []*regexp.Regexp -} - -// CapabilitiesChecker is an object that can determine if a request is -// authorized given a bascule.Authentication object. If it's not authorized, a -// reason and error are given for logging and metrics. -type CapabilitiesChecker interface { - Check(auth bascule.Authentication, vals ParsedValues) (string, error) -} - -// ParsedValues are values determined from the bascule Authentication. -type ParsedValues struct { - // Endpoint is the string representation of a regular expression that - // matches the URL for the request. The main benefit of this string is it - // most likely won't include strings that change from one request to the - // next (ie, device ID). - Endpoint string - // Partner is a string representation of the list of partners found in the - // JWT, where: - // - any list including "*" as a partner is determined to be "wildcard". - // - when the list is <1 item, the partner is determined to be "none". - // - when the list is >1 item, the partner is determined to be "many". - // - when the list is only one item, that is the partner value. - Partner string -} - -// CreateValidator provides a function for authorization middleware. The -// function parses the information needed for the CapabilitiesChecker, calls it -// to determine if the request is authorized, and maintains the results in a -// metric. The function can actually mark the request as unauthorized or just -// update the metric and allow the request, depending on configuration. This -// allows for monitoring before being more strict with authorization. -func (m MetricValidator) CreateValidator(errorOut bool) bascule.ValidatorFunc { - return func(ctx context.Context, _ bascule.Token) error { - // if we're not supposed to error out, the outcome should be accepted on failure - failureOutcome := AcceptedOutcome - if errorOut { - // if we actually error out, the outcome is the request being rejected - failureOutcome = RejectedOutcome - } - - auth, ok := bascule.FromContext(ctx) - if !ok { - m.Measures.CapabilityCheckOutcome.With(OutcomeLabel, failureOutcome, ReasonLabel, TokenMissing, ClientIDLabel, "", PartnerIDLabel, "", EndpointLabel, "").Add(1) - if errorOut { - return ErrNoAuth - } - return nil - } - - client, partnerID, endpoint, reason, err := m.prepMetrics(auth) - labels := []string{ClientIDLabel, client, PartnerIDLabel, partnerID, EndpointLabel, endpoint} - if err != nil { - labels = append(labels, OutcomeLabel, failureOutcome, ReasonLabel, reason) - m.Measures.CapabilityCheckOutcome.With(labels...).Add(1) - if errorOut { - return err - } - return nil - } - - v := ParsedValues{ - Endpoint: endpoint, - Partner: partnerID, - } - - reason, err = m.C.Check(auth, v) - if err != nil { - labels = append(labels, OutcomeLabel, failureOutcome, ReasonLabel, reason) - m.Measures.CapabilityCheckOutcome.With(labels...).Add(1) - if errorOut { - return err - } - return nil - } - - labels = append(labels, OutcomeLabel, AcceptedOutcome, ReasonLabel, "") - m.Measures.CapabilityCheckOutcome.With(labels...).Add(1) - return nil - } -} - -// prepMetrics gathers the information needed for metric label information. It -// gathers the client ID, partnerID, and endpoint (bucketed) for more information -// on the metric when a request is unauthorized. -func (m MetricValidator) prepMetrics(auth bascule.Authentication) (string, string, string, string, error) { - if auth.Token == nil { - return "", "", "", TokenMissingValues, ErrNoToken - } - client := auth.Token.Principal() - if auth.Token.Attributes() == nil { - return client, "", "", TokenMissingValues, ErrNilAttributes - } - - partnerVal, ok := bascule.GetNestedAttribute(auth.Token.Attributes(), PartnerKeys()...) - if !ok { - return client, "", "", UndeterminedPartnerID, fmt.Errorf("couldn't get partner IDs from attributes using keys %v", PartnerKeys()) - } - partnerIDs, err := cast.ToStringSliceE(partnerVal) - if err != nil { - //nolint:errorlint - return client, "", "", UndeterminedPartnerID, fmt.Errorf("partner IDs \"%v\" couldn't be cast to string slice: %v", partnerVal, err) - } - partnerID := DeterminePartnerMetric(partnerIDs) - - if auth.Request.URL == nil { - return client, partnerID, "", TokenMissingValues, ErrNoURL - } - escapedURL := auth.Request.URL.EscapedPath() - endpoint := determineEndpointMetric(m.Endpoints, escapedURL) - return client, partnerID, endpoint, "", nil -} - -// DeterminePartnerMetric takes a list of partners and decides what the partner -// metric label should be. -func DeterminePartnerMetric(partners []string) string { - if len(partners) < 1 { - return "none" - } - if len(partners) == 1 { - if partners[0] == "*" { - return "wildcard" - } - return partners[0] - } - for _, partner := range partners { - if partner == "*" { - return "wildcard" - } - } - return "many" -} - -// determineEndpointMetric takes a list of regular expressions and applies them -// to the url of the request to decide what the endpoint metric label should be. -func determineEndpointMetric(endpoints []*regexp.Regexp, urlHit string) string { - for _, r := range endpoints { - idxs := r.FindStringIndex(urlHit) - if idxs == nil { - continue - } - if idxs[0] == 0 { - return r.String() - } - } - return "not_recognized" -} - -// Metrics Listener -type MetricListener struct { - expLeeway time.Duration - nbfLeeway time.Duration - measures *AuthValidationMeasures -} - -type Option func(m *MetricListener) - -func NewMetricListener(m *AuthValidationMeasures, options ...Option) *MetricListener { - listener := MetricListener{ - measures: m, - } - - for _, o := range options { - o(&listener) - } - return &listener -} - -func (m *MetricListener) OnErrorResponse(e basculehttp.ErrorResponseReason, _ error) { - if m.measures == nil { - return - } - m.measures.ValidationOutcome.With(OutcomeLabel, e.String()).Add(1) -} - -func (m *MetricListener) OnAuthenticated(auth bascule.Authentication) { - now := time.Now() - - if m.measures == nil { - return // measure tools are not defined, skip - } - - if auth.Token == nil { - return - } - - m.measures.ValidationOutcome.With(OutcomeLabel, "Accepted").Add(1) - - c, ok := auth.Token.Attributes().Get("claims") - if !ok { - return // if there aren't any claims, skip - } - claims, ok := c.(jwt.Claims) - if !ok { - return // if claims aren't what we expect, skip - } - - //how far did we land from the NBF (in seconds): ie. -1 means 1 sec before, 1 means 1 sec after - if nbf, nbfPresent := claims.NotBefore(); nbfPresent { - nbf = nbf.Add(-m.nbfLeeway) - offsetToNBF := now.Sub(nbf).Seconds() - m.measures.NBFHistogram.Observe(offsetToNBF) - } - - //how far did we land from the EXP (in seconds): ie. -1 means 1 sec before, 1 means 1 sec after - if exp, expPresent := claims.Expiration(); expPresent { - exp = exp.Add(m.expLeeway) - offsetToEXP := now.Sub(exp).Seconds() - m.measures.ExpHistogram.Observe(offsetToEXP) - } -} - -// CapabilitiesValidator checks the capabilities provided in a -// bascule.Authentication object to determine if a request is authorized. It -// can also provide a function to be used in authorization middleware that -// pulls the Authentication object from a context before checking it. -type CapabilitiesValidator struct { - Checker CapabilityChecker -} - -type CapabilitiesError struct { - CapabilitiesFound []string - UrlToMatch string - MethodToMatch string -} - -func PartnerKeys() []string { - return partnerKeys -} - -func NewCapabilitiesError(capabilities []string, reqUrl string, method string) *CapabilitiesError { - return &CapabilitiesError{ - CapabilitiesFound: capabilities, - UrlToMatch: reqUrl, - MethodToMatch: method, - } -} - -func (c *CapabilitiesError) Error() string { - return fmt.Sprintf("%v", &c) -} - -// CreateValidator creates a function that determines whether or not a -// client is authorized to make a request to an endpoint. It uses the -// bascule.Authentication from the context to get the information needed by the -// CapabilityChecker to determine authorization. -func (c CapabilitiesValidator) CreateValidator(errorOut bool) bascule.ValidatorFunc { - return func(ctx context.Context, _ bascule.Token) error { - auth, ok := bascule.FromContext(ctx) - if !ok { - if errorOut { - return ErrNoAuth - } - return nil - } - - _, err := c.Check(auth, ParsedValues{}) - if err != nil && errorOut { - return err - } - - return nil - } -} - -// Check takes the needed values out of the given Authentication object in -// order to determine if a request is authorized. It determines this through -// iterating through each capability and calling the CapabilityChecker. If no -// capability authorizes the client for the given endpoint and method, it is -// unauthorized. -func (c CapabilitiesValidator) Check(auth bascule.Authentication, _ ParsedValues) (string, error) { - if auth.Token == nil { - return TokenMissingValues, ErrNoToken - } - vals, reason, err := getCapabilities(auth.Token.Attributes()) - if err != nil { - return reason, err - } - - if auth.Request.URL == nil { - return TokenMissingValues, ErrNoURL - } - reqURL := auth.Request.URL.EscapedPath() - method := auth.Request.Method - err = c.checkCapabilities(vals, reqURL, method) - if err != nil { - return NoCapabilitiesMatch, err - } - return "", nil -} - -// checkCapabilities uses a CapabilityChecker to check if each capability -// provided is authorized. If an authorized capability is found, no error is -// returned. -func (c CapabilitiesValidator) checkCapabilities(capabilities []string, reqURL string, method string) error { - for _, val := range capabilities { - if c.Checker.Authorized(val, reqURL, method) { - return nil - } - } - - return multierr.Append(ErrNoValidCapabilityFound, NewCapabilitiesError(capabilities, reqURL, method)) - -} - -// getCapabilities runs some error checks while getting the list of -// capabilities from the attributes. -func getCapabilities(attributes bascule.Attributes) ([]string, string, error) { - if attributes == nil { - return []string{}, UndeterminedCapabilities, ErrNilAttributes - } - - val, ok := attributes.Get(CapabilityKey) - if !ok { - return []string{}, UndeterminedCapabilities, fmt.Errorf("couldn't get capabilities using key %v", CapabilityKey) - } - - vals, err := cast.ToStringSliceE(val) - if err != nil { - //nolint:errorlint - return []string{}, UndeterminedCapabilities, fmt.Errorf("capabilities \"%v\" not the expected string slice: %v", val, err) - } - - if len(vals) == 0 { - return []string{}, EmptyCapabilitiesList, ErrNoVals - } - - return vals, "", nil - -} - -// CapabilityChecker is an object that can determine if a capability provides -// authorization to the endpoint. -type CapabilityChecker interface { - Authorized(string, string, string) bool -} - -// EndpointRegexCheck uses a regular expression to validate an endpoint and -// method provided in a capability against the endpoint hit and method used for -// the request. -type EndpointRegexCheck struct { - prefixToMatch *regexp.Regexp - acceptAllMethod string -} - -// NewEndpointRegexCheck creates an object that implements the -// CapabilityChecker interface. It takes a prefix that is expected at the -// beginning of a capability and a string that, if provided in the capability, -// authorizes all methods for that endpoint. After the prefix, the -// EndpointRegexCheck expects there to be an endpoint regular expression and an -// http method - separated by a colon. The expected format of a capability is: -// : -func NewEndpointRegexCheck(prefix string, acceptAllMethod string) (EndpointRegexCheck, error) { - matchPrefix, err := regexp.Compile("^" + prefix + "(.+):(.+?)$") - if err != nil { - return EndpointRegexCheck{}, fmt.Errorf("failed to compile prefix [%v]: %w", prefix, err) - } - - e := EndpointRegexCheck{ - prefixToMatch: matchPrefix, - acceptAllMethod: acceptAllMethod, - } - return e, nil -} - -// Authorized checks the capability against the endpoint hit and method used. -// If the capability has the correct prefix and is meant to be used with the -// method provided to access the endpoint provided, it is authorized. -func (e EndpointRegexCheck) Authorized(capability string, urlToMatch string, methodToMatch string) bool { - matches := e.prefixToMatch.FindStringSubmatch(capability) - - if matches == nil || len(matches) < 2 { - return false - } - - method := matches[2] - if method != e.acceptAllMethod && method != strings.ToLower(methodToMatch) { - return false - } - - re, err := regexp.Compile(matches[1]) //url regex that capability grants access to - if err != nil { - return false - } - - matchIdxs := re.FindStringIndex(urlToMatch) - if matchIdxs == nil || matchIdxs[0] != 0 { - return false - } - - return true -} - -// AlwaysCheck is a CapabilityChecker that always returns either true or false. -type AlwaysCheck bool - -// Authorized returns the saved boolean value, rather than checking the -// parameters given. -func (a AlwaysCheck) Authorized(_, _, _ string) bool { - return bool(a) -} - -// ConstCheck is a basic capability checker that determines a capability is -// authorized if it matches the ConstCheck's string. -type ConstCheck string - -// Authorized validates the capability provided against the stored string. -func (c ConstCheck) Authorized(capability, _, _ string) bool { - return string(c) == capability -} diff --git a/basculehelper/basculeHelper_test.go b/basculehelper/basculeHelper_test.go deleted file mode 100644 index 6d414e1..0000000 --- a/basculehelper/basculeHelper_test.go +++ /dev/null @@ -1,767 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehelper - -import ( - "context" - "errors" - "net/url" - "regexp" - "testing" - - "github.com/go-kit/kit/metrics/generic" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/xmidt-org/bascule" -) - -// MetricValidatorTests -type mockCapabilitiesChecker struct { - mock.Mock -} - -func (m *mockCapabilitiesChecker) Check(auth bascule.Authentication, v ParsedValues) (string, error) { - args := m.Called(auth, v) - return args.String(0), args.Error(1) -} - -func TestMetricValidatorFunc(t *testing.T) { - goodURL, err := url.Parse("/test") - require.Nil(t, err) - capabilities := []string{ - "test", - "a", - "joweiafuoiuoiwauf", - "it's a match", - } - goodAttributes := bascule.NewAttributes(map[string]interface{}{ - CapabilityKey: capabilities, - "allowedResources": map[string]interface{}{ - "allowedPartners": []string{"meh"}, - }, - }) - - tests := []struct { - description string - includeAuth bool - attributes bascule.Attributes - checkCallExpected bool - checkReason string - checkErr error - errorOut bool - errExpected bool - }{ - { - description: "Success", - includeAuth: true, - attributes: goodAttributes, - checkCallExpected: true, - errorOut: true, - }, - { - description: "Include Auth Error", - errorOut: true, - errExpected: true, - }, - { - description: "Include Auth Suppressed Error", - errorOut: false, - }, - { - description: "Prep Metrics Error", - includeAuth: true, - attributes: nil, - errorOut: true, - errExpected: true, - }, - { - description: "Prep Metrics Suppressed Error", - includeAuth: true, - attributes: nil, - errorOut: false, - }, - { - description: "Check Error", - includeAuth: true, - attributes: goodAttributes, - checkCallExpected: true, - checkReason: NoCapabilitiesMatch, - checkErr: errors.New("test check error"), - errorOut: true, - errExpected: true, - }, - { - description: "Check Suppressed Error", - includeAuth: true, - attributes: goodAttributes, - checkCallExpected: true, - checkReason: NoCapabilitiesMatch, - checkErr: errors.New("test check error"), - errorOut: false, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - - ctx := context.Background() - auth := bascule.Authentication{ - Token: bascule.NewToken("test", "princ", tc.attributes), - Request: bascule.Request{ - URL: goodURL, - Method: "GET", - }, - } - if tc.includeAuth { - ctx = bascule.WithAuthentication(ctx, auth) - } - mockCapabilitiesChecker := new(mockCapabilitiesChecker) - if tc.checkCallExpected { - mockCapabilitiesChecker.On("Check", mock.Anything, mock.Anything).Return(tc.checkReason, tc.checkErr).Once() - } - - counter := generic.NewCounter("test_capability_check") - mockMeasures := AuthCapabilityCheckMeasures{ - CapabilityCheckOutcome: counter, - } - - m := MetricValidator{ - C: mockCapabilitiesChecker, - Measures: &mockMeasures, - } - err := m.CreateValidator(tc.errorOut)(ctx, nil) - mockCapabilitiesChecker.AssertExpectations(t) - if tc.errExpected { - assert.NotNil(err) - return - } - assert.Nil(err) - }) - } -} - -func TestPrepMetrics(t *testing.T) { - type testType int - var ( - goodURL = "/asnkfn/aefkijeoij/aiogj" - matchingURL = "/fnvvdsjkfji/mac:12345544322345334/geigosj" - client = "special" - prepErr = errors.New("couldn't get partner IDs from attributes") - badValErr = errors.New("couldn't be cast to string slice") - goodEndpoint = `/fnvvdsjkfji/.*/geigosj\b` - goodRegex = regexp.MustCompile(goodEndpoint) - unusedEndpoint = `/a/b\b` - unusedRegex = regexp.MustCompile(unusedEndpoint) - ) - - tests := []struct { - description string - noPartnerID bool - partnerIDs interface{} - url string - includeToken bool - includeAttributes bool - includeURL bool - expectedPartner string - expectedEndpoint string - expectedReason string - expectedErr error - }{ - { - description: "Success", - partnerIDs: []string{"partner"}, - url: goodURL, - includeToken: true, - includeAttributes: true, - includeURL: true, - expectedPartner: "partner", - expectedEndpoint: "not_recognized", - expectedReason: "", - expectedErr: nil, - }, - { - description: "Success Abridged URL", - partnerIDs: []string{"partner"}, - url: matchingURL, - includeToken: true, - includeAttributes: true, - includeURL: true, - expectedPartner: "partner", - expectedEndpoint: goodEndpoint, - expectedReason: "", - expectedErr: nil, - }, - { - description: "Nil Token Error", - expectedReason: TokenMissingValues, - expectedErr: ErrNoToken, - }, - { - description: "Nil Token Attributes Error", - url: goodURL, - includeToken: true, - expectedReason: TokenMissingValues, - expectedErr: ErrNilAttributes, - }, - { - description: "No Partner ID Error", - noPartnerID: true, - url: goodURL, - includeToken: true, - includeAttributes: true, - expectedPartner: "", - expectedEndpoint: "", - expectedReason: UndeterminedPartnerID, - expectedErr: prepErr, - }, - { - description: "Non String Slice Partner ID Error", - partnerIDs: []testType{0, 1, 2}, - url: goodURL, - includeToken: true, - includeAttributes: true, - expectedPartner: "", - expectedEndpoint: "", - expectedReason: UndeterminedPartnerID, - expectedErr: badValErr, - }, - { - description: "Non Slice Partner ID Error", - partnerIDs: struct{ string }{}, - url: goodURL, - includeToken: true, - includeAttributes: true, - expectedPartner: "", - expectedEndpoint: "", - expectedReason: UndeterminedPartnerID, - expectedErr: badValErr, - }, - { - description: "Nil URL Error", - partnerIDs: []string{"partner"}, - url: goodURL, - includeToken: true, - includeAttributes: true, - expectedPartner: "partner", - expectedReason: TokenMissingValues, - expectedErr: ErrNoURL, - }, - } - - m := MetricValidator{ - Endpoints: []*regexp.Regexp{unusedRegex, goodRegex}, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - // setup auth - token := bascule.NewToken("mehType", client, nil) - if tc.includeAttributes { - a := map[string]interface{}{ - "allowedResources": map[string]interface{}{ - "allowedPartners": tc.partnerIDs, - }, - } - - if tc.noPartnerID { - a["allowedResources"] = 5 - } - attributes := bascule.NewAttributes(a) - token = bascule.NewToken("mehType", client, attributes) - } - auth := bascule.Authentication{ - Authorization: "testAuth", - Request: bascule.Request{ - Method: "get", - }, - } - if tc.includeToken { - auth.Token = token - } - if tc.includeURL { - u, err := url.ParseRequestURI(tc.url) - require.Nil(err) - auth.Request.URL = u - } - - c, partner, endpoint, reason, err := m.prepMetrics(auth) - if tc.includeToken { - assert.Equal(client, c) - } - assert.Equal(tc.expectedPartner, partner) - assert.Equal(tc.expectedEndpoint, endpoint) - assert.Equal(tc.expectedReason, reason) - if err == nil || tc.expectedErr == nil { - assert.Equal(tc.expectedErr, err) - } else { - assert.Contains(err.Error(), tc.expectedErr.Error()) - } - }) - } -} - -func TestDeterminePartnerMetric(t *testing.T) { - tests := []struct { - description string - partnersInput []string - expectedResult string - }{ - { - description: "No Partners", - expectedResult: "none", - }, - { - description: "one wildcard", - partnersInput: []string{"*"}, - expectedResult: "wildcard", - }, - { - description: "one partner", - partnersInput: []string{"TestPartner"}, - expectedResult: "TestPartner", - }, - { - description: "many partners", - partnersInput: []string{"partner1", "partner2", "partner3"}, - expectedResult: "many", - }, - { - description: "many partners with wildcard", - partnersInput: []string{"partner1", "partner2", "partner3", "*"}, - expectedResult: "wildcard", - }, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - partner := DeterminePartnerMetric(tc.partnersInput) - assert.Equal(tc.expectedResult, partner) - }) - } -} - -// CapabilitiesValidator Tests -func TestCapabilitiesChecker(t *testing.T) { - //nolint:gosimple - var v interface{} - v = CapabilitiesValidator{} - _, ok := v.(CapabilitiesChecker) - assert.True(t, ok) -} - -func TestCapabilitiesValidatorFunc(t *testing.T) { - capabilities := []string{ - "test", - "a", - "joweiafuoiuoiwauf", - "it's a match", - } - goodURL, err := url.Parse("/test") - require.Nil(t, err) - goodRequest := bascule.Request{ - URL: goodURL, - Method: "GET", - } - tests := []struct { - description string - includeAuth bool - includeToken bool - errorOut bool - errExpected bool - }{ - { - description: "Success", - includeAuth: true, - includeToken: true, - errorOut: true, - }, - { - description: "No Auth Error", - errorOut: true, - errExpected: true, - }, - { - description: "No Auth Suppressed Error", - }, - { - description: "Check Error", - includeAuth: true, - errorOut: true, - errExpected: true, - }, - { - description: "Check Suppressed Error", - includeAuth: true, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - ctx := context.Background() - auth := bascule.Authentication{ - Request: goodRequest, - } - if tc.includeToken { - auth.Token = bascule.NewToken("test", "princ", - bascule.NewAttributes(map[string]interface{}{CapabilityKey: capabilities})) - } - if tc.includeAuth { - ctx = bascule.WithAuthentication(ctx, auth) - } - c := CapabilitiesValidator{ - Checker: ConstCheck("it's a match"), - } - err := c.CreateValidator(tc.errorOut)(ctx, bascule.NewToken("", "", nil)) - if tc.errExpected { - assert.NotNil(err) - return - } - assert.Nil(err) - }) - } -} - -func TestCapabilitiesValidatorCheck(t *testing.T) { - capabilities := []string{ - "test", - "a", - "joweiafuoiuoiwauf", - "it's a match", - } - pv := ParsedValues{} - tests := []struct { - description string - includeToken bool - includeAttributes bool - includeURL bool - checker CapabilityChecker - expectedReason string - expectedErr error - }{ - { - description: "Success", - includeAttributes: true, - includeURL: true, - checker: ConstCheck("it's a match"), - expectedErr: nil, - }, - { - description: "No Token Error", - expectedReason: TokenMissingValues, - expectedErr: ErrNoToken, - }, - { - description: "Get Capabilities Error", - includeToken: true, - expectedReason: UndeterminedCapabilities, - expectedErr: ErrNilAttributes, - }, - { - description: "No URL Error", - includeAttributes: true, - expectedReason: TokenMissingValues, - expectedErr: ErrNoURL, - }, - { - description: "Check Capabilities Error", - includeAttributes: true, - includeURL: true, - checker: AlwaysCheck(false), - expectedReason: NoCapabilitiesMatch, - expectedErr: ErrNoValidCapabilityFound, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - c := CapabilitiesValidator{ - Checker: tc.checker, - } - a := bascule.Authentication{} - if tc.includeToken { - a.Token = bascule.NewToken("", "", nil) - } - if tc.includeAttributes { - a.Token = bascule.NewToken("test", "princ", - bascule.NewAttributes(map[string]interface{}{CapabilityKey: capabilities})) - } - if tc.includeURL { - goodURL, err := url.Parse("/test") - require.Nil(err) - a.Request = bascule.Request{ - URL: goodURL, - Method: "GET", - } - } - reason, err := c.Check(a, pv) - assert.Equal(tc.expectedReason, reason) - if err == nil || tc.expectedErr == nil { - assert.Equal(tc.expectedErr, err) - return - } - assert.Contains(err.Error(), tc.expectedErr.Error()) - }) - } -} - -func TestCheckCapabilities(t *testing.T) { - capabilities := []string{ - "test", - "a", - "joweiafuoiuoiwauf", - "it's a match", - } - - tests := []struct { - description string - goodCapability string - expectedErr error - }{ - { - description: "Success", - goodCapability: "it's a match", - }, - { - description: "No Capability Found Error", - expectedErr: ErrNoValidCapabilityFound, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - c := CapabilitiesValidator{ - Checker: ConstCheck(tc.goodCapability), - } - err := c.checkCapabilities(capabilities, "", "") - if err == nil || tc.expectedErr == nil { - assert.Equal(tc.expectedErr, err) - return - } - assert.Contains(err.Error(), tc.expectedErr.Error()) - }) - } -} - -func TestGetCapabilities(t *testing.T) { - type testType int - goodKeyVal := []string{"cap1", "cap2"} - emptyVal := []string{} - getCapabilitiesErr := errors.New("couldn't get capabilities using key") - badCapabilitiesErr := errors.New("not the expected string slice") - tests := []struct { - description string - nilAttributes bool - missingAttribute bool - keyValue interface{} - expectedVals []string - expectedReason string - expectedErr error - }{ - { - description: "Success", - keyValue: goodKeyVal, - expectedVals: goodKeyVal, - expectedReason: "", - expectedErr: nil, - }, - { - description: "Nil Attributes Error", - nilAttributes: true, - expectedVals: emptyVal, - expectedReason: UndeterminedCapabilities, - expectedErr: ErrNilAttributes, - }, - { - description: "No Attribute Error", - missingAttribute: true, - expectedVals: emptyVal, - expectedReason: UndeterminedCapabilities, - expectedErr: getCapabilitiesErr, - }, - { - description: "Nil Capabilities Error", - keyValue: nil, - expectedVals: emptyVal, - expectedReason: UndeterminedCapabilities, - expectedErr: badCapabilitiesErr, - }, - { - description: "Non List Capabilities Error", - keyValue: struct{ string }{"abcd"}, - expectedVals: emptyVal, - expectedReason: UndeterminedCapabilities, - expectedErr: badCapabilitiesErr, - }, - { - description: "Non String List Capabilities Error", - keyValue: []testType{0, 1, 2}, - expectedVals: emptyVal, - expectedReason: UndeterminedCapabilities, - expectedErr: badCapabilitiesErr, - }, - { - description: "Empty Capabilities Error", - keyValue: emptyVal, - expectedVals: emptyVal, - expectedReason: EmptyCapabilitiesList, - expectedErr: ErrNoVals, - }, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - m := map[string]interface{}{CapabilityKey: tc.keyValue} - if tc.missingAttribute { - m = map[string]interface{}{} - } - attributes := bascule.NewAttributes(m) - if tc.nilAttributes { - attributes = nil - } - vals, reason, err := getCapabilities(attributes) - assert.Equal(tc.expectedVals, vals) - assert.Equal(tc.expectedReason, reason) - if err == nil || tc.expectedErr == nil { - assert.Equal(tc.expectedErr, err) - } else { - assert.Contains(err.Error(), tc.expectedErr.Error()) - } - }) - } -} - -// CapabilityChecker Tests -func TestAlwaysCheck(t *testing.T) { - assert := assert.New(t) - alwaysTrue := AlwaysCheck(true) - assert.True(alwaysTrue.Authorized("a", "b", "c")) - alwaysFalse := AlwaysCheck(false) - assert.False(alwaysFalse.Authorized("a", "b", "c")) -} - -func TestConstCapabilityChecker(t *testing.T) { - //nolint:gosimple - var v interface{} - v = ConstCheck("test") - _, ok := v.(CapabilityChecker) - assert.True(t, ok) -} - -func TestConstCheck(t *testing.T) { - tests := []struct { - description string - capability string - okExpected bool - }{ - { - description: "Success", - capability: "perfectmatch", - okExpected: true, - }, - { - description: "Not a Match", - capability: "meh", - okExpected: false, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - c := ConstCheck("perfectmatch") - ok := c.Authorized(tc.capability, "ignored1", "ignored2") - assert.Equal(tc.okExpected, ok) - }) - } -} - -func TestEndpointRegexCapabilityChecker(t *testing.T) { - assert := assert.New(t) - var v interface{} - v, err := NewEndpointRegexCheck("test", "") - assert.Nil(err) - _, ok := v.(CapabilityChecker) - assert.True(ok) -} -func TestNewEndpointRegexError(t *testing.T) { - e, err := NewEndpointRegexCheck(`\M`, "") - assert := assert.New(t) - assert.Empty(e) - assert.NotNil(err) -} - -func TestEndpointRegexCheck(t *testing.T) { - tests := []struct { - description string - prefix string - acceptAllMethod string - capability string - url string - method string - okExpected bool - }{ - { - description: "Success", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: "a:b:c:.*:get", - url: "/test/ffff//", - method: "get", - okExpected: true, - }, - { - description: "No Match Error", - prefix: "a:b:c:", - capability: "a:.*:get", - method: "get", - }, - { - description: "Wrong Method Error", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: "a:b:c:.*:get", - method: "post", - }, - { - description: "Regex Doesn't Compile Error", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: `a:b:c:\M:get`, - method: "get", - }, - { - description: "URL Doesn't Match Capability Error", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: "a:b:c:[A..Z]+:get", - url: "1111", - method: "get", - }, - { - description: "URL Capability Match Wrong Location Error", - prefix: "a:b:c:", - acceptAllMethod: "all", - capability: "a:b:c:[A..Z]+:get", - url: "11AAAAA", - method: "get", - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - e, err := NewEndpointRegexCheck(tc.prefix, tc.acceptAllMethod) - require.Nil(err) - require.NotEmpty(e) - ok := e.Authorized(tc.capability, tc.url, tc.method) - assert.Equal(tc.okExpected, ok) - }) - } -} diff --git a/basculehttp/README.md b/basculehttp/README.md deleted file mode 100644 index 6700eca..0000000 --- a/basculehttp/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# basculehttp - -The package for auth related middleware, implemented as [alice-style http decorators](https://github.com/justinas/alice). - -[![GoDoc](https://godoc.org/github.com/xmidt-org/bascule/basculehttp?status.svg)](https://godoc.org/github.com/xmidt-org/bascule/basculehttp) - -## Summary - -This package makes it easy to validate the Authorization of an incoming http -request. - -## Decorators - -This packages has three http decorators (which act as middleware): - -1. **Constructor**: Parses the Authorization header into a Token and runs some - basic validation using a TokenFactory. The basic validation varies and is - determined by the TokenFactory. Basic and JWT TokenFactories are included - in the package, but the consumer can also create its own TokenFactory. - After the Token is created, it is added to the request context. -2. **Enforcer**: Gets the Token from the request context and then validates - that the Token is authorized using validator functions provided by the - consumer. -3. **Listener**: Gets the Token from the request context and then provides it - to a function set by the consumer and called by the decorator. Some - examples of using the Listener is to log a statement related to the Token - found, or to add to some metrics based on something in the Token. \ No newline at end of file diff --git a/basculehttp/accessor.go b/basculehttp/accessor.go new file mode 100644 index 0000000..c036e7c --- /dev/null +++ b/basculehttp/accessor.go @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "net/http" + "strings" +) + +const ( + // DefaultAuthorizationHeader is the name of the header used by default to obtain + // the raw credentials. + DefaultAuthorizationHeader = "Authorization" +) + +// DuplicateHeaderError indicates that an HTTP header had more than one value +// when only one value was expected. +type DuplicateHeaderError struct { + // Header is the name of the duplicate header. + Header string +} + +func (err *DuplicateHeaderError) Error() string { + var o strings.Builder + o.WriteString(`Duplicate header: "`) + o.WriteString(err.Header) + o.WriteString(`"`) + return o.String() +} + +// MissingHeaderError indicates that an expected HTTP header is missing. +type MissingHeaderError struct { + // Header is the name of the missing header. + Header string +} + +func (err *MissingHeaderError) Error() string { + var o strings.Builder + o.WriteString(`Missing header: "`) + o.WriteString(err.Header) + o.WriteString(`"`) + return o.String() +} + +// StatusCode returns http.StatusUnauthorized, as the request carries +// no authorization in it. +func (err *MissingHeaderError) StatusCode() int { + return http.StatusUnauthorized +} + +// Accessor is the strategy for obtaining credentials from an HTTP request. +type Accessor interface { + // GetCredentials returns the raw credentials from a request. + GetCredentials(*http.Request) (string, error) +} + +var defaultAccessor Accessor = HeaderAccessor{} + +// DefaultAccessor returns the builtin default strategy for obtaining raw credentials +// from an HTTP request. The returned Accessor simply retrieves the Authorization header +// value if it exists. +func DefaultAccessor() Accessor { return defaultAccessor } + +// HeaderAccessor obtains the raw credentials from a specific header in +// an HTTP request. +type HeaderAccessor struct { + // Header is the name of the HTTP header to use. If not supplied, + // DefaultAuthorizationHeader is used. + // + // If no authorization header can be found in an HTTP request, + // MissingHeaderError is returned. + Header string + + // ErrorOnDuplicate controls whether an error is returned if more + // than one Header is found in the request. By default, this is false. + ErrorOnDuplicate bool +} + +func (ha HeaderAccessor) GetCredentials(r *http.Request) (raw string, err error) { + h := ha.Header + if len(h) == 0 { + h = DefaultAuthorizationHeader + } + + values := r.Header.Values(h) + switch { + case len(values) == 0: + err = &MissingHeaderError{ + Header: h, + } + + case len(values) == 1 || !ha.ErrorOnDuplicate: + raw = values[0] + + default: + err = &DuplicateHeaderError{ + Header: h, + } + } + + return +} diff --git a/basculehttp/basic.go b/basculehttp/basic.go new file mode 100644 index 0000000..9dfbe15 --- /dev/null +++ b/basculehttp/basic.go @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "context" + "encoding/base64" + "strings" + + "github.com/xmidt-org/bascule/v1" +) + +// InvalidBasicAuthError indicates that the Basic credentials were improperly +// encoded, either due to base64 issues or formatting. +type InvalidBasicAuthError struct { + // Cause represents the lower level error that occurred, e.g. a base64 + // encoding error. + Cause error +} + +func (err *InvalidBasicAuthError) Unwrap() error { return err.Cause } + +func (err *InvalidBasicAuthError) Error() string { + var o strings.Builder + o.WriteString("Basic auth string not encoded properly") + + if err.Cause != nil { + o.WriteString(": ") + o.WriteString(err.Cause.Error()) + } + + return o.String() +} + +type basicTokenParser struct{} + +func (btp basicTokenParser) Parse(_ context.Context, c bascule.Credentials) (t bascule.Token, err error) { + var decoded []byte + decoded, err = base64.StdEncoding.DecodeString(c.Value) + if err != nil { + err = &InvalidBasicAuthError{ + Cause: err, + } + + return + } + + username, _, found := strings.Cut(string(decoded), ":") + if found { + t = &Token{ + principal: username, + } + } else { + err = &InvalidBasicAuthError{} + } + + return +} diff --git a/basculehttp/basicTokenFactory.go b/basculehttp/basicTokenFactory.go deleted file mode 100644 index 7093fe9..0000000 --- a/basculehttp/basicTokenFactory.go +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "bytes" - "context" - "encoding/base64" - "errors" - "fmt" - "net/http" - - "github.com/xmidt-org/bascule" - "go.uber.org/fx" -) - -var ( - ErrorMalformedValue = errors.New("expected : in decoded value") - ErrorPrincipalNotFound = errors.New("principal not found") - ErrorInvalidPassword = errors.New("invalid password") -) - -type EncodedBasicKeys struct { - Basic []string `json:"basic" yaml:"basic"` -} - -// EncodedBasicKeysIn contains string representations of the basic auth allowed. -type EncodedBasicKeysIn struct { - fx.In - Keys EncodedBasicKeys `name:"encoded_basic_auths"` -} - -// TokenFactoryFunc makes it so any function that has the same signature as -// TokenFactory's ParseAndValidate function implements TokenFactory. -type TokenFactoryFunc func(context.Context, *http.Request, bascule.Authorization, string) (bascule.Token, error) - -func (tff TokenFactoryFunc) ParseAndValidate(ctx context.Context, r *http.Request, a bascule.Authorization, v string) (bascule.Token, error) { - return tff(ctx, r, a, v) -} - -// BasicTokenFactory parses a basic auth and verifies it is in a map of valid -// basic auths. -type BasicTokenFactory map[string]string - -// ParseAndValidate expects the given value to be a base64 encoded string with -// the username followed by a colon and then the password. The function checks -// that the username password pair is in the map and returns a Token if it is. -func (btf BasicTokenFactory) ParseAndValidate(ctx context.Context, _ *http.Request, _ bascule.Authorization, value string) (bascule.Token, error) { - decoded, err := base64.StdEncoding.DecodeString(value) - if err != nil { - return nil, fmt.Errorf("could not decode string: %v", err) - } - - i := bytes.IndexByte(decoded, ':') - if i <= 0 { - return nil, ErrorMalformedValue - } - principal := string(decoded[:i]) - val, ok := btf[principal] - if !ok { - return nil, ErrorPrincipalNotFound - } - if val != string(decoded[i+1:]) { - // failed authentication - return nil, ErrorInvalidPassword - } - // "basic" is a placeholder here ... token types won't always map to the - // Authorization header. For example, a JWT should have a type of "jwt" or some such, not "bearer" - return bascule.NewToken("basic", principal, bascule.NewAttributes(map[string]interface{}{})), nil -} - -// NewBasicTokenFactoryFromList takes a list of base64 encoded basic auth keys, -// decodes them, and supplies that list in map form of username to password. If -// a username is encoded in two different auth keys, it will be overwritten by -// the last occurrence of that username with a password. If anoth -func NewBasicTokenFactoryFromList(encodedBasicAuthKeys []string) (BasicTokenFactory, error) { - btf := make(BasicTokenFactory) - errs := bascule.Errors{} - - for _, encodedKey := range encodedBasicAuthKeys { - decoded, err := base64.StdEncoding.DecodeString(encodedKey) - if err != nil { - errs = append(errs, fmt.Errorf("failed to base64-decode basic auth key [%v]: %v", encodedKey, err)) - continue - } - - i := bytes.IndexByte(decoded, ':') - if i <= 0 { - errs = append(errs, fmt.Errorf("basic auth key [%v] is malformed", encodedKey)) - continue - } - - btf[string(decoded[:i])] = string(decoded[i+1:]) - } - - if len(errs) != 0 { - return btf, errs - } - - // explicitly return nil so we don't have any empty error lists being returned. - return btf, nil -} - -// ProvideBasicTokenFactory uses configuration at the key given to build a basic -// token factory. It provides a constructor option with the basic token -// factory. -func ProvideBasicTokenFactory(key string) fx.Option { - return fx.Provide( - fx.Annotated{ - Group: "bascule_constructor_options", - Target: func(in EncodedBasicKeysIn) (COption, error) { - if len(in.Keys.Basic) == 0 { - return nil, nil - } - tf, err := NewBasicTokenFactoryFromList(in.Keys.Basic) - if err != nil { - return nil, err - } - return WithTokenFactory(BasicAuthorization, tf), nil - }, - }, - ) -} diff --git a/basculehttp/basicTokenFactory_test.go b/basculehttp/basicTokenFactory_test.go deleted file mode 100644 index 94e61d0..0000000 --- a/basculehttp/basicTokenFactory_test.go +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "context" - "encoding/base64" - "errors" - "fmt" - "net/http/httptest" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/sallust" - "go.uber.org/fx" -) - -func TestBasicTokenFactory(t *testing.T) { - btf := BasicTokenFactory(map[string]string{ - "user": "pass", - "test": "valid", - }) - tests := []struct { - description string - value string - expectedToken bascule.Token - expectedErr error - }{ - { - description: "Success", - value: base64.StdEncoding.EncodeToString([]byte("user:pass")), - expectedToken: bascule.NewToken("basic", "user", bascule.NewAttributes(map[string]interface{}{})), - }, - { - description: "Can't Decode Error", - value: "abcdef", - expectedErr: errors.New("illegal base64 data"), - }, - { - description: "Malformed Value Error", - value: base64.StdEncoding.EncodeToString([]byte("abcdef")), - expectedErr: ErrorMalformedValue, - }, - { - description: "Key Not in Map Error", - value: base64.StdEncoding.EncodeToString([]byte("u:p")), - expectedErr: ErrorPrincipalNotFound, - }, - { - description: "Invalid Password Error", - value: base64.StdEncoding.EncodeToString([]byte("user:p")), - expectedErr: ErrorInvalidPassword, - }, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - req := httptest.NewRequest("get", "/", nil) - token, err := btf.ParseAndValidate(context.Background(), req, "", tc.value) - assert.Equal(tc.expectedToken, token) - if tc.expectedErr == nil || err == nil { - assert.Equal(tc.expectedErr, err) - } else { - assert.Contains(err.Error(), tc.expectedErr.Error()) - } - }) - } -} - -func TestNewBasicTokenFactoryFromList(t *testing.T) { - goodKey := `dXNlcjpwYXNz` - badKeyDecode := `dXNlcjpwYXN\\\` - badKeyNoColon := `dXNlcnBhc3M=` - goodMap := map[string]string{"user": "pass"} - emptyMap := map[string]string{} - - tests := []struct { - description string - keyList []string - expectedDecodedMap BasicTokenFactory - expectedErr error - }{ - { - description: "Success", - keyList: []string{goodKey}, - expectedDecodedMap: goodMap, - }, - { - description: "Success With Errors", - keyList: []string{goodKey, badKeyDecode, badKeyNoColon}, - expectedDecodedMap: goodMap, - expectedErr: errors.New("multiple errors"), - }, - { - description: "Decode Error", - keyList: []string{badKeyDecode}, - expectedDecodedMap: emptyMap, - expectedErr: errors.New("failed to base64-decode basic auth key"), - }, - { - description: "Success", - keyList: []string{badKeyNoColon}, - expectedDecodedMap: emptyMap, - expectedErr: errors.New("malformed"), - }, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - m, err := NewBasicTokenFactoryFromList(tc.keyList) - assert.Equal(tc.expectedDecodedMap, m) - if tc.expectedErr == nil || err == nil { - assert.Equal(tc.expectedErr, err) - } else { - assert.Contains(err.Error(), tc.expectedErr.Error()) - } - }) - } -} - -func TestProvideBasicTokenFactory(t *testing.T) { - type In struct { - fx.In - Options []COption `group:"bascule_constructor_options"` - } - - tests := []struct { - description string - key string - optionExpected bool - keys EncodedBasicKeys - expectedErr error - }{ - { - description: "Success", - key: "good", - optionExpected: true, - keys: EncodedBasicKeys{Basic: []string{"dXNlcjpwYXNz", "dXNlcjpwYXNz", "dXNlcjpwYXNz"}}, - }, - { - description: "Disabled success", - key: "nonexistent", - }, - { - description: "Failure", - key: "bad", - keys: EncodedBasicKeys{Basic: []string{"AAAAAAAA"}}, - expectedErr: errors.New("malformed"), - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - result := In{} - assert := assert.New(t) - require := require.New(t) - app := fx.New( - fx.Provide( - func() (c sallust.Config) { - return sallust.Config{} - }, - - fx.Annotated{ - Name: "encoded_basic_auths", - Target: func() EncodedBasicKeys { - return tc.keys - }, - }, - ), - sallust.WithLogger(), - ProvideBasicTokenFactory(tc.key), - fx.Invoke( - func(in In) { - result = in - }, - ), - ) - err := app.Err() - if tc.expectedErr == nil { - require.NoError(err) - require.True(len(result.Options) == 1) - if tc.optionExpected { - require.NotNil(result.Options[0]) - return - } - require.Nil(result.Options[0]) - return - } - assert.Nil(result.Options) - require.Error(err) - assert.True(strings.Contains(err.Error(), tc.expectedErr.Error()), - fmt.Errorf("error [%v] doesn't contain error [%v]", - err, tc.expectedErr), - ) - }) - } -} diff --git a/basculehttp/bearerTokenFactory.go b/basculehttp/bearerTokenFactory.go deleted file mode 100644 index e37a74d..0000000 --- a/basculehttp/bearerTokenFactory.go +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "context" - "errors" - "fmt" - "net/http" - - "github.com/golang-jwt/jwt" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/clortho" - "github.com/xmidt-org/clortho/clorthofx" - "go.uber.org/fx" -) - -const ( - jwtPrincipalKey = "sub" -) - -var ( - ErrEmptyValue = errors.New("empty value") - ErrInvalidPrincipal = errors.New("invalid principal") - ErrInvalidToken = errors.New("token isn't valid") - ErrUnexpectedClaims = errors.New("claims wasn't MapClaims as expected") - ErrNilResolver = errors.New("resolver cannot be nil") -) - -// BearerTokenFactory parses and does basic validation for a JWT token, -// converting it into a bascule Token. -type BearerTokenFactory struct { - fx.In - DefaultKeyID string `name:"default_key_id" optional:"true"` - Resolver clortho.Resolver `name:"key_resolver" optional:"true"` - Parser bascule.JWTParser `name:"parser" optional:"true"` - Leeway bascule.Leeway `name:"jwt_leeway" optional:"true"` -} - -// ParseAndValidate expects the given value to be a JWT with a kid header. The -// kid should be resolvable by the Resolver and the JWT should be Parseable and -// pass any basic validation checks done by the Parser. If everything goes -// well, a Token of type "jwt" is returned. -func (btf BearerTokenFactory) ParseAndValidate(ctx context.Context, _ *http.Request, _ bascule.Authorization, value string) (bascule.Token, error) { - if len(value) == 0 { - return nil, ErrEmptyValue - } - - keyfunc := func(token *jwt.Token) (interface{}, error) { - keyID, ok := token.Header["kid"].(string) - if !ok { - keyID = btf.DefaultKeyID - } - - key, err := btf.Resolver.Resolve(ctx, keyID) - if err != nil { - return nil, fmt.Errorf("failed to resolve key: %v", err) - } - return key.Public(), nil - } - - leewayclaims := bascule.ClaimsWithLeeway{ - MapClaims: make(jwt.MapClaims), - Leeway: btf.Leeway, - } - - jwtToken, err := btf.Parser.ParseJWT(value, &leewayclaims, keyfunc) - if err != nil { - return nil, fmt.Errorf("failed to parse JWS: %v", err) - } - if !jwtToken.Valid { - return nil, ErrInvalidToken - } - - claims, ok := jwtToken.Claims.(*bascule.ClaimsWithLeeway) - if !ok { - return nil, fmt.Errorf("failed to parse JWS: %w", ErrUnexpectedClaims) - } - claimsMap, err := claims.GetMap() - if err != nil { - return nil, fmt.Errorf("failed to get map of claims with object [%v]: %v", claims, err) - } - jwtClaims := bascule.NewAttributes(claimsMap) - principalVal, ok := jwtClaims.Get(jwtPrincipalKey) - if !ok { - return nil, fmt.Errorf("%w: principal value not found at key %v", ErrInvalidPrincipal, jwtPrincipalKey) - } - principal, ok := principalVal.(string) - if !ok { - return nil, fmt.Errorf("%w: principal value [%v] not a string", ErrInvalidPrincipal, principalVal) - } - - return bascule.NewToken("jwt", principal, jwtClaims), nil -} - -// ProvideBearerTokenFactory uses the key given to unmarshal configuration -// needed to build a bearer token factory. It provides a constructor option -// with the bearer token factory. -func ProvideBearerTokenFactory(optional bool) fx.Option { - return fx.Options( - clorthofx.Provide(), - fx.Provide( - fx.Annotated{ - Group: "bascule_constructor_options", - Target: func(f BearerTokenFactory) (COption, error) { - if f.Parser == nil { - f.Parser = bascule.DefaultJWTParser - } - - if f.Resolver == nil { - if optional { - return nil, nil - } - return nil, ErrNilResolver - } - - return WithTokenFactory(BearerAuthorization, f), nil - }, - }, - ), - ) -} diff --git a/basculehttp/bearerTokenFactory_test.go b/basculehttp/bearerTokenFactory_test.go deleted file mode 100644 index 3869eae..0000000 --- a/basculehttp/bearerTokenFactory_test.go +++ /dev/null @@ -1,225 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "context" - "errors" - "net/http/httptest" - "testing" - - "github.com/golang-jwt/jwt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/clortho" - "github.com/xmidt-org/sallust" - "go.uber.org/fx" -) - -func TestBearerTokenFactory(t *testing.T) { - parseFailErr := errors.New("parse fail test") - resolveFailErr := errors.New("resolve fail test") - tests := []struct { - description string - value string - parseCalled bool - parseErr error - resolveCalled bool - resolveErr error - claims jwt.Claims - validToken bool - expectedToken bascule.Token - expectedErr error - }{ - { - description: "Success", - value: "abcd", - parseCalled: true, - resolveCalled: true, - claims: &bascule.ClaimsWithLeeway{ - MapClaims: jwt.MapClaims{jwtPrincipalKey: "test"}, - }, - validToken: true, - expectedToken: bascule.NewToken("jwt", "test", bascule.BasicAttributes{jwtPrincipalKey: "test"}), - expectedErr: nil, - }, - { - description: "Empty Value Error", - value: "", - expectedErr: ErrEmptyValue, - }, - { - description: "Parse Failure Error", - value: "abcd", - parseCalled: true, - parseErr: parseFailErr, - expectedErr: parseFailErr, - }, - { - description: "Resolve Key Error", - value: "abcd", - parseCalled: true, - resolveCalled: true, - resolveErr: resolveFailErr, - expectedErr: resolveFailErr, - }, - { - description: "Invalid Token Error", - value: "abcd", - parseCalled: true, - resolveCalled: true, - validToken: false, - expectedErr: ErrInvalidToken, - }, - { - description: "Convert to Claims Error", - value: "abcd", - parseCalled: true, - resolveCalled: true, - validToken: true, - expectedErr: ErrUnexpectedClaims, - }, - { - description: "Get Principal Error", - value: "abcd", - parseCalled: true, - resolveCalled: true, - validToken: true, - claims: &bascule.ClaimsWithLeeway{}, - expectedErr: ErrInvalidPrincipal, - }, - { - description: "Non-string Principal Error", - value: "abcd", - parseCalled: true, - resolveCalled: true, - validToken: true, - claims: &bascule.ClaimsWithLeeway{ - MapClaims: jwt.MapClaims{jwtPrincipalKey: 55.0}, - }, - expectedErr: ErrInvalidPrincipal, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - r := new(MockResolver) - p := new(mockParser) - key := new(mockKey) - if tc.parseCalled { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, tc.claims) - token.Valid = tc.validToken - p.On("ParseJWT", mock.Anything, mock.Anything, mock.Anything).Return(token, tc.parseErr).Once() - } - if tc.resolveCalled { - r.On("Resolve", mock.Anything, mock.Anything).Return(key, tc.resolveErr).Once() - if tc.resolveErr == nil { - key.On("Public").Return(nil).Once() - } - } - btf := BearerTokenFactory{ - DefaultKeyID: "default key id", - Resolver: r, - Parser: p, - } - req := httptest.NewRequest("get", "/", nil) - token, err := btf.ParseAndValidate(context.Background(), req, "", tc.value) - assert.Equal(tc.expectedToken, token) - key.AssertExpectations(t) - if tc.expectedErr == nil || err == nil { - assert.Equal(tc.expectedErr, err) - } else { - assert.Contains(err.Error(), tc.expectedErr.Error()) - } - }) - } -} - -func TestProvideBearerTokenFactory(t *testing.T) { - type In struct { - fx.In - Options []COption `group:"bascule_constructor_options"` - } - - t.Run("Success", func(t *testing.T) { - result := In{} - require := require.New(t) - app := fx.New( - fx.Provide( - fx.Annotated{ - Name: "default_key_id", - Target: func() string { - return "default" - }, - }, - fx.Annotated{ - Name: "key_resolver", - Target: func() clortho.Resolver { - r := new(MockResolver) - return r - }, - }, - fx.Annotated{ - Name: "parser", - Target: func() bascule.JWTParser { - p := new(mockParser) - return p - }, - }, - fx.Annotated{ - Name: "jwt_leeway", - Target: func() bascule.Leeway { - return bascule.Leeway{EXP: 5} - }, - }, - func() (c sallust.Config) { - return sallust.Config{} - }, - ), - sallust.WithLogger(), - ProvideBearerTokenFactory(false), - fx.Invoke( - func(in In) { - result = in - }, - ), - ) - err := app.Err() - require.NoError(err) - require.NotEmpty(result.Options) - require.NotNil(result.Options[0]) - }) -} - -func TestOptionalProvideBearerTokenFactory(t *testing.T) { - type In struct { - fx.In - Options []COption `group:"bascule_constructor_options"` - } - - t.Run("Silent failure", func(t *testing.T) { - result := In{} - require := require.New(t) - app := fx.New( - fx.Provide( - func() (c sallust.Config) { - return sallust.Config{} - }, - ), - sallust.WithLogger(), - ProvideBearerTokenFactory(true), - fx.Invoke( - func(in In) { - result = in - }, - ), - ) - err := app.Err() - require.NoError(err) - require.NotEmpty(result.Options) - require.Nil(result.Options[0]) - }) -} diff --git a/basculehttp/chain.go b/basculehttp/chain.go deleted file mode 100644 index abae007..0000000 --- a/basculehttp/chain.go +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "github.com/justinas/alice" - "go.uber.org/fx" -) - -// MetricListenerIn is used for uber fx wiring. -type MetricListenerIn struct { - fx.In - M *MetricListener `name:"bascule_metric_listener"` -} - -// ChainIn is used for uber fx wiring. -type ChainIn struct { - fx.In - SetLogger alice.Constructor `name:"alice_set_logger"` - Constructor alice.Constructor `name:"alice_constructor"` - Enforcer alice.Constructor `name:"alice_enforcer"` - Listener alice.Constructor `name:"alice_listener"` - SetLoggerInfo alice.Constructor `name:"alice_set_logger_info"` -} - -// Build provides the alice constructors chained together in a set order. -func (c ChainIn) Build() alice.Chain { - return alice.New(c.SetLogger, c.Constructor, c.Enforcer, c.Listener, c.SetLoggerInfo) -} - -// ProvideServerChain builds the alice middleware and then provides them -// together in a single alice chain. -func ProvideServerChain() fx.Option { - return fx.Options( - ProvideLogger(), - ProvideMetricListener(), - ProvideEnforcer(), - ProvideConstructor(), - fx.Provide( - fx.Annotated{ - Name: "auth_chain", - Target: func(in ChainIn) alice.Chain { - return in.Build() - }, - }, - )) -} diff --git a/v2/basculehttp/challenge.go b/basculehttp/challenge.go similarity index 86% rename from v2/basculehttp/challenge.go rename to basculehttp/challenge.go index d788db0..b81e033 100644 --- a/v2/basculehttp/challenge.go +++ b/basculehttp/challenge.go @@ -7,11 +7,14 @@ import ( "net/http" "strings" - "github.com/xmidt-org/bascule/v2" + "github.com/xmidt-org/bascule/v1" ) const ( - BasicScheme bascule.Scheme = "Basic" + // BasicScheme is the name of the basic HTTP authentication scheme. + BasicScheme bascule.Scheme = "Basic" + + // BearerScheme is the name of the bearer HTTP authentication scheme. BearerScheme bascule.Scheme = "Bearer" // WwwAuthenticateHeaderName is the HTTP header used for StatusUnauthorized challenges. @@ -36,20 +39,24 @@ type Challenge interface { // a StatusUnauthorized response. type Challenges []Challenge +// Add appends challenges to this set. +func (chs *Challenges) Add(ch ...Challenge) { + if *chs == nil { + *chs = make(Challenges, 0, len(ch)) + } + + *chs = append(*chs, ch...) +} + // WriteHeader inserts one WWW-Authenticate header per challenge in this set. // If this set is empty, the given http.Header is not modified. -// -// This method returns the count of headers added, which will be zero (0) for -// an empty Challenges. -func (chs Challenges) WriteHeader(h http.Header) int { +func (chs Challenges) WriteHeader(h http.Header) { var o strings.Builder for _, ch := range chs { ch.FormatAuthenticate(o) h.Add(WwwAuthenticateHeaderName, o.String()) o.Reset() } - - return len(chs) } // BasicChallenge represents a WWW-Authenticate basic auth challenge. diff --git a/basculehttp/constructor.go b/basculehttp/constructor.go deleted file mode 100644 index dc0337b..0000000 --- a/basculehttp/constructor.go +++ /dev/null @@ -1,234 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - - "github.com/justinas/alice" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/sallust" - "go.uber.org/fx" - "go.uber.org/zap" -) - -const ( - // DefaultHeaderName is the http header to get the authorization - // information from. - DefaultHeaderName = "Authorization" - - // DefaultHeaderDelimiter is the character between the authorization and - // its key. - DefaultHeaderDelimiter = " " - - // BasicAuthorization follows the RFC spec for Oauth 2.0 and is a canonical - // MIME header for Basic Authorization. - BasicAuthorization bascule.Authorization = "Basic" - - // BasicAuthorization follows the RFC spec for Oauth 2.0 and is a canonical - // MIME header for Basic Authorization. - BearerAuthorization bascule.Authorization = "Bearer" -) - -var ( - errNoAuthHeader = errors.New("no authorization header") - errBadAuthHeader = errors.New("unexpected authorization header value") - errKeyNotSupported = errors.New("key not supported") -) - -// TokenFactory is a strategy interface responsible for creating and validating -// a secure Token. -type TokenFactory interface { - ParseAndValidate(context.Context, *http.Request, bascule.Authorization, string) (bascule.Token, error) -} - -// COption is any function that modifies the constructor - used to configure -// the constructor. -type COption func(*constructor) - -// COptionsIn is the uber.fx wired struct needed to group together the -// options for the bascule constructor middleware, which does initial parsing -// of the auth provided. -type COptionsIn struct { - fx.In - Options []COption `group:"bascule_constructor_options"` -} - -type constructor struct { - headerName string - headerDelimiter string - authorizations map[bascule.Authorization]TokenFactory - getLogger func(context.Context) *zap.Logger - parseURL ParseURL - onErrorResponse OnErrorResponse - onErrorHTTPResponse OnErrorHTTPResponse -} - -func (c *constructor) authenticationOutput(logger *zap.Logger, request *http.Request) (bascule.Authentication, ErrorResponseReason, error) { - urlVal := *request.URL // copy the URL before modifying it - u, err := c.parseURL(&urlVal) - if err != nil { - return bascule.Authentication{}, GetURLFailed, fmt.Errorf("failed to parse url '%v': %v", request.URL, err) - } - authorization := request.Header.Get(c.headerName) - if len(authorization) == 0 { - return bascule.Authentication{}, MissingHeader, errNoAuthHeader - } - i := strings.Index(authorization, c.headerDelimiter) - if i < 1 { - return bascule.Authentication{}, InvalidHeader, errBadAuthHeader - } - - key := bascule.Authorization(authorization[:i]) - tf, supported := c.authorizations[key] - if !supported { - return bascule.Authentication{}, KeyNotSupported, fmt.Errorf("%w: [%v]", errKeyNotSupported, key) - } - - ctx := request.Context() - token, err := tf.ParseAndValidate(ctx, request, key, authorization[i+len(c.headerDelimiter):]) - if err != nil { - return bascule.Authentication{}, ParseFailed, fmt.Errorf("failed to parse and validate token: %v", err) - } - - return bascule.Authentication{ - Authorization: key, - Token: token, - Request: bascule.Request{ - URL: u, - Method: request.Method, - }, - }, -1, nil -} - -func (c *constructor) decorate(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logger := c.getLogger(r.Context()) - if logger == nil { - logger = sallust.Get(r.Context()) - } - auth, errReason, err := c.authenticationOutput(logger, r) - if err != nil { - logger.Error(err.Error(), zap.String("auth", r.Header.Get(c.headerName))) - c.onErrorResponse(errReason, err) - c.onErrorHTTPResponse(w, errReason) - return - } - ctx := bascule.WithAuthentication(r.Context(), auth) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -// NewConstructor creates an Alice-style decorator function that acts as -// middleware: parsing the http request to get a Token, which is added to the -// context. -func NewConstructor(options ...COption) func(http.Handler) http.Handler { - c := &constructor{ - headerName: DefaultHeaderName, - headerDelimiter: DefaultHeaderDelimiter, - authorizations: make(map[bascule.Authorization]TokenFactory), - getLogger: sallust.Get, - parseURL: DefaultParseURLFunc, - onErrorResponse: DefaultOnErrorResponse, - onErrorHTTPResponse: DefaultOnErrorHTTPResponse, - } - - for _, o := range options { - if o == nil { - continue - } - o(c) - } - - return c.decorate -} - -// WithHeaderName sets the headername and verifies it's valid. The headername -// is the name of the header to get the authorization information from. -func WithHeaderName(headerName string) COption { - return func(c *constructor) { - if len(headerName) > 0 { - c.headerName = headerName - } - } -} - -// WithHeaderDelimiter sets the value expected between the authorization key and token. -func WithHeaderDelimiter(delimiter string) COption { - return func(c *constructor) { - if len(delimiter) > 0 { - c.headerDelimiter = delimiter - } - } -} - -// WithTokenFactory sets the TokenFactory for the constructor to use. -func WithTokenFactory(key bascule.Authorization, tf TokenFactory) COption { - return func(c *constructor) { - if tf != nil { - c.authorizations[key] = tf - } - } -} - -// WithCLogger sets the function to use to get the logger from the context. -// If no logger is set, nothing is logged. -func WithCLogger(getLogger func(context.Context) *zap.Logger) COption { - return func(c *constructor) { - if getLogger != nil { - c.getLogger = getLogger - } - } -} - -// WithParseURLFunc sets the function to use to make any changes to the URL -// before it is added to the context. -func WithParseURLFunc(parseURL ParseURL) COption { - return func(c *constructor) { - if parseURL != nil { - c.parseURL = parseURL - } - } -} - -// WithCErrorResponseFunc sets the function that is called when an error occurs. -func WithCErrorResponseFunc(f OnErrorResponse) COption { - return func(c *constructor) { - if f != nil { - c.onErrorResponse = f - } - } -} - -// WithCErrorHTTPResponseFunc sets the function whose job is to translate -// bascule errors into the appropriate HTTP response. -func WithCErrorHTTPResponseFunc(f OnErrorHTTPResponse) COption { - return func(c *constructor) { - if f != nil { - c.onErrorHTTPResponse = f - } - } -} - -// ProvideConstructor is a helper function for wiring up a basculehttp -// constructor with uber fx. Any options or optional values added with uber fx -// will be used to create the constructor. -func ProvideConstructor() fx.Option { - return fx.Options( - ProvideOnErrorHTTPResponse(), - ProvideParseURL(), - fx.Provide( - fx.Annotated{ - Name: "alice_constructor", - Target: func(in COptionsIn) alice.Constructor { - return NewConstructor(in.Options...) - }, - }, - ), - ) -} diff --git a/basculehttp/constructor_test.go b/basculehttp/constructor_test.go deleted file mode 100644 index 7c879ee..0000000 --- a/basculehttp/constructor_test.go +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/xmidt-org/sallust" -) - -func TestConstructor(t *testing.T) { - testHeader := "test header" - testDelimiter := "=" - - c := NewConstructor( - WithHeaderName(testHeader), - WithHeaderDelimiter(testDelimiter), - nil, - WithTokenFactory("Basic", BasicTokenFactory{"codex": "codex"}), - WithCLogger(sallust.Get), - WithParseURLFunc(CreateRemovePrefixURLFunc("/test", DefaultParseURLFunc)), - WithCErrorResponseFunc(DefaultOnErrorResponse), - WithCErrorHTTPResponseFunc(LegacyOnErrorHTTPResponse), - ) - c2 := NewConstructor( - WithHeaderName(""), - WithHeaderDelimiter(""), - WithCLogger(sallust.Get), - WithParseURLFunc(CreateRemovePrefixURLFunc("", nil)), - ) - tests := []struct { - description string - constructor func(http.Handler) http.Handler - requestHeaderKey string - requestHeaderValue string - expectedStatusCode int - endpoint string - }{ - { - description: "Success", - constructor: c, - requestHeaderKey: testHeader, - requestHeaderValue: "Basic=Y29kZXg6Y29kZXg=", - expectedStatusCode: http.StatusOK, - endpoint: "/test", - }, - { - description: "URL Parsing Error", - constructor: c, - endpoint: "/blah", - expectedStatusCode: http.StatusForbidden, - }, - { - description: "No Authorization Header Error", - constructor: c2, - requestHeaderKey: DefaultHeaderName, - requestHeaderValue: "", - expectedStatusCode: http.StatusUnauthorized, - endpoint: "/", - }, - { - description: "No Space in Auth Header Error", - constructor: c, - requestHeaderKey: testHeader, - requestHeaderValue: "abcd", - expectedStatusCode: http.StatusBadRequest, - endpoint: "/test", - }, - { - description: "Key Not Supported Error", - constructor: c2, - requestHeaderKey: DefaultHeaderName, - requestHeaderValue: "abcd ", - expectedStatusCode: http.StatusUnauthorized, - endpoint: "/test", - }, - { - description: "Key Wrong Case Error", - constructor: c, - requestHeaderKey: testHeader, - requestHeaderValue: "bAsIc=Y29kZXg6Y29kZXg=", - expectedStatusCode: http.StatusForbidden, - endpoint: "/test", - }, - { - description: "Parse and Validate Error", - constructor: c, - requestHeaderKey: testHeader, - requestHeaderValue: "Basic=AFJDK", - expectedStatusCode: http.StatusForbidden, - endpoint: "/test", - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - handler := tc.constructor(next) - - writer := httptest.NewRecorder() - req := httptest.NewRequest("get", tc.endpoint, nil) - req.Header.Add(tc.requestHeaderKey, tc.requestHeaderValue) - handler.ServeHTTP(writer, req) - assert.Equal(tc.expectedStatusCode, writer.Code) - if tc.expectedStatusCode == http.StatusUnauthorized { - assert.Equal(string(BearerAuthorization), writer.Header().Get(AuthTypeHeaderKey)) - } - }) - } -} diff --git a/basculehttp/credentials.go b/basculehttp/credentials.go new file mode 100644 index 0000000..fc1b46a --- /dev/null +++ b/basculehttp/credentials.go @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "strings" + + "github.com/xmidt-org/bascule/v1" +) + +// fastIsSpace tests an ASCII byte to see if it's whitespace. +// HTTP headers are restricted to US-ASCII, so we don't need +// the full unicode stack. +func fastIsSpace(b byte) bool { + return b == ' ' || b == '\t' || b == '\n' || b == '\r' || b == '\v' || b == '\f' +} + +var defaultCredentialsParser bascule.CredentialsParser = bascule.CredentialsParserFunc( + func(raw string) (c bascule.Credentials, err error) { + // format is + // the code is strict: it requires no leading or trailing space + // and exactly one (1) space as a separator. + scheme, value, found := strings.Cut(raw, " ") + if found && len(scheme) > 0 && !fastIsSpace(value[0]) && !fastIsSpace(value[len(value)-1]) { + c = bascule.Credentials{ + Scheme: bascule.Scheme(scheme), + Value: value, + } + } else { + err = &bascule.InvalidCredentialsError{ + Raw: raw, + } + } + + return + }, +) + +// DefaultCredentialsParser returns the default strategy for parsing credentials. This +// builtin strategy is very strict on whitespace. The format must correspond exactly +// to the format specified in https://www.rfc-editor.org/rfc/rfc7235. +func DefaultCredentialsParser() bascule.CredentialsParser { + return defaultCredentialsParser +} diff --git a/basculehttp/credentials_test.go b/basculehttp/credentials_test.go new file mode 100644 index 0000000..ef19078 --- /dev/null +++ b/basculehttp/credentials_test.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "github.com/xmidt-org/bascule/v1" +) + +type CredentialsTestSuite struct { + suite.Suite +} + +func (suite *CredentialsTestSuite) testDefaultCredentialsParserSuccess() { + const ( + expectedScheme bascule.Scheme = "Test" + expectedValue = "credentialValue" + ) + + testCases := []string{ + "Test credentialValue", + } + + for _, testCase := range testCases { + suite.Run(testCase, func() { + dp := DefaultCredentialsParser() + suite.Require().NotNil(dp) + + creds, err := dp.Parse(testCase) + suite.Require().NoError(err) + suite.Equal( + bascule.Credentials{ + Scheme: expectedScheme, + Value: expectedValue, + }, + creds, + ) + }) + } +} + +func (suite *CredentialsTestSuite) testDefaultCredentialsParserFailure() { + const ( + expectedScheme bascule.Scheme = "Test" + expectedValue = "credentialValue" + ) + + testCases := []string{ + "", + " ", + "thisisnotvalid", + "Test\tcredentialValue", + " Test credentialValue", + "Test credentialValue ", + "Test credentialValue", + } + + for _, testCase := range testCases { + suite.Run(testCase, func() { + dp := DefaultCredentialsParser() + suite.Require().NotNil(dp) + + creds, err := dp.Parse(testCase) + suite.Require().Error(err) + suite.Equal(bascule.Credentials{}, creds) + + var ice *bascule.InvalidCredentialsError + if suite.ErrorAs(err, &ice) { + suite.Equal(testCase, ice.Raw) + } + }) + } +} + +func (suite *CredentialsTestSuite) TestDefaultCredentialsParser() { + suite.Run("Success", suite.testDefaultCredentialsParserSuccess) + suite.Run("Failure", suite.testDefaultCredentialsParserFailure) +} + +func TestCredentials(t *testing.T) { + suite.Run(t, new(CredentialsTestSuite)) +} diff --git a/basculehttp/doc.go b/basculehttp/doc.go index 457f9fe..cf5bca5 100644 --- a/basculehttp/doc.go +++ b/basculehttp/doc.go @@ -2,9 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /* -Package basculehttp provides Alice-style http middleware that parses a Token -from an http header, validates the Token, and allows for the consumer to add -additional logs or metrics upon an error or a valid Token. The package contains -listener middleware that tracks if requests were authorized or not. +Package basculehttp provides a token-based security workflow for HTTP handlers +using bascule. */ package basculehttp diff --git a/basculehttp/enforcer.go b/basculehttp/enforcer.go deleted file mode 100644 index 784e692..0000000 --- a/basculehttp/enforcer.go +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "context" - "errors" - "net/http" - - "github.com/justinas/alice" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/sallust" - "go.uber.org/fx" - "go.uber.org/zap" -) - -//go:generate stringer -type=NotFoundBehavior - -// NotFoundBehavior is an enum that specifies what to do when the -// Authorization used isn't found in the map of rules. -type NotFoundBehavior int - -const ( - Forbid NotFoundBehavior = iota - Allow -) - -// EOption is any function that modifies the enforcer - used to configure -// the enforcer. -type EOption func(*enforcer) - -// EOptionsIn is the uber.fx wired struct needed to group together the options -// for the bascule enforcer middleware, which runs checks against the token. -type EOptionsIn struct { - fx.In - Options []EOption `group:"bascule_enforcer_options"` -} - -type enforcer struct { - notFoundBehavior NotFoundBehavior - rules map[bascule.Authorization]bascule.Validator - getLogger func(context.Context) *zap.Logger - onErrorResponse OnErrorResponse -} - -func (e *enforcer) decorate(next http.Handler) http.Handler { - return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - ctx := request.Context() - logger := e.getLogger(ctx) - if logger == nil { - logger = sallust.Get(ctx) - } - auth, ok := bascule.FromContext(ctx) - if !ok { - err := errors.New("no authentication found") - logger.Error(err.Error()) - e.onErrorResponse(MissingAuthentication, err) - response.WriteHeader(http.StatusForbidden) - return - } - rules, ok := e.rules[auth.Authorization] - if !ok { - err := errors.New("no rules found for authorization") - logger.Error(err.Error(), zap.Any("rules", rules), - zap.String("authorization", string(auth.Authorization)), zap.Int("behavior", int(e.notFoundBehavior))) - switch e.notFoundBehavior { - case Forbid: - e.onErrorResponse(ChecksNotFound, err) - response.WriteHeader(http.StatusForbidden) - return - case Allow: - // continue - default: - e.onErrorResponse(ChecksNotFound, err) - response.WriteHeader(http.StatusForbidden) - return - } - } else { - err := rules.Check(ctx, auth.Token) - if err != nil { - logger.Error(err.Error()) - e.onErrorResponse(ChecksFailed, err) - WriteResponse(response, http.StatusForbidden, err) - return - } - } - logger.Debug("authentication accepted by enforcer") - next.ServeHTTP(response, request) - }) -} - -// NewListenerDecorator creates an Alice-style decorator function that acts as -// middleware, allowing for Listeners to be called after a token has been -// authenticated. -func NewEnforcer(options ...EOption) func(http.Handler) http.Handler { - e := &enforcer{ - rules: make(map[bascule.Authorization]bascule.Validator), - getLogger: sallust.Get, - onErrorResponse: DefaultOnErrorResponse, - } - - for _, o := range options { - if o == nil { - continue - } - o(e) - } - - return e.decorate -} - -// WithNotFoundBehavior sets the behavior upon not finding the Authorization -// value in the rules map. -func WithNotFoundBehavior(behavior NotFoundBehavior) EOption { - return func(e *enforcer) { - if behavior > 0 { - e.notFoundBehavior = behavior - } - } -} - -// WithRules sets the validator to be used for a given Authorization value. -func WithRules(key bascule.Authorization, v bascule.Validator) EOption { - return func(e *enforcer) { - if v != nil { - e.rules[key] = v - } - } -} - -// WithELogger sets the function to use to get the logger from the context. -// If no logger is set, nothing is logged. -func WithELogger(getLogger func(context.Context) *zap.Logger) EOption { - return func(e *enforcer) { - if getLogger != nil { - e.getLogger = getLogger - } - } -} - -// WithEErrorResponseFunc sets the function that is called when an error occurs. -func WithEErrorResponseFunc(f OnErrorResponse) EOption { - return func(e *enforcer) { - if f != nil { - e.onErrorResponse = f - } - } -} - -// ProvideEnforcer is a helper function for wiring up an enforcer with uber fx. -// Any options added with uber fx will be used to create the enforcer. -func ProvideEnforcer() fx.Option { - return fx.Provide( - fx.Annotated{ - Name: "alice_enforcer", - Target: func(in EOptionsIn) alice.Constructor { - return NewEnforcer(in.Options...) - }, - }, - ) -} diff --git a/basculehttp/enforcer_test.go b/basculehttp/enforcer_test.go deleted file mode 100644 index 5584e1c..0000000 --- a/basculehttp/enforcer_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/bascule/basculechecks" - "github.com/xmidt-org/sallust" -) - -func TestEnforcer(t *testing.T) { - e := NewEnforcer( - WithNotFoundBehavior(Allow), - WithELogger(sallust.Get), - ) - e2 := NewEnforcer( - WithRules("jwt", bascule.Validators{basculechecks.NonEmptyType()}), - WithELogger(sallust.Get), - WithEErrorResponseFunc(DefaultOnErrorResponse), - ) - emptyAttributes := bascule.NewAttributes(map[string]interface{}{}) - tests := []struct { - description string - enforcer func(http.Handler) http.Handler - noAuth bool - auth bascule.Authentication - expectedStatusCode int - }{ - { - description: "Success", - enforcer: e2, - auth: bascule.Authentication{ - Authorization: "jwt", - Token: bascule.NewToken("test", "", emptyAttributes), - }, - expectedStatusCode: http.StatusOK, - }, - { - description: "No Auth Error", - enforcer: e2, - noAuth: true, - expectedStatusCode: http.StatusForbidden, - }, - { - description: "Forbid Error", - enforcer: e2, - auth: bascule.Authentication{Authorization: "test"}, - expectedStatusCode: http.StatusForbidden, - }, - { - description: "Allow Success", - enforcer: e, - auth: bascule.Authentication{Authorization: "test"}, - expectedStatusCode: http.StatusOK, - }, - { - description: "Rule Check Error", - enforcer: e2, - auth: bascule.Authentication{ - Authorization: "jwt", - Token: bascule.NewToken("", "", emptyAttributes), - }, - expectedStatusCode: http.StatusForbidden, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - handler := tc.enforcer(next) - - writer := httptest.NewRecorder() - req := httptest.NewRequest("get", "/", nil) - if !tc.noAuth { - req = req.WithContext(bascule.WithAuthentication(context.Background(), tc.auth)) - } - handler.ServeHTTP(writer, req) - assert.Equal(tc.expectedStatusCode, writer.Code) - }) - } -} diff --git a/basculehttp/error.go b/basculehttp/error.go new file mode 100644 index 0000000..ac92447 --- /dev/null +++ b/basculehttp/error.go @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "encoding" + "encoding/json" + "errors" + "net/http" +) + +// ErrorStatusCoder is a strategy for determining the HTTP response code for an error. +// +// The defaultCode is used when this strategy cannot determine the code from the error. +// This default can be a sentinel for decorators, e.g. zero (0), or can be an actual +// status code. +type ErrorStatusCoder func(request *http.Request, defaultCode int, err error) int + +// DefaultErrorStatusCoder is the strategy used when no ErrorStatusCoder is supplied. +// This function examines err to see if it or any wrapped error provides a StatusCode() +// method. If found, Status() is used. Otherwise, this function returns the default code. +// +// This function can also be decorated. Passing a sentinel value for defaultCode allows +// a decorator to take further action. +func DefaultErrorStatusCoder(_ *http.Request, defaultCode int, err error) int { + type statusCoder interface { + StatusCode() int + } + + var sc statusCoder + if errors.As(err, &sc) { + return sc.StatusCode() + } + + return defaultCode +} + +// ErrorMarshaler is a strategy for marshaling an error's contents, particularly to +// be used in an HTTP response body. +type ErrorMarshaler func(request *http.Request, err error) (contentType string, content []byte, marshalErr error) + +// DefaultErrorMarshaler examines the error for several standard marshalers. The supported marshalers +// together with the returned content types are as follows, in order: +// +// - json.Marshaler "application/json" +// - encoding.TextMarshaler "text/plain; charset=utf-8" +// - encoding.BinaryMarshaler "application/octet-stream" +// +// If the error or any of its wrapped errors does not implement a supported marshaler interface, +// the error's Error() text is used with a content type of "text/plain; charset=utf-8". +func DefaultErrorMarshaler(_ *http.Request, err error) (contentType string, content []byte, marshalErr error) { + // walk the wrapped errors manually, since that's way more efficient + // that walking the error tree once for each desired type + for wrapped := err; wrapped != nil && len(content) == 0 && marshalErr == nil; wrapped = errors.Unwrap(wrapped) { + switch m := wrapped.(type) { //nolint: errorlint + case json.Marshaler: + contentType = "application/json" + content, marshalErr = m.MarshalJSON() + + case encoding.TextMarshaler: + contentType = "text/plain; charset=utf-8" + content, marshalErr = m.MarshalText() + + case encoding.BinaryMarshaler: + contentType = "application/octet-stream" + content, marshalErr = m.MarshalBinary() + } + } + + if len(content) == 0 && marshalErr == nil { + // fallback + contentType = "text/plain; charset=utf-8" + content = []byte(err.Error()) + } + + return +} + +type statusCodeError struct { + error + statusCode int +} + +func (err *statusCodeError) StatusCode() int { + return err.statusCode +} + +// UseStatusCode associates an HTTP status code with the given error. +// This function will override any existing status code associated with err. +func UseStatusCode(statusCode int, err error) error { + return &statusCodeError{ + error: err, + statusCode: statusCode, + } +} diff --git a/basculehttp/errorResponse.go b/basculehttp/errorResponse.go deleted file mode 100644 index 1b72cbb..0000000 --- a/basculehttp/errorResponse.go +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "net/http" - - "go.uber.org/fx" -) - -// AuthTypeHeaderKey is the header key that's used when requests are denied -// with a 401 status code. It specifies the suggested token type that should -// be used for a successful request. -const AuthTypeHeaderKey = "WWW-Authenticate" - -// OnErrorResponse is a function that takes the error response reason and the -// error and can do something with it. This is useful for adding additional -// metrics or logs. -type OnErrorResponse func(ErrorResponseReason, error) - -// default function does nothing -func DefaultOnErrorResponse(_ ErrorResponseReason, _ error) {} - -// OnErrorHTTPResponse allows users to decide what the response should be -// for a given reason. -type OnErrorHTTPResponse func(http.ResponseWriter, ErrorResponseReason) - -// OnErrorHTTPResponseIn is uber fx wiring allowing for OnErrorHTTPResponse to -// be optional. -type OnErrorHTTPResponseIn struct { - fx.In - R OnErrorHTTPResponse `optional:"true"` -} - -// DefaultOnErrorHTTPResponse will write a 401 status code along the -// 'WWW-Authenticate: Bearer' header for all error cases related to building -// the security token. For error checks that happen once a valid token has been -// created will result in a 403. -func DefaultOnErrorHTTPResponse(w http.ResponseWriter, reason ErrorResponseReason) { - switch reason { - case ChecksNotFound, ChecksFailed: - w.WriteHeader(http.StatusForbidden) - default: - w.Header().Set(AuthTypeHeaderKey, string(BearerAuthorization)) - w.WriteHeader(http.StatusUnauthorized) - } -} - -// LegacyOnErrorHTTPResponse will write a 403 status code back for any error -// reason except for InvalidHeader for which a 400 is written. -func LegacyOnErrorHTTPResponse(w http.ResponseWriter, reason ErrorResponseReason) { - switch reason { - case InvalidHeader: - w.WriteHeader(http.StatusBadRequest) - default: - w.WriteHeader(http.StatusForbidden) - } -} - -// ProvideOnErrorHTTPResponse creates the constructor option to include an -// OnErrorHTTPResponse function if it is provided. -func ProvideOnErrorHTTPResponse() fx.Option { - return fx.Provide( - fx.Annotated{ - Group: "bascule_constructor_options", - Target: func(in OnErrorHTTPResponseIn) COption { - return WithCErrorHTTPResponseFunc(in.R) - }, - }, - ) -} diff --git a/basculehttp/errorResponseReason.go b/basculehttp/errorResponseReason.go deleted file mode 100644 index d5ad65e..0000000 --- a/basculehttp/errorResponseReason.go +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -// ErrorResponseReason is an enum that specifies the reason parsing/validating -// a token failed. Its primary use is for metrics and logging. -type ErrorResponseReason int - -const ( - Unknown ErrorResponseReason = iota - MissingHeader - InvalidHeader - KeyNotSupported - ParseFailed - GetURLFailed - MissingAuthentication - ChecksNotFound - ChecksFailed -) - -const ( - UnknownReason = "unknown" -) - -var responseReasonMarshal = map[ErrorResponseReason]string{ - MissingHeader: "missing_header", - InvalidHeader: "invalid_header", - KeyNotSupported: "key_not_supported", - ParseFailed: "parse_failed", - GetURLFailed: "get_url_failed", - MissingAuthentication: "missing_authentication", - ChecksNotFound: "checks_not_found", - ChecksFailed: "checks_failed", -} - -// String provides a metric label safe string of the response reason. -func (e ErrorResponseReason) String() string { - reason, ok := responseReasonMarshal[e] - if !ok { - return UnknownReason - } - return reason -} diff --git a/basculehttp/errorResponseReason_test.go b/basculehttp/errorResponseReason_test.go deleted file mode 100644 index 1a1c06f..0000000 --- a/basculehttp/errorResponseReason_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestErrorResponseReasonStr(t *testing.T) { - tests := []struct { - reason ErrorResponseReason - expectedString string - }{ - { - reason: MissingHeader, - expectedString: "missing_header", - }, - { - reason: InvalidHeader, - expectedString: "invalid_header", - }, - { - reason: KeyNotSupported, - expectedString: "key_not_supported", - }, - { - reason: ParseFailed, - expectedString: "parse_failed", - }, - { - reason: GetURLFailed, - expectedString: "get_url_failed", - }, - { - reason: MissingAuthentication, - expectedString: "missing_authentication", - }, - { - reason: ChecksNotFound, - expectedString: "checks_not_found", - }, - { - reason: ChecksFailed, - expectedString: "checks_failed", - }, - { - reason: -1, - expectedString: UnknownReason, - }, - { - reason: 0, - expectedString: UnknownReason, - }, - { - reason: 1000, - expectedString: UnknownReason, - }, - } - for _, tc := range tests { - t.Run(fmt.Sprintf("%v %v", tc.reason, tc.expectedString), - func(t *testing.T) { - r := tc.reason.String() - assert.Equal(t, tc.expectedString, r) - }) - } -} diff --git a/basculehttp/errorResponse_test.go b/basculehttp/errorResponse_test.go deleted file mode 100644 index 74e57b1..0000000 --- a/basculehttp/errorResponse_test.go +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDefaultOnErrorHTTPResponse(t *testing.T) { - tcs := []struct { - Description string - Reason ErrorResponseReason - ExpectAuthTypeHeader bool - ExpectedCode int - }{ - { - Description: "MissingHeader", - Reason: MissingHeader, - ExpectedCode: 401, - ExpectAuthTypeHeader: true, - }, - { - Description: "InvalidHeader", - Reason: InvalidHeader, - ExpectedCode: 401, - ExpectAuthTypeHeader: true, - }, - { - Description: "KeyNotSupported", - Reason: KeyNotSupported, - ExpectedCode: 401, - ExpectAuthTypeHeader: true, - }, - { - Description: "ParseFailed", - Reason: ParseFailed, - ExpectedCode: 401, - ExpectAuthTypeHeader: true, - }, - { - Description: "GetURLFailed", - Reason: GetURLFailed, - ExpectedCode: 401, - ExpectAuthTypeHeader: true, - }, - { - Description: "MissingAuth", - Reason: MissingAuthentication, - ExpectedCode: 401, - ExpectAuthTypeHeader: true, - }, - { - Description: "ChecksNotFound", - Reason: ChecksNotFound, - ExpectedCode: 403, - ExpectAuthTypeHeader: false, - }, - { - Description: "ChecksFailed", - Reason: ChecksFailed, - ExpectedCode: 403, - ExpectAuthTypeHeader: false, - }, - } - - for _, tc := range tcs { - t.Run(tc.Description, func(t *testing.T) { - assert := assert.New(t) - - recorder := httptest.NewRecorder() - DefaultOnErrorHTTPResponse(recorder, tc.Reason) - assert.Equal(tc.ExpectedCode, recorder.Code) - - authType := recorder.Header().Get(AuthTypeHeaderKey) - if tc.ExpectAuthTypeHeader { - assert.Equal(string(BearerAuthorization), authType) - } else { - assert.Empty(authType) - } - }) - } -} - -func TestLegacyOnErrorHTTPResponse(t *testing.T) { - tcs := []struct { - Description string - Reason ErrorResponseReason - ExpectedCode int - }{ - { - Description: "MissingHeader", - Reason: MissingHeader, - ExpectedCode: 403, - }, - { - Description: "InvalidHeader", - Reason: InvalidHeader, - ExpectedCode: 400, - }, - { - Description: "KeyNotSupported", - Reason: KeyNotSupported, - ExpectedCode: 403, - }, - { - Description: "ParseFailed", - Reason: ParseFailed, - ExpectedCode: 403, - }, - { - Description: "GetURLFailed", - Reason: GetURLFailed, - ExpectedCode: 403, - }, - { - Description: "MissingAuth", - Reason: MissingAuthentication, - ExpectedCode: 403, - }, - { - Description: "ChecksNotFound", - Reason: ChecksNotFound, - ExpectedCode: 403, - }, - { - Description: "ChecksFailed", - Reason: ChecksFailed, - ExpectedCode: 403, - }, - } - - for _, tc := range tcs { - t.Run(tc.Description, func(t *testing.T) { - assert := assert.New(t) - recorder := httptest.NewRecorder() - LegacyOnErrorHTTPResponse(recorder, tc.Reason) - assert.Equal(tc.ExpectedCode, recorder.Code) - assert.Empty(recorder.Header().Get(AuthTypeHeaderKey)) - }) - } -} diff --git a/basculehttp/http.go b/basculehttp/http.go deleted file mode 100644 index 49e5506..0000000 --- a/basculehttp/http.go +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import "net/http" - -// statusCode follows the go-kit convention. Errors and other objects that implement -// this interface are allowed to supply an HTTP response status code. -type statusCoder interface { - StatusCode() int -} - -// headerer allows errors and other types to supply headers, mainly for writing -// HTTP responses. -type headerer interface { - Headers() http.Header -} - -// ErrorHeaderer implements headerer, allowing an error to supply http headers -// in an error response. -type ErrorHeaderer struct { - err error - headers http.Header -} - -// Error returns the error string. -func (e ErrorHeaderer) Error() string { - return e.err.Error() -} - -// Headers returns the stored http headers attached to the error. -func (e ErrorHeaderer) Headers() http.Header { - return e.headers -} - -// NewErrorHeaderer creates an ErrorHeaderer with the error and headers -// provided. -func NewErrorHeaderer(err error, headers map[string][]string) error { - return ErrorHeaderer{err: err, headers: headers} -} - -// WriteResponse performs some basic reflection on v to allow it to modify responses written -// to an HTTP response. Useful mainly for errors. -func WriteResponse(response http.ResponseWriter, defaultStatusCode int, v interface{}) { - if h, ok := v.(headerer); ok { - for name, values := range h.Headers() { - for _, value := range values { - response.Header().Add(name, value) - } - } - } - - status := defaultStatusCode - if s, ok := v.(statusCoder); ok { - status = s.StatusCode() - } - - response.WriteHeader(status) -} diff --git a/basculehttp/http_test.go b/basculehttp/http_test.go deleted file mode 100644 index 5d3bed6..0000000 --- a/basculehttp/http_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "errors" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestErrorHeaderer(t *testing.T) { - assert := assert.New(t) - expectedErr := "test error" - headers := map[string][]string{"test key": {"a", "b", "c", "d"}} - eh := NewErrorHeaderer(errors.New(expectedErr), headers) - var e headerer - assert.True(errors.As(eh, &e)) - err := eh.Error() - assert.Equal(expectedErr, err) - h := e.Headers() - assert.Equal(http.Header(headers), h) -} - -type coder int - -func (b coder) StatusCode() int { - return int(b) -} - -func TestWriteResponse(t *testing.T) { - assert := assert.New(t) - recorder := httptest.NewRecorder() - err := errors.New("test error") - headers := map[string][]string{"test key": {"a", "b", "c", "d"}} - WriteResponse(recorder, http.StatusOK, NewErrorHeaderer(err, headers)) - assert.Equal(http.StatusOK, recorder.Code) - assert.Equal(http.Header(headers), recorder.Header()) - recorder = httptest.NewRecorder() - c := coder(http.StatusForbidden) - WriteResponse(recorder, http.StatusBadRequest, c) - assert.Equal(http.StatusForbidden, recorder.Code) - assert.Equal(http.Header{}, recorder.Header()) -} diff --git a/basculehttp/listener.go b/basculehttp/listener.go deleted file mode 100644 index 7c80fc8..0000000 --- a/basculehttp/listener.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "net/http" - - "github.com/xmidt-org/bascule" -) - -// Listener is anything that takes the Authentication information of an -// authenticated Token. -type Listener interface { - OnAuthenticated(bascule.Authentication) -} - -type listenerDecorator struct { - listeners []Listener -} - -func (l *listenerDecorator) decorate(next http.Handler) http.Handler { - return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - ctx := request.Context() - auth, ok := bascule.FromContext(ctx) - if !ok { - response.WriteHeader(http.StatusForbidden) - return - } - for _, listener := range l.listeners { - listener.OnAuthenticated(auth) - } - next.ServeHTTP(response, request) - - }) -} - -// NewListenerDecorator creates an Alice-style decorator function that acts as -// middleware, allowing for Listeners to be called after a token has been -// authenticated. -func NewListenerDecorator(listeners ...Listener) func(http.Handler) http.Handler { - l := &listenerDecorator{} - - l.listeners = append(l.listeners, listeners...) - return l.decorate -} diff --git a/basculehttp/listener_test.go b/basculehttp/listener_test.go deleted file mode 100644 index 6abc2ed..0000000 --- a/basculehttp/listener_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "context" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/xmidt-org/bascule" -) - -var ( - next = http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { - writer.WriteHeader(http.StatusOK) - }) -) - -func TestListenerDecorator(t *testing.T) { - assert := assert.New(t) - mockListener := new(mockListener) - mockListener.On("OnAuthenticated", mock.Anything).Once() - f := NewListenerDecorator(mockListener) - handler := f(next) - - writer := httptest.NewRecorder() - req := httptest.NewRequest("get", "/", nil) - handler.ServeHTTP(writer, req) - assert.Equal(http.StatusForbidden, writer.Code) - - u, err := url.ParseRequestURI("/") - assert.NoError(err) - - ctx := bascule.WithAuthentication(context.Background(), bascule.Authentication{ - Authorization: "jwt", - Token: bascule.NewToken("", "", bascule.NewAttributes(map[string]interface{}{})), - Request: bascule.Request{ - URL: u, - Method: "get", - }, - }) - req = req.WithContext(ctx) - writer = httptest.NewRecorder() - handler.ServeHTTP(writer, req) - assert.Equal(http.StatusOK, writer.Code) - -} diff --git a/basculehttp/log.go b/basculehttp/log.go deleted file mode 100644 index 8c307e7..0000000 --- a/basculehttp/log.go +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "context" - "net" - "net/http" - "strings" - - "github.com/justinas/alice" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/candlelight" - "github.com/xmidt-org/sallust" - "go.uber.org/fx" - "go.uber.org/zap" -) - -func sanitizeHeaders(headers http.Header) (filtered http.Header) { - filtered = headers.Clone() - if authHeader := filtered.Get("Authorization"); authHeader != "" { - filtered.Del("Authorization") - parts := strings.Split(authHeader, " ") - if len(parts) == 2 { - filtered.Set("Authorization-Type", parts[0]) - } - } - return -} - -// SetLogger creates an alice constructor that sets up a zap logger that can be -// used for all logging related to the current request. The logger is added to -// the request's context. -func SetLogger(logger *zap.Logger) alice.Constructor { - return func(delegate http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - tid := r.Header.Get(candlelight.HeaderWPATIDKeyName) - if tid == "" { - tid = candlelight.GenTID() - } - - var source string - host, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - source = r.RemoteAddr - } else { - source = host - } - - l := logger.With( - zap.Any("request.Headers", sanitizeHeaders(r.Header)), //lgtm [go/clear-text-logging] - zap.String("request.URL", r.URL.EscapedPath()), - zap.String("request.Method", r.Method), - zap.String("request.address", source), - zap.String("request.path", r.URL.Path), - zap.String("request.query", r.URL.RawQuery), - zap.String("request.tid", tid), - ) - traceID, spanID, ok := candlelight.ExtractTraceInfo(r.Context()) - if ok { - l = l.With( - zap.String(candlelight.TraceIdLogKeyName, traceID), - zap.String(candlelight.SpanIDLogKeyName, spanID), - ) - } - r = r.WithContext(sallust.With(r.Context(), l)) - delegate.ServeHTTP(w, r) - }) - } -} - -// ProvideLogger provides functions that use zap loggers, getting from and -// setting to a context. The zap logger is translated into a go-kit logger for -// compatibility with the alice middleware. Options are also provided for the -// middleware so they can use the context logger. -func ProvideLogger() fx.Option { - return fx.Options( - fx.Supply(sallust.Get), - fx.Provide( - // set up middleware to add request-specific logger to context - fx.Annotated{ - Name: "alice_set_logger", - Target: SetLogger, - }, - - // add logger constructor option - fx.Annotated{ - Group: "bascule_constructor_options", - Target: func(getLogger func(context.Context) *zap.Logger) COption { - return WithCLogger(getLogger) - }, - }, - - // add logger enforcer option - fx.Annotated{ - Group: "bascule_enforcer_options", - Target: func(getLogger func(context.Context) *zap.Logger) EOption { - return WithELogger(getLogger) - }, - }, - - // add info to logger - fx.Annotated{ - Name: "alice_set_logger_info", - Target: SetBasculeInfo, - }, - ), - ) -} - -// SetBasculeInfo creates an alice constructor that takes -// the logger and bascule Auth and adds -// relevant bascule information to the logger before putting the logger -// back in the context. -func SetBasculeInfo() alice.Constructor { - return func(delegate http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - logger := sallust.Get(ctx) - - var satClientID = "N/A" - // retrieve satClientID from request context - if auth, ok := bascule.FromContext(r.Context()); ok { - satClientID = auth.Token.Principal() - } - - r = r.WithContext(sallust.With(ctx, logger.With(zap.String("satClientID", satClientID)))) - delegate.ServeHTTP(w, r) - }) - } -} diff --git a/basculehttp/log_test.go b/basculehttp/log_test.go deleted file mode 100644 index 01f9dba..0000000 --- a/basculehttp/log_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSanitizeHeaders(t *testing.T) { - testCases := []struct { - Description string - Input http.Header - Expected http.Header - }{ - { - Description: "Filtered", - Input: http.Header{"Authorization": []string{"Basic xyz"}, "HeaderA": []string{"x"}}, - Expected: http.Header{"HeaderA": []string{"x"}, "Authorization-Type": []string{"Basic"}}, - }, - { - Description: "Handled human error", - Input: http.Header{"Authorization": []string{"BasicXYZ"}, "HeaderB": []string{"y"}}, - Expected: http.Header{"HeaderB": []string{"y"}}, - }, - { - Description: "Not a perfect system", - Input: http.Header{"Authorization": []string{"MySecret IWantToLeakIt"}}, - Expected: http.Header{"Authorization-Type": []string{"MySecret"}}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Description, func(t *testing.T) { - assert := assert.New(t) - actual := sanitizeHeaders(tc.Input) - assert.Equal(tc.Expected, actual) - }) - - } -} diff --git a/basculehttp/metricListener.go b/basculehttp/metricListener.go deleted file mode 100644 index 6c0db7a..0000000 --- a/basculehttp/metricListener.go +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "errors" - - "github.com/justinas/alice" - "github.com/prometheus/client_golang/prometheus" - "github.com/xmidt-org/bascule" - "go.uber.org/fx" -) - -const ( - defaultServer = "primary" -) - -var ( - ErrNilMeasures = errors.New("measures cannot be nil") -) - -// MetricListener keeps track of request authentication and authorization using -// metrics. A counter is updated on success and failure to reflect the outcome -// and reason for failure, if applicable. MetricListener implements the Listener -// and has an OnErrorResponse function in order for the metrics to be updated at -// the correct time. -type MetricListener struct { - server string - measures *AuthValidationMeasures -} - -// Option is how the MetricListener is configured. -type Option func(m *MetricListener) - -// MetricListenerOptionsIn is an uber fx wired struct that can be used to build -// a MetricListener. -type MetricListenerOptionsIn struct { - fx.In - Measures AuthValidationMeasures - Options []Option `group:"bascule_metric_listener_options"` -} - -// OnAuthenticated is called after a request passes through the constructor and -// enforcer successfully. It updates various metrics related to the accepted -// request. -func (m *MetricListener) OnAuthenticated(auth bascule.Authentication) { - outcome := AcceptedOutcome - // this is weird and we should take note of it. - if auth.Token == nil { - outcome = EmptyOutcome - } - m.measures.ValidationOutcome. - With(prometheus.Labels{ - ServerLabel: m.server, - OutcomeLabel: outcome, - }). - Add(1) -} - -// OnErrorResponse is called if the constructor or enforcer have a problem with -// authenticating/authorizing the request. The ErrorResponseReason is used as -// the outcome label value in a metric. -func (m *MetricListener) OnErrorResponse(e ErrorResponseReason, _ error) { - m.measures.ValidationOutcome. - With(prometheus.Labels{ServerLabel: m.server, OutcomeLabel: e.String()}). - Add(1) -} - -// WithServer provides the server label value to be used by all MetricListener -// metrics. -func WithServer(s string) Option { - return func(m *MetricListener) { - if s != "" { - m.server = s - } - } -} - -// NewMetricListener creates a new MetricListener that uses the measures -// provided and is configured with the given options. The measures cannot be -// nil. -func NewMetricListener(m *AuthValidationMeasures, options ...Option) (*MetricListener, error) { - if m == nil { - return nil, ErrNilMeasures - } - - listener := MetricListener{ - server: defaultServer, - measures: m, - } - - for _, o := range options { - o(&listener) - } - return &listener, nil -} - -// ProvideMetricListener provides the metric listener as well as the options -// needed for adding it into various middleware. -func ProvideMetricListener() fx.Option { - return fx.Provide( - fx.Annotated{ - Name: "bascule_metric_listener", - Target: func(in MetricListenerOptionsIn) (*MetricListener, error) { - return NewMetricListener(&in.Measures, in.Options...) - }, - }, - fx.Annotated{ - Name: "alice_listener", - Target: func(in MetricListenerIn) alice.Constructor { - return NewListenerDecorator(in.M) - }, - }, - fx.Annotated{ - Group: "bascule_constructor_options", - Target: func(in MetricListenerIn) COption { - return WithCErrorResponseFunc(in.M.OnErrorResponse) - }, - }, - fx.Annotated{ - Group: "bascule_enforcer_options", - Target: func(in MetricListenerIn) EOption { - return WithEErrorResponseFunc(in.M.OnErrorResponse) - }, - }, - ) -} diff --git a/basculehttp/metricListener_test.go b/basculehttp/metricListener_test.go deleted file mode 100644 index c8a625a..0000000 --- a/basculehttp/metricListener_test.go +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "errors" - "fmt" - "testing" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/touchstone/touchtest" -) - -const testServerName = "testserver" - -func TestNewMetricListener(t *testing.T) { - m := &AuthValidationMeasures{} - tests := []struct { - description string - measures *AuthValidationMeasures - options []Option - expectedMetricListener *MetricListener - expectedErr error - }{ - { - description: "Success", - measures: m, - options: []Option{ - WithServer(testServerName), - WithServer(""), - }, - expectedMetricListener: &MetricListener{ - server: testServerName, - measures: m, - }, - }, - { - description: "Success with defaults", - measures: m, - expectedMetricListener: &MetricListener{ - server: defaultServer, - measures: m, - }, - }, - { - description: "Nil measures error", - expectedErr: ErrNilMeasures, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - ml, err := NewMetricListener(tc.measures, tc.options...) - assert.Equal(tc.expectedMetricListener, ml) - if tc.expectedErr == nil { - assert.NoError(err) - return - } - assert.True(errors.Is(err, tc.expectedErr), - fmt.Errorf("error [%v] doesn't contain error [%v] in its err chain", - err, tc.expectedErr), - ) - }) - } -} - -func TestOnAuthenticated(t *testing.T) { - tests := []struct { - description string - token bascule.Token - expectedOutcome string - }{ - { - description: "Success", - token: bascule.NewToken("test", "princ", nil), - expectedOutcome: AcceptedOutcome, - }, - { - description: "Success with empty token", - expectedOutcome: EmptyOutcome, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - testAssert := touchtest.New(t) - - // set up metric stuff. - expectedRegistry := prometheus.NewPedanticRegistry() - expectedCounter := prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "testCounter", - Help: "testCounter", - }, - []string{ServerLabel, OutcomeLabel}, - ) - expectedRegistry.Register(expectedCounter) - expectedCounter.With(prometheus.Labels{ - ServerLabel: testServerName, - OutcomeLabel: tc.expectedOutcome, - }).Inc() - actualRegistry := prometheus.NewPedanticRegistry() - mockMeasures := AuthValidationMeasures{ - ValidationOutcome: prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "testCounter", - Help: "testCounter", - }, - []string{ServerLabel, OutcomeLabel}, - ), - } - actualRegistry.MustRegister(mockMeasures.ValidationOutcome) - - m := &MetricListener{ - server: testServerName, - measures: &mockMeasures, - } - m.OnAuthenticated(bascule.Authentication{Token: tc.token}) - testAssert.Expect(expectedRegistry) - assert.True(testAssert.GatherAndCompare(actualRegistry)) - }) - } -} -func TestOnErrorResponse(t *testing.T) { - tests := []struct { - description string - reason ErrorResponseReason - }{ - { - description: "Checks failed", - reason: ChecksFailed, - }, - { - description: "Get URL failed", - reason: GetURLFailed, - }, - { - description: "Key not supported", - reason: KeyNotSupported, - }, - { - description: "Negative unknown reason", - reason: -1, - }, - { - description: "Big number unknown reason", - reason: 10000000, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - testAssert := touchtest.New(t) - - // set up metric stuff. - expectedRegistry := prometheus.NewPedanticRegistry() - expectedCounter := prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "testCounter", - Help: "testCounter", - }, - []string{ServerLabel, OutcomeLabel}, - ) - expectedRegistry.Register(expectedCounter) - expectedCounter.With(prometheus.Labels{ - ServerLabel: testServerName, - OutcomeLabel: tc.reason.String(), - }).Inc() - actualRegistry := prometheus.NewPedanticRegistry() - mockMeasures := AuthValidationMeasures{ - ValidationOutcome: prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "testCounter", - Help: "testCounter", - }, - []string{ServerLabel, OutcomeLabel}, - ), - } - actualRegistry.MustRegister(mockMeasures.ValidationOutcome) - - m := &MetricListener{ - server: testServerName, - measures: &mockMeasures, - } - m.OnErrorResponse(tc.reason, errors.New("testing error")) - testAssert.Expect(expectedRegistry) - assert.True(testAssert.GatherAndCompare(actualRegistry)) - }) - } -} diff --git a/basculehttp/metrics.go b/basculehttp/metrics.go deleted file mode 100644 index 5bf7abc..0000000 --- a/basculehttp/metrics.go +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/xmidt-org/touchstone" - "go.uber.org/fx" -) - -// Names for our metrics -const ( - AuthValidationOutcome = "auth_validation" -) - -// labels -const ( - OutcomeLabel = "outcome" - ServerLabel = "server" -) - -// outcome values other than error response reasons -const ( - AcceptedOutcome = "accepted" - EmptyOutcome = "accepted_but_empty" -) - -// help messages -const ( - authValidationOutcomeHelpMsg = "Counter for success and failure reason results through bascule" -) - -// ProvideMetrics provides the metrics relevant to this package as uber/fx -// options. The provided metrics are prometheus vectors which gives access to -// more advanced operations such as CurryWith(labels). -func ProvideMetrics() fx.Option { - return fx.Options( - touchstone.CounterVec( - prometheus.CounterOpts{ - Name: AuthValidationOutcome, - Help: authValidationOutcomeHelpMsg, - ConstLabels: nil, - }, ServerLabel, OutcomeLabel), - ) -} - -// AuthValidationMeasures describes the defined metrics that will be used by clients -type AuthValidationMeasures struct { - fx.In - - ValidationOutcome *prometheus.CounterVec `name:"auth_validation"` -} diff --git a/basculehttp/middleware.go b/basculehttp/middleware.go new file mode 100644 index 0000000..de53faf --- /dev/null +++ b/basculehttp/middleware.go @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import ( + "context" + "net/http" + "strconv" + + "github.com/xmidt-org/bascule/v1" + "go.uber.org/multierr" +) + +// MiddlewareOption is a functional option for tailoring a Middleware. +type MiddlewareOption interface { + apply(*Middleware) error +} + +type middlewareOptionFunc func(*Middleware) error + +func (mof middlewareOptionFunc) apply(m *Middleware) error { + return mof(m) +} + +// WithAccessor configures a credentials Accessor for this Middleware. If not supplied +// or if the supplied Accessor is nil, DefaultAccessor() is used. +func WithAccessor(a Accessor) MiddlewareOption { + return middlewareOptionFunc(func(m *Middleware) error { + if a != nil { + m.accessor = a + } else { + m.accessor = DefaultAccessor() + } + + return nil + }) +} + +// WithCredentialsParser configures a credentials parser for this Middleware. If not supplied +// or if the supplied CredentialsParser is nil, DefaultCredentialsParser() is used. +func WithCredentialsParser(cp bascule.CredentialsParser) MiddlewareOption { + return middlewareOptionFunc(func(m *Middleware) error { + if cp != nil { + m.credentialsParser = cp + } else { + m.credentialsParser = DefaultCredentialsParser() + } + + return nil + }) +} + +// WithTokenParser registers a token parser for the given scheme. If the scheme has +// already been registered, the given parser will replace that registration. +// +// The parser cannot be nil. +func WithTokenParser(scheme bascule.Scheme, tp bascule.TokenParser) MiddlewareOption { + return middlewareOptionFunc(func(m *Middleware) error { + m.tokenParsers.Register(scheme, tp) + return nil + }) +} + +// WithAuthentication adds validators used for authentication to this Middleware. Each +// invocation of this option is cumulative. Authentication validators are run in the order +// supplied by this option. +func WithAuthentication(v ...bascule.Validator) MiddlewareOption { + return middlewareOptionFunc(func(m *Middleware) error { + m.authentication.Add(v...) + return nil + }) +} + +// WithChallenges adds WWW-Authenticate challenges to be used when a StatusUnauthorized is +// detected. Multiple invocations of this option are cumulative. Each challenge results +// in a separate WWW-Authenticate header, in the order specified by this option. +func WithChallenges(ch ...Challenge) MiddlewareOption { + return middlewareOptionFunc(func(m *Middleware) error { + m.challenges.Add(ch...) + return nil + }) +} + +// WithAuthorization adds authorizers to this Middleware. Each invocation of this option +// is cumulative. Authorizers are executed for each request in the order supplied +// via this option. +// +// A Middleware requires all its options to pass in order to allow access. Callers can +// use Authorizers.Any to create authorizers that require only (1) authorizer to pass. +// This is useful for use cases like admin access or alternate capabilities. +func WithAuthorization(a ...bascule.Authorizer[*http.Request]) MiddlewareOption { + return middlewareOptionFunc(func(m *Middleware) error { + m.authorization.Add(a...) + return nil + }) +} + +// WithErrorStatusCoder sets the strategy used to write errors to HTTP responses. If this +// option is omitted or if esc is nil, DefaultErrorStatusCoder is used. +func WithErrorStatusCoder(esc ErrorStatusCoder) MiddlewareOption { + return middlewareOptionFunc(func(m *Middleware) error { + if esc != nil { + m.errorStatusCoder = esc + } else { + m.errorStatusCoder = DefaultErrorStatusCoder + } + + return nil + }) +} + +// WithErrorMarshaler sets the strategy used to marshal errors to HTTP response bodies. If this +// option is omitted or if esc is nil, DefaultErrorMarshaler is used. +func WithErrorMarshaler(em ErrorMarshaler) MiddlewareOption { + return middlewareOptionFunc(func(m *Middleware) error { + if em != nil { + m.errorMarshaler = em + } else { + m.errorMarshaler = DefaultErrorMarshaler + } + + return nil + }) +} + +// Middleware is an immutable configuration that can decorate multiple handlers. +type Middleware struct { + accessor Accessor + credentialsParser bascule.CredentialsParser + tokenParsers bascule.TokenParsers + authentication bascule.Validators + authorization bascule.Authorizers[*http.Request] + challenges Challenges + + errorStatusCoder ErrorStatusCoder + errorMarshaler ErrorMarshaler +} + +// NewMiddleware creates an immutable Middleware instance from a supplied set of options. +// No options will result in a Middleware with default behavior. +func NewMiddleware(opts ...MiddlewareOption) (m *Middleware, err error) { + m = &Middleware{ + accessor: DefaultAccessor(), + credentialsParser: DefaultCredentialsParser(), + tokenParsers: DefaultTokenParsers(), + errorStatusCoder: DefaultErrorStatusCoder, + errorMarshaler: DefaultErrorMarshaler, + } + + for _, o := range opts { + err = multierr.Append(err, o.apply(m)) + } + + return +} + +// Then produces an http.Handler that uses this Middleware's workflow to protected +// a given handler. +func (m *Middleware) Then(protected http.Handler) http.Handler { + if protected == nil { + protected = http.DefaultServeMux + } + + return &frontDoor{ + middleware: m, + protected: protected, + } +} + +// ThenFunc is like Then, but protects a handler function. +func (m *Middleware) ThenFunc(protected http.HandlerFunc) http.Handler { + if protected == nil { + return m.Then(nil) + } + + return m.Then(protected) +} + +// writeError handles writing error information to an HTTP response. This will include any WWW-Authenticate +// challenges that are configured if a 401 status is detected. +// +// The defaultCode is used as the response status code if the given error does not supply a StatusCode method. +// +// If the error supports JSON or text marshaling, that is used for the response body. Otherwise, a text/plain +// response with the Error() method's text is used. +func (m *Middleware) writeError(response http.ResponseWriter, request *http.Request, defaultCode int, err error) { + statusCode := m.errorStatusCoder(request, defaultCode, err) + if statusCode == http.StatusUnauthorized { + m.challenges.WriteHeader(response.Header()) + } + + contentType, content, marshalErr := m.errorMarshaler(request, err) + + // TODO: what if marshalErr != nil ? + if marshalErr == nil { + response.Header().Set("Content-Type", contentType) + response.Header().Set("Content-Length", strconv.Itoa(len(content))) + response.WriteHeader(statusCode) + response.Write(content) // TODO: handle errors here somehow + } +} + +func (m *Middleware) getCredentialsAndToken(ctx context.Context, request *http.Request) (c bascule.Credentials, t bascule.Token, err error) { + var raw string + raw, err = m.accessor.GetCredentials(request) + if err == nil { + c, err = m.credentialsParser.Parse(raw) + } + + if err == nil { + t, err = m.tokenParsers.Parse(ctx, c) + } + + return +} + +func (m *Middleware) authenticate(ctx context.Context, token bascule.Token) error { + return m.authentication.Validate(ctx, token) +} + +func (m *Middleware) authorize(ctx context.Context, token bascule.Token, request *http.Request) error { + return m.authorization.Authorize(ctx, token, request) +} + +// frontDoor is the internal handler implementation that protects a handler +// using the bascule workflow. +type frontDoor struct { + middleware *Middleware + protected http.Handler +} + +// ServeHTTP implements the bascule workflow, using the configured middleware. +func (fd *frontDoor) ServeHTTP(response http.ResponseWriter, request *http.Request) { + ctx := request.Context() + creds, token, err := fd.middleware.getCredentialsAndToken(ctx, request) + if err != nil { + // by default, failing to parse a token is a malformed request + fd.middleware.writeError(response, request, http.StatusBadRequest, err) + return + } + + ctx = bascule.WithCredentials(ctx, creds) + err = fd.middleware.authenticate(ctx, token) + if err == nil { + // at this point in the workflow, the request has valid credentials. we use + // StatusForbidden as the default because any failure to authenticate isn't a + // case where the caller needs to supply credentials. Rather, the supplied + // credentials aren't adequate enough. + fd.middleware.writeError(response, request, http.StatusForbidden, err) + return + } + + ctx = bascule.WithToken(ctx, token) + err = fd.middleware.authorize(ctx, token, request) + if err == nil { + fd.middleware.writeError(response, request, http.StatusForbidden, err) + return + } + + fd.protected.ServeHTTP(response, request.WithContext(ctx)) +} diff --git a/basculehttp/mocks_test.go b/basculehttp/mocks_test.go deleted file mode 100644 index 9a04bb5..0000000 --- a/basculehttp/mocks_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "crypto" - - "github.com/stretchr/testify/mock" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/clortho" - - "context" - - "github.com/golang-jwt/jwt" -) - -// mockListener -type mockListener struct { - mock.Mock -} - -func (l *mockListener) OnAuthenticated(a bascule.Authentication) { - l.Called(a) -} - -// mock JWT parser -type mockParser struct { - mock.Mock -} - -// we want to test the parseFunc so it needs to be called here. -func (p *mockParser) ParseJWT(token string, claims jwt.Claims, parseFunc jwt.Keyfunc) (*jwt.Token, error) { - args := p.Called(token, claims, parseFunc) - t := args.Get(0).(*jwt.Token) - err := args.Error(1) - if err != nil { - return t, err - } - _, err = parseFunc(t) - return t, err -} - -// mockKey is a mock for key. -type mockKey struct { - mock.Mock - clortho.Thumbprinter -} - -func (key *mockKey) Public() crypto.PublicKey { - arguments := key.Called() - return arguments.Get(0) -} - -func (key *mockKey) KeyType() string { - arguments := key.Called() - return arguments.String(0) -} - -func (key *mockKey) KeyID() string { - arguments := key.Called() - return arguments.String(0) -} - -func (key *mockKey) KeyUsage() string { - arguments := key.Called() - return arguments.String(0) -} - -func (key *mockKey) Raw() interface{} { - arguments := key.Called() - return arguments.Get(0) -} - -// MockResolver is a stretchr mock for Resolver. It's exposed for other package tests. -type MockResolver struct { - mock.Mock -} - -func (resolver *MockResolver) Resolve(ctx context.Context, keyId string) (clortho.Key, error) { - arguments := resolver.Called(ctx, keyId) - if key, ok := arguments.Get(0).(clortho.Key); ok { - return key, arguments.Error(1) - } else { - return nil, arguments.Error(1) - } -} -func (resolver *MockResolver) AddListener(l clortho.ResolveListener) clortho.CancelListenerFunc { - arguments := resolver.Called(l) - return arguments.Get(0).(clortho.CancelListenerFunc) -} diff --git a/basculehttp/notfoundbehavior_string.go b/basculehttp/notfoundbehavior_string.go deleted file mode 100644 index a3bcd8b..0000000 --- a/basculehttp/notfoundbehavior_string.go +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by "stringer -type=NotFoundBehavior"; DO NOT EDIT. - -package basculehttp - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[Forbid-0] - _ = x[Allow-1] -} - -const _NotFoundBehavior_name = "ForbidAllow" - -var _NotFoundBehavior_index = [...]uint8{0, 6, 11} - -func (i NotFoundBehavior) String() string { - if i < 0 || i >= NotFoundBehavior(len(_NotFoundBehavior_index)-1) { - return "NotFoundBehavior(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _NotFoundBehavior_name[_NotFoundBehavior_index[i]:_NotFoundBehavior_index[i+1]] -} diff --git a/basculehttp/provide.go b/basculehttp/provide.go deleted file mode 100644 index 3e2be1a..0000000 --- a/basculehttp/provide.go +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/bascule/basculechecks" - "go.uber.org/fx" -) - -// BearerValidatorsIn is a struct used for uber fx wiring, providing an easy way -// to combine validators meant to be used on bearer tokens. -type BearerValidatorsIn struct { - fx.In - Vs []bascule.Validator `group:"bascule_bearer_validators"` - Capabilities bascule.Validator `name:"bascule_validator_capabilities" optional:"true"` -} - -// ProvideBasicAuth uses the key given to provide a constructor option to create -// basic tokens and an enforcer option to allow all basic tokens. For basic -// tokens, the token factory's validation checks are usually all that is needed. -func ProvideBasicAuth(key string) fx.Option { - return fx.Options( - ProvideBasicTokenFactory(key), - fx.Provide( - fx.Annotated{ - Group: "bascule_enforcer_options", - Target: func() EOption { - return WithRules(BasicAuthorization, basculechecks.AllowAll()) - }, - }, - ), - ) -} - -// ProvideBearerValidator builds some basic validators for bearer tokens and -// then bundles them and any other injected bearer validators to be used against -// bearer tokens. A enforcer option is provided to configure this in the -// enforcer. -func ProvideBearerValidator() fx.Option { - return fx.Provide( - fx.Annotated{ - Group: "bascule_bearer_validators", - Target: func() bascule.Validator { - return basculechecks.NonEmptyPrincipal() - }, - }, - fx.Annotated{ - Group: "bascule_bearer_validators", - Target: func() bascule.Validator { - return basculechecks.ValidType([]string{"jwt"}) - }, - }, - fx.Annotated{ - Group: "bascule_enforcer_options", - Target: func(in BearerValidatorsIn) EOption { - if len(in.Vs) == 0 { - return nil - } - // don't add any nil validators. - rules := []bascule.Validator{} - for _, v := range in.Vs { - if v != nil { - rules = append(rules, v) - } - } - if len(rules) == 0 { - return nil - } - if in.Capabilities != nil { - rules = append(rules, in.Capabilities) - } - return WithRules(BearerAuthorization, bascule.Validators(rules)) - }, - }, - ) -} diff --git a/basculehttp/provide_test.go b/basculehttp/provide_test.go deleted file mode 100644 index e7d3f1b..0000000 --- a/basculehttp/provide_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "testing" - - "github.com/justinas/alice" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/bascule/basculechecks" - "github.com/xmidt-org/clortho" - "github.com/xmidt-org/sallust" - "github.com/xmidt-org/touchstone" - "go.uber.org/fx" - "go.uber.org/fx/fxtest" - "go.uber.org/zap" -) - -func TestProvideBearerMiddleware(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - l, err := zap.NewDevelopment() - require.NoError(err) - - type In struct { - fx.In - AuthChain alice.Chain `name:"auth_chain"` - } - result := In{} - app := fxtest.New( - t, - - // supplying dependencies - fx.Supply(l), - touchstone.Provide(), - fx.Provide( - func() (c sallust.Config) { - return sallust.Config{} - }, - func() (c basculechecks.CapabilitiesMapConfig) { - return basculechecks.CapabilitiesMapConfig{ - Endpoints: map[string]string{ - ".*/a/.*": "whatsup", - ".*/b/.*": "nm", - }, - Default: "eh", - } - }, - fx.Annotated{ - Name: "default_key_id", - Target: func() string { - return "default" - }, - }, - fx.Annotated{ - Name: "key_resolver", - Target: func() clortho.Resolver { - r := new(MockResolver) - return r - }, - }, - fx.Annotated{ - Name: "parser", - Target: func() bascule.JWTParser { - p := new(mockParser) - return p - }, - }, - fx.Annotated{ - Name: "jwt_leeway", - Target: func() bascule.Leeway { - return bascule.Leeway{EXP: 5} - }, - }, - ), - - // the parts we care about - ProvideMetrics(), - ProvideBearerTokenFactory(false), - basculechecks.ProvideMetrics(), - basculechecks.ProvideCapabilitiesMapValidator(), - ProvideBearerValidator(), - ProvideServerChain(), - - fx.Invoke( - func(in In) { - result = in - }, - ), - ) - require.NoError(app.Err()) - app.RequireStart() - assert.NotNil(result.AuthChain) - app.RequireStop() -} - -func TestProvideOptionalMiddleware(t *testing.T) { - type In struct { - fx.In - AuthChain alice.Chain `name:"auth_chain"` - } - t.Run("no capability check or bearer token factory or basic auth", func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - l, err := zap.NewDevelopment() - require.NoError(err) - - result := In{} - app := fxtest.New( - t, - - // supplying dependencies - fx.Supply(l), - touchstone.Provide(), - fx.Provide( - func() (c sallust.Config) { - return sallust.Config{} - }, - fx.Annotated{ - Name: "encoded_basic_auths", - Target: func() EncodedBasicKeys { - return EncodedBasicKeys{Basic: []string{"dXNlcjpwYXNz", "dXNlcjpwYXNz", "dXNlcjpwYXNz"}} - }, - }, - func() (c basculechecks.CapabilitiesValidatorConfig) { - return basculechecks.CapabilitiesValidatorConfig{ - Type: "enforce", - EndpointBuckets: []string{"abc", "def", `\M`, "adbecf"}, - } - }, - ), - // the parts we care about - ProvideMetrics(), - ProvideBasicAuth(""), - ProvideBearerTokenFactory(true), - basculechecks.ProvideMetrics(), - basculechecks.ProvideRegexCapabilitiesValidator(), - ProvideBearerValidator(), - ProvideServerChain(), - - fx.Invoke( - func(in In) { - result = in - }, - ), - ) - require.NoError(app.Err()) - app.RequireStart() - assert.NotNil(result.AuthChain) - app.RequireStop() - }) -} diff --git a/basculehttp/token.go b/basculehttp/token.go new file mode 100644 index 0000000..df708d1 --- /dev/null +++ b/basculehttp/token.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package basculehttp + +import "github.com/xmidt-org/bascule/v1" + +// Token is bascule's default HTTP token. +type Token struct { + principal string +} + +func (t *Token) Principal() string { + return t.principal +} + +// DefaultTokenParsers returns the default suite of parsers supported by +// bascule. This method returns a distinct instance each time it is called, +// thus allowing calling code to tailor it independently of other calls. +func DefaultTokenParsers() bascule.TokenParsers { + return bascule.TokenParsers{ + BasicScheme: basicTokenParser{}, + } +} diff --git a/basculehttp/urlParsing.go b/basculehttp/urlParsing.go deleted file mode 100644 index ab767e4..0000000 --- a/basculehttp/urlParsing.go +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "errors" - "net/url" - "strings" - - "go.uber.org/fx" -) - -// ParseURL is a function that modifies the url given then returns it. -type ParseURL func(*url.URL) (*url.URL, error) - -// ParseURLIn is uber fx wiring allowing for ParseURL to be optional. -type ParseURLIn struct { - fx.In - P ParseURL `optional:"true"` -} - -// DefaultParseURLFunc does nothing. It returns the same url it received. -func DefaultParseURLFunc(u *url.URL) (*url.URL, error) { - return u, nil -} - -// CreateRemovePrefixURLFunc parses the URL by removing the prefix specified. -func CreateRemovePrefixURLFunc(prefix string, next ParseURL) ParseURL { - return func(u *url.URL) (*url.URL, error) { - escapedPath := u.EscapedPath() - if !strings.HasPrefix(escapedPath, prefix) { - return nil, errors.New("unexpected URL, did not start with expected prefix") - } - u.Path = escapedPath[len(prefix):] - u.RawPath = escapedPath[len(prefix):] - if next == nil { - return u, nil - } - return next(u) - } -} - -// ProvideParseURL creates the constructor option to include a ParseURL function -// if it is provided. -func ProvideParseURL() fx.Option { - return fx.Provide( - fx.Annotated{ - Group: "bascule_constructor_options", - Target: func(in ParseURLIn) COption { - return WithParseURLFunc(in.P) - }, - }, - ) -} diff --git a/basculejwt/doc.go b/basculejwt/doc.go new file mode 100644 index 0000000..c4f48d0 --- /dev/null +++ b/basculejwt/doc.go @@ -0,0 +1,5 @@ +/* +Package basculejwt provides JWT support for the bascule workflow. The canonical +parser is implemented in terms of https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwt#Parse. +*/ +package basculejwt diff --git a/basculejwt/token.go b/basculejwt/token.go new file mode 100644 index 0000000..a5294c8 --- /dev/null +++ b/basculejwt/token.go @@ -0,0 +1,83 @@ +package basculejwt + +import ( + "context" + "time" + + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/xmidt-org/bascule/v1" +) + +// Claims exposes standard JWT claims from a Token. +type Claims interface { + // Audience returns the aud field of the JWT. + Audience() []string + + // Expiration returns the exp field of the JWT. + Expiration() time.Time + + // IssuedAt returns the iat field of the JWT. + IssuedAt() time.Time + + // Issuer returns the iss field of the JWT. + Issuer() string + + // JwtID returns the jti field of the JWT. + JwtID() string + + // NotBefore returns the nbf field of the JWT. + NotBefore() time.Time + + // Subject returns the sub field of the JWT. For tokens that + // implement this interface, this method returns the same value + // as tne Principal method. + Subject() string +} + +// Token is the interface implemented by JWT-based tokens supplied by this package. +// User-defined claims can be accessed through the bascule.Attributes interface. +// +// Note that the Princpal method returns the same value as the Subject claim. +type Token interface { + bascule.Token + bascule.Attributes + Claims +} + +// token is the internal implementation of the JWT Token interface. It fronts +// a lestrrat-go Token. +type token struct { + jwt.Token +} + +func (t *token) Principal() string { + return t.Token.Subject() +} + +// tokenParser is the canonical parser for bascule that deals with JWTs. +type tokenParser struct { + options []jwt.ParseOption +} + +// NewTokenParser constructs a parser using the supplied set of parse options. +// The returned parser will produce tokens that implement the Token interface +// in this package. +func NewTokenParser(options ...jwt.ParseOption) (bascule.TokenParser, error) { + return &tokenParser{ + options: append( + make([]jwt.ParseOption, 0, len(options)), + options..., + ), + }, nil +} + +func (tp *tokenParser) Parse(_ context.Context, c bascule.Credentials) (bascule.Token, error) { + jwtToken, err := jwt.ParseString(c.Value, tp.options...) + if err != nil { + return nil, err + } + + return &token{ + Token: jwtToken, + }, nil +} diff --git a/context.go b/context.go index 7e528d2..3207fd9 100644 --- a/context.go +++ b/context.go @@ -5,37 +5,59 @@ package bascule import ( "context" - "net/url" ) -// Authorization represents the authorization mechanism performed on the token, -// e.g. "Basic", "Bearer", etc for HTTP security environments. -type Authorization string +// Contexter is anything that logically holds a context. For example, *http.Request +// implements this interface. +type Contexter interface { + Context() context.Context +} + +type credentialsContextKey struct{} + +// GetCredentials examines the context and returns the credentials used to +// build the Token. If no credentials are in the context, this function +// returns false. +func GetCredentials(ctx context.Context) (c Credentials, found bool) { + c, found = ctx.Value(credentialsContextKey{}).(Credentials) + return +} -// Authentication represents the output of a security pipeline. -type Authentication struct { - Authorization Authorization - Token Token - Request Request +// GetCredentialsFrom uses the context held by src to obtain credentials. +// As with GetCredentials, if no credentials are found this function returns false. +func GetCredentialsFrom(src Contexter) (Credentials, bool) { + return GetCredentials(src.Context()) } -// Request holds request information that may be useful for validating the -// token. -type Request struct { - URL *url.URL - Method string +// WithCredentials constructs a new context with the supplied credentials. +func WithCredentials(ctx context.Context, c Credentials) context.Context { + return context.WithValue( + ctx, + credentialsContextKey{}, + c, + ) } -type authenticationKey struct{} +type tokenContextKey struct{} + +// GetToken retrieves a Token from a context. If not token is in the context, +// this function returns false. +func GetToken(ctx context.Context) (t Token, found bool) { + t, found = ctx.Value(tokenContextKey{}).(Token) + return +} -// WithAuthentication adds the auth given to the context given, provided a way -// for other users of the context to get the authentication. -func WithAuthentication(ctx context.Context, auth Authentication) context.Context { - return context.WithValue(ctx, authenticationKey{}, auth) +// GetTokenFrom uses the context held by src to obtain a Token. As with GetToken, +// if no token is found this function returns false. +func GetTokenFrom(src Contexter) (Token, bool) { + return GetToken(src.Context()) } -// FromContext gets the Authentication from the context provided. -func FromContext(ctx context.Context) (Authentication, bool) { - auth, ok := ctx.Value(authenticationKey{}).(Authentication) - return auth, ok +// WithToken constructs a new context with the supplied token. +func WithToken(ctx context.Context, t Token) context.Context { + return context.WithValue( + ctx, + tokenContextKey{}, + t, + ) } diff --git a/context_test.go b/context_test.go index 42db346..fa0d79a 100644 --- a/context_test.go +++ b/context_test.go @@ -5,32 +5,183 @@ package bascule import ( "context" - "net/url" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) +type ContextTestSuite struct { + TestSuite +} + +func (suite *ContextTestSuite) testGetCredentialsSuccess() { + ctx := context.WithValue( + context.Background(), + credentialsContextKey{}, + suite.testCredentials(), + ) + + creds, ok := GetCredentials(ctx) + suite.Require().True(ok) + suite.Equal( + suite.testCredentials(), + creds, + ) +} + +func (suite *ContextTestSuite) testGetCredentialsMissing() { + creds, ok := GetCredentials(context.Background()) + suite.Equal(Credentials{}, creds) + suite.False(ok) +} + +func (suite *ContextTestSuite) testGetCredentialsWrongType() { + ctx := context.WithValue(context.Background(), credentialsContextKey{}, 123) + creds, ok := GetCredentials(ctx) + suite.Equal(Credentials{}, creds) + suite.False(ok) +} + +func (suite *ContextTestSuite) TestGetCredentials() { + suite.Run("Success", suite.testGetCredentialsSuccess) + suite.Run("Missing", suite.testGetCredentialsMissing) + suite.Run("WrongType", suite.testGetCredentialsWrongType) +} + +func (suite *ContextTestSuite) testGetCredentialsFromSuccess() { + c := suite.contexter( + context.WithValue( + context.Background(), + credentialsContextKey{}, + suite.testCredentials(), + ), + ) + + creds, ok := GetCredentialsFrom(c) + suite.Require().True(ok) + suite.Equal( + suite.testCredentials(), + creds, + ) +} + +func (suite *ContextTestSuite) testGetCredentialsFromMissing() { + creds, ok := GetCredentialsFrom( + suite.contexter(context.Background()), + ) + + suite.Equal(Credentials{}, creds) + suite.False(ok) +} + +func (suite *ContextTestSuite) testGetCredentialsFromWrongType() { + c := suite.contexter( + context.WithValue(context.Background(), credentialsContextKey{}, 123), + ) + + creds, ok := GetCredentialsFrom(c) + suite.Equal(Credentials{}, creds) + suite.False(ok) +} + +func (suite *ContextTestSuite) TestGetCredentialsFrom() { + suite.Run("Success", suite.testGetCredentialsFromSuccess) + suite.Run("Missing", suite.testGetCredentialsFromMissing) + suite.Run("WrongType", suite.testGetCredentialsFromWrongType) +} + +func (suite *ContextTestSuite) TestWithCredentials() { + ctx := WithCredentials(context.Background(), suite.testCredentials()) + + creds, ok := ctx.Value(credentialsContextKey{}).(Credentials) + suite.Require().True(ok) + suite.Equal(suite.testCredentials(), creds) +} + +func (suite *ContextTestSuite) testGetTokenSuccess() { + ctx := context.WithValue( + context.Background(), + tokenContextKey{}, + suite.testToken(), + ) + + token, ok := GetToken(ctx) + suite.Require().True(ok) + suite.Equal( + suite.testToken(), + token, + ) +} + +func (suite *ContextTestSuite) testGetTokenMissing() { + token, ok := GetToken(context.Background()) + suite.Nil(token) + suite.False(ok) +} + +func (suite *ContextTestSuite) testGetTokenWrongType() { + ctx := context.WithValue(context.Background(), tokenContextKey{}, 123) + token, ok := GetToken(ctx) + suite.Nil(token) + suite.False(ok) +} + +func (suite *ContextTestSuite) TestGetToken() { + suite.Run("Success", suite.testGetTokenSuccess) + suite.Run("Missing", suite.testGetTokenMissing) + suite.Run("WrongType", suite.testGetTokenWrongType) +} + +func (suite *ContextTestSuite) testGetTokenFromSuccess() { + c := suite.contexter( + context.WithValue( + context.Background(), + tokenContextKey{}, + suite.testToken(), + ), + ) + + token, ok := GetTokenFrom(c) + suite.Require().True(ok) + suite.Equal( + suite.testToken(), + token, + ) +} + +func (suite *ContextTestSuite) testGetTokenFromMissing() { + token, ok := GetTokenFrom( + suite.contexter(context.Background()), + ) + + suite.Nil(token) + suite.False(ok) +} + +func (suite *ContextTestSuite) testGetTokenFromWrongType() { + c := suite.contexter( + context.WithValue(context.Background(), tokenContextKey{}, 123), + ) + + token, ok := GetTokenFrom(c) + suite.Nil(token) + suite.False(ok) +} + +func (suite *ContextTestSuite) TestGetTokenFrom() { + suite.Run("Success", suite.testGetTokenFromSuccess) + suite.Run("Missing", suite.testGetTokenFromMissing) + suite.Run("WrongType", suite.testGetTokenFromWrongType) +} + +func (suite *ContextTestSuite) TestWithToken() { + ctx := WithToken(context.Background(), suite.testToken()) + + token, ok := ctx.Value(tokenContextKey{}).(Token) + suite.Require().True(ok) + suite.Equal(suite.testToken(), token) +} + func TestContext(t *testing.T) { - assert := assert.New(t) - u, err := url.ParseRequestURI("/a/b/c") - assert.NoError(err) - expectedAuth := Authentication{ - Authorization: "authorization string", - Token: simpleToken{ - tokenType: "test", - principal: "test principal", - attributes: NewAttributes(map[string]interface{}{"testkey": "testval", "attr": 5}), - }, - Request: Request{ - URL: u, - Method: "GET", - }, - } - ctx := context.Background() - newCtx := WithAuthentication(ctx, expectedAuth) - assert.NotNil(newCtx) - auth, ok := FromContext(newCtx) - assert.True(ok) - assert.Equal(expectedAuth, auth) + suite.Run(t, new(ContextTestSuite)) } diff --git a/v2/credentials.go b/credentials.go similarity index 65% rename from v2/credentials.go rename to credentials.go index 6d5f6aa..2ac2cce 100644 --- a/v2/credentials.go +++ b/credentials.go @@ -5,30 +5,6 @@ package bascule import "strings" -// MissingCredentialsError indicates that credentials could not be found. -// Typically, this error will be returned by code that extracts credentials -// from some other source, e.g. an HTTP request. -type MissingCredentialsError struct { - // Cause represents the lower-level error that occurred, if any. - Cause error - - // Reason contains any additional information about the missing credentials. - Reason string -} - -func (err *MissingCredentialsError) Unwrap() error { return err.Cause } - -func (err *MissingCredentialsError) Error() string { - var o strings.Builder - o.WriteString("Missing credentials") - if len(err.Reason) > 0 { - o.WriteString(": ") - o.WriteString(err.Reason) - } - - return o.String() -} - // InvalidCredentialsError is returned typically by CredentialsParser.Parse // to indicate that a raw, serialized credentials were badly formatted. type InvalidCredentialsError struct { @@ -43,9 +19,30 @@ func (err *InvalidCredentialsError) Unwrap() error { return err.Cause } func (err *InvalidCredentialsError) Error() string { var o strings.Builder - o.WriteString(`Invalid credentials: "`) + o.WriteString(`Invalid credentials "`) o.WriteString(err.Raw) o.WriteString(`"`) + + if err.Cause != nil { + o.WriteString(": ") + o.WriteString(err.Cause.Error()) + } + + return o.String() +} + +// UnsupportedSchemeError indicates that a credential scheme was not +// supported via the particular way bascule was configured. +type UnsupportedSchemeError struct { + // Scheme is the authorization scheme that wasn't supported. + Scheme Scheme +} + +func (err *UnsupportedSchemeError) Error() string { + var o strings.Builder + o.WriteString(`Unsupported credential scheme: "`) + o.WriteString(string(err.Scheme)) + o.WriteString(`"`) return o.String() } @@ -68,3 +65,10 @@ type CredentialsParser interface { // returns the Credentials object. Parse(raw string) (Credentials, error) } + +// CredentialsParserFunc is a function type that implements CredentialsParser. +type CredentialsParserFunc func(string) (Credentials, error) + +func (cpf CredentialsParserFunc) Parse(raw string) (Credentials, error) { + return cpf(raw) +} diff --git a/credentials_test.go b/credentials_test.go new file mode 100644 index 0000000..1afe63b --- /dev/null +++ b/credentials_test.go @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/suite" +) + +type CredentialsTestSuite struct { + suite.Suite +} + +func (suite *CredentialsTestSuite) TestInvalidCredentialsError() { + suite.Run("WithCause", func() { + cause := errors.New("cause") + err := InvalidCredentialsError{ + Cause: cause, + Raw: "raw", + } + + suite.Same(err.Unwrap(), cause) + suite.Contains(err.Error(), "cause") + suite.Contains(err.Error(), "raw") + }) + + suite.Run("NoCause", func() { + err := InvalidCredentialsError{ + Raw: "raw", + } + + suite.Nil(err.Unwrap()) + suite.Contains(err.Error(), "raw") + }) +} + +func (suite *CredentialsTestSuite) TestUnsupportedSchemeError() { + err := UnsupportedSchemeError{ + Scheme: Scheme("scheme"), + } + + suite.Contains(err.Error(), "scheme") +} + +func (suite *CredentialsTestSuite) TestCredentialsParserFunc() { + const expectedRaw = "expected raw credentials" + expectedErr := errors.New("expected error") + var c CredentialsParser = CredentialsParserFunc(func(raw string) (Credentials, error) { + suite.Equal(expectedRaw, raw) + return Credentials{ + Scheme: Scheme("test"), + Value: "value", + }, expectedErr + }) + + creds, err := c.Parse(expectedRaw) + suite.Equal( + Credentials{ + Scheme: Scheme("test"), + Value: "value", + }, + creds, + ) + + suite.Same(expectedErr, err) +} + +func TestCredentials(t *testing.T) { + suite.Run(t, new(CredentialsTestSuite)) +} diff --git a/doc.go b/doc.go index a97311a..e9deaca 100644 --- a/doc.go +++ b/doc.go @@ -4,5 +4,4 @@ /* Package bascule provides a configurable way to validate an auth token. */ - package bascule diff --git a/error.go b/error.go deleted file mode 100644 index 63526d5..0000000 --- a/error.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -import ( - "strings" -) - -// Error is an optional interface to be implemented by security related errors -type Error interface { - Cause() error - Reason() string -} - -// MultiError is an interface that provides a list of errors. -type MultiError interface { - Errors() []error -} - -// Errors is a Multierror that also acts as an error, so that a log-friendly -// string can be returned but each error in the list can also be accessed. -type Errors []error - -// Error concatenates the list of error strings to provide a single string -// that can be used to represent the errors that occurred. -func (e Errors) Error() string { - var output strings.Builder - output.Write([]byte("multiple errors: [")) - for i, msg := range e { - if i > 0 { - output.WriteRune(',') - output.WriteRune(' ') - } - - output.WriteString(msg.Error()) - } - output.WriteRune(']') - - return output.String() -} - -// Errors returns the list of errors. -func (e Errors) Errors() []error { - return e -} diff --git a/error_test.go b/error_test.go deleted file mode 100644 index 6e43939..0000000 --- a/error_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestErrors(t *testing.T) { - assert := assert.New(t) - errors := []error{errors.New("error1"), errors.New("testing error list"), errors.New("test"), Errors([]error{errors.New("inner list test"), errors.New("")})} - errorsString := "multiple errors: [error1, testing error list, test, multiple errors: [inner list test, ]]" - assert.Equal(errorsString, Errors(errors).Error()) - assert.Equal(errors, Errors(errors).Errors()) -} diff --git a/examples/acquirer/acquirer.go b/examples/acquirer/acquirer.go deleted file mode 100644 index 78aba1e..0000000 --- a/examples/acquirer/acquirer.go +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "encoding/base64" - "fmt" - "io" - "net/http" - "os" - "time" - - "github.com/xmidt-org/bascule/acquire" -) - -func main() { - // set up acquirer and add the auth to the request - acquirer, err := acquire.NewFixedAuthAcquirer("Basic " + base64.StdEncoding.EncodeToString([]byte("testuser:testpass"))) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to create basic auth plain text acquirer: %v\n", err.Error()) - os.Exit(1) - } - - request, err := http.NewRequest(http.MethodGet, "http://localhost:6000/test", nil) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to create request: %v\n", err.Error()) - os.Exit(1) - } - if err = acquire.AddAuth(request, acquirer); err != nil { - fmt.Fprintf(os.Stderr, "failed to add auth: %v\n", err.Error()) - os.Exit(1) - } - - httpclient := &http.Client{ - Timeout: 5 * time.Second, - } - resp, err := httpclient.Do(request) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to send request: %v\n", err.Error()) - os.Exit(1) - } - defer resp.Body.Close() - - if resp.Body != nil { - respBody, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to read body: %v\n", err.Error()) - os.Exit(1) - } - // output the body if it's good - fmt.Fprintf(os.Stdout, "Body: \n%s\n", respBody) - } - // output the code - fmt.Fprintf(os.Stdout, "Status code received: %v\n", resp.StatusCode) - os.Exit(0) -} diff --git a/examples/acquirer/go.mod b/examples/acquirer/go.mod deleted file mode 100644 index 3b036b6..0000000 --- a/examples/acquirer/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/xmidt-org/bascule/examples/acquirer - -go 1.12 - -require github.com/xmidt-org/bascule v0.3.1 // indirect diff --git a/examples/acquirer/go.sum b/examples/acquirer/go.sum deleted file mode 100644 index 3eef790..0000000 --- a/examples/acquirer/go.sum +++ /dev/null @@ -1,148 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA= -github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/goph/emperror v0.17.1/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jtacoma/uritemplates v1.0.0/go.mod h1:IhIICdE9OcvgUnGwTtJxgBQ+VrTrti5PcbLVSJianO8= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da/go.mod h1:oLH0CmIaxCGXD67VKGR5AacGXZSMznlmeqM8RzPrcY8= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -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.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xmidt-org/bascule v0.3.1 h1:I6qUJE6RHsfGEnwHQdbF47nFBcqOUfNqzhDTbuGj42o= -github.com/xmidt-org/bascule v0.3.1/go.mod h1:fx2JeJAwEoepVcE3Fgd6JQSBvUAccyuFDzMmct1eWYw= -github.com/xmidt-org/webpa-common v1.1.0/go.mod h1:oCpKzOC+9h2vYHVzAU/06tDTQuBN4RZz+rhgIXptpOI= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/examples/basculehttp/basculehttp.go b/examples/basculehttp/basculehttp.go deleted file mode 100644 index bf872bf..0000000 --- a/examples/basculehttp/basculehttp.go +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "net/http" - - "github.com/gorilla/mux" - "github.com/justinas/alice" - "github.com/xmidt-org/bascule" - "github.com/xmidt-org/bascule/basculehttp" - "github.com/xmidt-org/sallust" - "github.com/xmidt-org/webpa-common/logging" - "go.uber.org/zap" -) - -func SetLogger(logger *zap.Logger) func(delegate http.Handler) http.Handler { - return func(delegate http.Handler) http.Handler { - return http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - ctx := r.WithContext(logging.WithLogger(r.Context(), - logger.With(zap.Any("requestHeaders", r.Header), zap.String("requestURL", r.URL.EscapedPath()), zap.String("method", r.Method)))) - delegate.ServeHTTP(w, ctx) - }) - } -} - -// currently only sets up basic auth -func authChain(logger *zap.Logger) alice.Chain { - basicAllowed := map[string]string{ - "testuser": "testpass", - "pls": "letmein", - } - options := []basculehttp.COption{ - basculehttp.WithCLogger(sallust.Get), - basculehttp.WithTokenFactory("Basic", basculehttp.BasicTokenFactory(basicAllowed)), - } - - authConstructor := basculehttp.NewConstructor(options...) - - basicRules := bascule.Validators{ - bascule.CreateNonEmptyPrincipalCheck(), - bascule.CreateNonEmptyTypeCheck(), - bascule.CreateValidTypeCheck([]string{"basic"}), - } - - authEnforcer := basculehttp.NewEnforcer( - basculehttp.WithELogger(sallust.Get), - basculehttp.WithRules("Basic", basicRules), - ) - - return alice.New(SetLogger(logger), authConstructor, authEnforcer) -} - -func simpleResponse(writer http.ResponseWriter, request *http.Request) { - writer.Write([]byte("good auth!")) - writer.WriteHeader(200) - return -} - -func main() { - router := mux.NewRouter() - authFuncs := authChain(sallust.Default()) - router.Handle("/test", authFuncs.ThenFunc(simpleResponse)) - http.ListenAndServe(":6000", router) -} diff --git a/examples/basculehttp/go.mod b/examples/basculehttp/go.mod deleted file mode 100644 index 2fbf4b2..0000000 --- a/examples/basculehttp/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module github.com/xmidt-org/bascule/examples/basculehttp - -go 1.12 - -require ( - github.com/go-kit/kit v0.9.0 // indirect - github.com/xmidt-org/bascule v0.3.1 // indirect - github.com/xmidt-org/webpa-common v1.3.0 // indirect -) diff --git a/examples/basculehttp/go.sum b/examples/basculehttp/go.sum deleted file mode 100644 index a6b432e..0000000 --- a/examples/basculehttp/go.sum +++ /dev/null @@ -1,154 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA= -github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/goph/emperror v0.17.1/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jtacoma/uritemplates v1.0.0/go.mod h1:IhIICdE9OcvgUnGwTtJxgBQ+VrTrti5PcbLVSJianO8= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da h1:5y58+OCjoHCYB8182mpf/dEsq0vwTKPOo4zGfH0xW9A= -github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da/go.mod h1:oLH0CmIaxCGXD67VKGR5AacGXZSMznlmeqM8RzPrcY8= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -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.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xmidt-org/bascule v0.3.1 h1:I6qUJE6RHsfGEnwHQdbF47nFBcqOUfNqzhDTbuGj42o= -github.com/xmidt-org/bascule v0.3.1/go.mod h1:fx2JeJAwEoepVcE3Fgd6JQSBvUAccyuFDzMmct1eWYw= -github.com/xmidt-org/webpa-common v1.1.0/go.mod h1:oCpKzOC+9h2vYHVzAU/06tDTQuBN4RZz+rhgIXptpOI= -github.com/xmidt-org/webpa-common v1.3.0 h1:AembEPW16kQpUWqZ8Kn1uxjdWDuCzccqFGNWoPExK14= -github.com/xmidt-org/webpa-common v1.3.0/go.mod h1:oCpKzOC+9h2vYHVzAU/06tDTQuBN4RZz+rhgIXptpOI= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/go.mod b/go.mod index 6f5c3da..2c92e41 100644 --- a/go.mod +++ b/go.mod @@ -1,79 +1,28 @@ -module github.com/xmidt-org/bascule +module github.com/xmidt-org/bascule/v1 go 1.21 require ( - github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2 - github.com/go-kit/kit v0.13.0 - github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/justinas/alice v1.2.0 - github.com/prometheus/client_golang v1.18.0 - github.com/spf13/cast v1.6.0 - github.com/stretchr/testify v1.9.0 - github.com/xmidt-org/candlelight v0.0.21 - github.com/xmidt-org/clortho v0.0.4 - github.com/xmidt-org/sallust v0.2.2 - github.com/xmidt-org/touchstone v0.1.5 - github.com/xmidt-org/webpa-common/v2 v2.3.2 - go.uber.org/fx v1.20.1 + github.com/lestrrat-go/jwx/v2 v2.0.20 + github.com/stretchr/testify v1.8.4 go.uber.org/multierr v1.11.0 - go.uber.org/zap v1.27.0 ) require ( - github.com/VividCortex/gohistogram v1.0.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/uuid v1.5.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c // indirect - github.com/jtacoma/uritemplates v1.0.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/jwx/v2 v2.0.19 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.46.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/segmentio/asm v1.2.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - github.com/xmidt-org/chronon v0.1.1 // indirect - github.com/xmidt-org/wrp-go/v3 v3.2.3 // indirect - go.opentelemetry.io/otel v1.21.0 // indirect - go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 // indirect - go.opentelemetry.io/otel/exporters/zipkin v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/sdk v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect - go.opentelemetry.io/proto/otlp v1.0.0 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/dig v1.17.1 // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect - google.golang.org/grpc v1.60.1 // indirect - google.golang.org/protobuf v1.32.0 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/sys v0.17.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4db1867..57bafd8 100644 --- a/go.sum +++ b/go.sum @@ -1,57 +1,16 @@ -github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2 h1:koK7z0nSsRiRiBWwa+E714Puh+DO+ZRdIyAXiXzL+lg= -github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA= -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/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= -github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= -github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jtacoma/uritemplates v1.0.0 h1:xwx5sBF7pPAb0Uj8lDC1Q/aBPpOFyQza7OC705ZlLCo= -github.com/jtacoma/uritemplates v1.0.0/go.mod h1:IhIICdE9OcvgUnGwTtJxgBQ+VrTrti5PcbLVSJianO8= -github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= -github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= @@ -62,114 +21,33 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= -github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= +github.com/lestrrat-go/jwx/v2 v2.0.20 h1:sAgXuWS/t8ykxS9Bi2Qtn5Qhpakw1wrcjxChudjolCc= +github.com/lestrrat-go/jwx/v2 v2.0.20/go.mod h1:UlCSmKqw+agm5BsOBfEAbTvKsEApaGNqHAEUTv5PJC4= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= -github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/xmidt-org/candlelight v0.0.21 h1:sNvF7a5hcyIkFWMh1kPGhEOQWNZtVZuLGaG1BMwMZvk= -github.com/xmidt-org/candlelight v0.0.21/go.mod h1:VepVufM9StCXHBSzumURivv2dKyc32TpKFJvxnKGsLU= -github.com/xmidt-org/chronon v0.1.1 h1:SzOYkT/nmps3jH4sWu6A52ToKvv5Bu0Gb45/ec5Ty9U= -github.com/xmidt-org/chronon v0.1.1/go.mod h1:8VF1skJAouQihpKfXE8oZZkbQpV1TSR/7QltNxq8T4k= -github.com/xmidt-org/clortho v0.0.4 h1:4G1uKle0rfm+LwQ3EV7W+jn7kgWnozAoK9DAbz0/etw= -github.com/xmidt-org/clortho v0.0.4/go.mod h1:1YypMcDmHVrSqSzpMp4fvwloSKc5PQnHmoaPcKWchHk= -github.com/xmidt-org/sallust v0.2.2 h1:MrINLEr7cMj6ENx/O76fvpfd5LNGYnk7OipZAGXPYA0= -github.com/xmidt-org/sallust v0.2.2/go.mod h1:ytBoypcPw10OmjM6b92Jx3eoqWX4J5zVXOQozGwz4qs= -github.com/xmidt-org/touchstone v0.1.5 h1:Afm3P0EzCOWD1ITyVLsEDPVQkSE0t2ZhHyV+kOkNZS8= -github.com/xmidt-org/touchstone v0.1.5/go.mod h1:Dz0fA1eWjm/2WrsdEeaQZMevkmfdYTsAbQfLaTrB8Eo= -github.com/xmidt-org/webpa-common/v2 v2.3.2 h1:66RUmlkltI3iet55WLsSVUW6D5Z7+JpxWyrn84ScQc4= -github.com/xmidt-org/webpa-common/v2 v2.3.2/go.mod h1:WnMf2dLIZOQ5Gvje9Ges/ovHl2pqERFpfP+ST49v6bw= -github.com/xmidt-org/wrp-go/v3 v3.2.3 h1:Qmcsnz8QQZ2ZYVLvhyuTH11ixySes1Lm30XNdGDTpn8= -github.com/xmidt-org/wrp-go/v3 v3.2.3/go.mod h1:ZXUMN7ZTOv/F+9/WypWexNxIt33WVLQvb2hidwIHvqw= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= -go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E= -go.opentelemetry.io/otel/exporters/zipkin v1.21.0 h1:D+Gv6lSfrFBWmQYyxKjDd0Zuld9SRXpIrEsKZvE4DO4= -go.opentelemetry.io/otel/exporters/zipkin v1.21.0/go.mod h1:83oMKR6DzmHisFOW3I+yIMGZUTjxiWaiBI8M8+TU5zE= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= -go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= -go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs= -google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= -google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo= -google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= -gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/jws.go b/jws.go deleted file mode 100644 index 51a9811..0000000 --- a/jws.go +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -import ( - "encoding/json" - "errors" - - "github.com/golang-jwt/jwt" -) - -// JWTParser parses raw Tokens into JWT objects -type JWTParser interface { - ParseJWT(string, jwt.Claims, jwt.Keyfunc) (*jwt.Token, error) -} - -type defaultJWTParser struct{} - -func (parser defaultJWTParser) ParseJWT(token string, claims jwt.Claims, parseFunc jwt.Keyfunc) (*jwt.Token, error) { - if jwtToken, err := jwt.ParseWithClaims(token, claims, parseFunc); err == nil { - return jwtToken, nil - } else { - return nil, err - } -} - -// DefaultJWTParser is the parser implementation that simply delegates to -// the jwt library's jws.ParseJWT function. -var DefaultJWTParser JWTParser = defaultJWTParser{} - -type ClaimsWithLeeway struct { - jwt.MapClaims - Leeway Leeway -} - -// Leeway is the amount of buffer to include with the time, to allow for clock -// skew. -type Leeway struct { - EXP int64 `json:"expLeeway" yaml:"expLeeway"` - NBF int64 `json:"nbfLeeway" yaml:"nbfLeeway"` - IAT int64 `json:"iatLeeway" yaml:"iatLeeway"` -} - -// Valid implements the jwt.Claims interface, ensuring that the token claism -// are valid. This implementation checks the time based claims: exp, iat, nbf. -func (c *ClaimsWithLeeway) Valid() error { - vErr := new(jwt.ValidationError) - now := jwt.TimeFunc().Unix() - - if !c.VerifyExpiresAt(now+c.Leeway.EXP, false) { - vErr.Inner = errors.New("Token is expired") - vErr.Errors |= jwt.ValidationErrorExpired - } - - if !c.VerifyIssuedAt(now-c.Leeway.IAT, false) { - vErr.Inner = errors.New("Token used before issued") - vErr.Errors |= jwt.ValidationErrorIssuedAt - } - - if !c.VerifyNotBefore(now-c.Leeway.NBF, false) { - vErr.Inner = errors.New("Token is not valid yet") - vErr.Errors |= jwt.ValidationErrorNotValidYet - } - - if vErr.Errors == 0 { - return nil - } - - return vErr -} - -func (c *ClaimsWithLeeway) UnmarshalJSON(data []byte) error { - c.MapClaims = make(jwt.MapClaims) // just to be sure it's clean before each unmarshal - return json.Unmarshal(data, &c.MapClaims) -} - -// GetMap returns a map of string to interfaces of the values in the ClaimsWithLeeway -func (c *ClaimsWithLeeway) GetMap() (map[string]interface{}, error) { - return c.MapClaims, nil -} diff --git a/jws_test.go b/jws_test.go deleted file mode 100644 index 4cf6070..0000000 --- a/jws_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -import ( - "testing" - - "github.com/golang-jwt/jwt" - "github.com/stretchr/testify/assert" -) - -func TestValid(t *testing.T) { - assert := assert.New(t) - claims := ClaimsWithLeeway{ - MapClaims: make(jwt.MapClaims), - Leeway: Leeway{ - EXP: 5, - NBF: 5, - IAT: 5, - }, - } - err := claims.Valid() - assert.NoError(err) -} diff --git a/testSuite_test.go b/testSuite_test.go new file mode 100644 index 0000000..554ef02 --- /dev/null +++ b/testSuite_test.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package bascule + +import ( + "context" + "net/http" + + "github.com/stretchr/testify/suite" +) + +const ( + testScheme Scheme = "Test" +) + +type testToken struct { + principal string +} + +func (tt *testToken) Principal() string { + return tt.principal +} + +// TestSuite holds generally useful functionality for testing bascule. +type TestSuite struct { + suite.Suite +} + +func (suite *TestSuite) testContext() context.Context { + return context.WithValue( + context.Background(), + struct{}{}, + "test value", + ) +} + +func (suite *TestSuite) testCredentials() Credentials { + return Credentials{ + Scheme: testScheme, + Value: "test", + } +} + +func (suite *TestSuite) testToken() Token { + return &testToken{ + principal: "test", + } +} + +func (suite *TestSuite) contexter(ctx context.Context) Contexter { + return new(http.Request).WithContext(ctx) +} diff --git a/token.go b/token.go index 1c4129a..9a2c2c6 100644 --- a/token.go +++ b/token.go @@ -1,53 +1,60 @@ // SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC // SPDX-License-Identifier: Apache-2.0 -// package bascule provides a token interface and basic implementation, which -// can be validated and added and taken from a context. Some basic checks -// which can be used to validate are also provided. package bascule -// Token is the behavior supplied by all secure tokens -type Token interface { - // Type is the custom token type assigned by plugin code - Type() string +import ( + "context" +) - // Principal is the security principal, e.g. the user name or client id +// Token is a runtime representation of credentials. This interface will be further +// customized by infrastructure. +type Token interface { + // Principal is the security subject of this token, e.g. the user name or other + // user identifier. Principal() string - - // Attributes are an arbitrary set of name/value pairs associated with the token. - // Typically, these will be filled with information supplied by the user, e.g. the claims of a JWT. - Attributes() Attributes } -// Attributes is the interface that wraps methods which dictate how to interact -// with a token's attributes. Getter functions return a boolean as second element -// which indicates that a value of the requested type exists at the given key path. -type Attributes interface { - Get(key string) (interface{}, bool) +// TokenParser produces tokens from credentials. +type TokenParser interface { + // Parse turns a Credentials into a token. This method may validate parts + // of the credential's value, but should not perform any authentication itself. + // + // Some token parsers may interact with external systems, such as databases. The supplied + // context should be passed to any calls that might need to honor cancelation semantics. + Parse(context.Context, Credentials) (Token, error) } -// simpleToken is a very basic token type that can serve as the Token for many types of secure pipelines -type simpleToken struct { - tokenType string - principal string - attributes Attributes -} +// TokenParserFunc is a closure type that implements TokenParser. +type TokenParserFunc func(context.Context, Credentials) (Token, error) -func (st simpleToken) Type() string { - return st.tokenType +func (tpf TokenParserFunc) Parse(ctx context.Context, c Credentials) (Token, error) { + return tpf(ctx, c) } -func (st simpleToken) Principal() string { - return st.principal -} +// TokenParsers is a registry of parsers based on credential schemes. +// The zero value of this type is valid and ready to use. +type TokenParsers map[Scheme]TokenParser + +// Register adds or replaces the parser associated with the given scheme. +func (tp *TokenParsers) Register(scheme Scheme, p TokenParser) { + if *tp == nil { + *tp = make(TokenParsers) + } -func (st simpleToken) Attributes() Attributes { - return st.attributes + (*tp)[scheme] = p } -// NewToken creates a Token from basic information. Many secure pipelines can use the returned value as -// their token. Specialized pipelines can create additional interfaces and augment the returned Token -// as desired. Alternatively, some pipelines can simply create their own Tokens out of whole cloth. -func NewToken(tokenType, principal string, attributes Attributes) Token { - return simpleToken{tokenType, principal, attributes} +// Parse chooses a TokenParser based on the Scheme and invokes that +// parser. If the credential scheme is unsupported, an error is returned. +func (tp TokenParsers) Parse(ctx context.Context, c Credentials) (t Token, err error) { + if p, ok := tp[c.Scheme]; ok { + t, err = p.Parse(ctx, c) + } else { + err = &UnsupportedSchemeError{ + Scheme: c.Scheme, + } + } + + return } diff --git a/token_test.go b/token_test.go index f14232f..94b916f 100644 --- a/token_test.go +++ b/token_test.go @@ -4,19 +4,81 @@ package bascule import ( + "context" + "errors" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) -var attrs = NewAttributes(map[string]interface{}{"testkey": "testval", "attr": 5}) +type TokenParsersSuite struct { + TestSuite +} + +func (suite *TokenParsersSuite) assertUnsupportedScheme(scheme Scheme, err error) { + var use *UnsupportedSchemeError + if suite.ErrorAs(err, &use) { + suite.Equal(scheme, use.Scheme) + } +} + +func (suite *TokenParsersSuite) testParseEmpty() { + var tp TokenParsers + + // legal, but will always fail + token, err := tp.Parse(context.Background(), suite.testCredentials()) + suite.Nil(token) + suite.assertUnsupportedScheme(testScheme, err) +} + +func (suite *TokenParsersSuite) testParseUnsupported() { + var tp TokenParsers + tp.Register( + Scheme("Supported"), + TokenParserFunc( + func(context.Context, Credentials) (Token, error) { + suite.Fail("TokenParser should not have been called") + return nil, nil + }, + ), + ) + + token, err := tp.Parse(context.Background(), suite.testCredentials()) + suite.Nil(token) + suite.assertUnsupportedScheme(testScheme, err) +} + +func (suite *TokenParsersSuite) testParseSupported() { + var ( + expectedErr = errors.New("expected Parse error") + + testCtx = suite.testContext() + testCredentials = suite.testCredentials() + ) + + var tp TokenParsers + tp.Register( + testCredentials.Scheme, + TokenParserFunc( + func(ctx context.Context, c Credentials) (Token, error) { + suite.Equal(testCtx, ctx) + suite.Equal(testCredentials, c) + return suite.testToken(), expectedErr + }, + ), + ) + + token, err := tp.Parse(testCtx, testCredentials) + suite.Equal(suite.testToken(), token) + suite.Same(expectedErr, err) +} + +func (suite *TokenParsersSuite) TestParse() { + suite.Run("Empty", suite.testParseEmpty) + suite.Run("Unsupported", suite.testParseUnsupported) + suite.Run("Supported", suite.testParseSupported) +} -func TestToken(t *testing.T) { - assert := assert.New(t) - tokenType := "test type" - principal := "test principal" - token := NewToken(tokenType, principal, attrs) - assert.Equal(tokenType, token.Type()) - assert.Equal(principal, token.Principal()) - assert.Equal(attrs, token.Attributes()) +func TestTokenParsers(t *testing.T) { + suite.Run(t, new(TokenParsersSuite)) } diff --git a/v2/authenticator.go b/v2/authenticator.go deleted file mode 100644 index e7b5a2c..0000000 --- a/v2/authenticator.go +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -import "go.uber.org/multierr" - -// Authenticator provides a strategy for verifying that a Token -// is valid beyond just simple parsing. For example, an Authenticator may -// verify certain roles or capabilities. -type Authenticator interface { - // Authenticate verifies the given Token. - Authenticate(Token) error -} - -// Authenticators is an aggregate Authenticator. -type Authenticators []Authenticator - -// Authenticate applies each contained Authenticator in order. All Authenticators -// are executed. The returned error, if not nil, will be an aggregate of all errors -// that occurred. -func (as Authenticators) Authenticate(t Token) (err error) { - for _, auth := range as { - err = multierr.Append(err, auth.Authenticate(t)) - } - - return -} diff --git a/v2/authorizer.go b/v2/authorizer.go deleted file mode 100644 index f342e15..0000000 --- a/v2/authorizer.go +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -// Authorizer is a strategy for verifying that a given Token has -// access to resources. -type Authorizer interface { - // Authorize verifies that the given Token can access a resource. - Authorize(resource any, t Token) error -} diff --git a/v2/basculehttp/accessor.go b/v2/basculehttp/accessor.go deleted file mode 100644 index 0a18264..0000000 --- a/v2/basculehttp/accessor.go +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "net/http" - "strings" - - "github.com/xmidt-org/bascule/v2" -) - -const DefaultAuthorizationHeader = "Authorization" - -// Accessor is the strategy for extracting the raw, serialized credentials -// from an HTTP request. -type Accessor interface { - // GetCredentials obtains the raw, serialized credentials from the request. - GetCredentials(*http.Request) (string, error) -} - -var defaultAccessor Accessor = AuthorizationAccessor{} - -func DefaultAccessor() Accessor { return defaultAccessor } - -// AuthorizationAccessor is an Accessor that pulls the serialized credentials -// from an HTTP header of the format defined by https://www.rfc-editor.org/rfc/rfc7235#section-4.2. -// Only the single header is considered. -type AuthorizationAccessor struct { - // Header is the name of the Authorization header. If unset, then - // DefaultAuthorizationHeader is used. - Header string -} - -func (aa AuthorizationAccessor) header() string { - if len(aa.Header) == 0 { - return DefaultAuthorizationHeader - } - - return aa.Header -} - -func (aa AuthorizationAccessor) GetCredentials(r *http.Request) (serialized string, err error) { - header := aa.header() - serialized = r.Header.Get(header) - - if len(serialized) == 0 { - var reason strings.Builder - reason.WriteString("missing header ") - reason.WriteString(header) - err = &bascule.MissingCredentialsError{ - Reason: reason.String(), - } - } - - return -} diff --git a/v2/basculehttp/basicToken.go b/v2/basculehttp/basicToken.go deleted file mode 100644 index c34c231..0000000 --- a/v2/basculehttp/basicToken.go +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "encoding/base64" - "strings" - - "github.com/xmidt-org/bascule/v2" -) - -type basicToken struct { - credentials bascule.Credentials - userName string -} - -func (bt *basicToken) Credentials() bascule.Credentials { return bt.credentials } - -func (bt *basicToken) Principal() string { return bt.userName } - -type basicTokenParser struct{} - -func (basicTokenParser) Parse(c bascule.Credentials) (t bascule.Token, err error) { - var decoded []byte - decoded, err = base64.StdEncoding.DecodeString(c.Value) - if err == nil { - if userName, _, ok := strings.Cut(string(decoded), ":"); ok { - t = &basicToken{ - credentials: c, - userName: userName, - } - } else { - err = &bascule.InvalidCredentialsError{ - Raw: c.Value, - } - } - } - - return -} diff --git a/v2/basculehttp/frontDoor.go b/v2/basculehttp/frontDoor.go deleted file mode 100644 index f803895..0000000 --- a/v2/basculehttp/frontDoor.go +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "errors" - "net/http" - - "github.com/xmidt-org/bascule/v2" - "go.uber.org/multierr" -) - -type FrontDoorOption interface { - apply(*frontDoor) error -} - -type frontDoorOptionFunc func(*frontDoor) error - -func (fdof frontDoorOptionFunc) apply(fd *frontDoor) error { return fdof(fd) } - -// WithAccessor associates a strategy for extracting the raw, serialized token -// from a request. If this option is not supplied, DefaultAccessor() is used. -func WithAccessor(a Accessor) FrontDoorOption { - return frontDoorOptionFunc(func(fd *frontDoor) error { - fd.accessor = a - return nil - }) -} - -// WithTokenFactory associates the given token factory with a front door. -func WithTokenFactory(tf bascule.TokenFactory) FrontDoorOption { - return frontDoorOptionFunc(func(fd *frontDoor) error { - fd.tokenFactory = tf - return nil - }) -} - -// WithChallenges describes challenges to be issued when no credentials -// are supplied. If no challenges are associated with a FrontDoor, then -// http.StatusForbidden is returned whenever credentials are not found in -// the request. Otherwise, http.StatusUnauthorized is returned along -// with a WWW-Authenticate header for each challenge. -func WithChallenges(c ...Challenge) FrontDoorOption { - return frontDoorOptionFunc(func(fd *frontDoor) error { - fd.challenges = append(fd.challenges, c...) - return nil - }) -} - -// FrontDoor is a server middleware that handles the full authentication workflow. -// Authorization is handled separately. -type FrontDoor interface { - Then(next http.Handler) http.Handler -} - -// NewFrontDoor constructs a FrontDoor middleware using the supplied options. -func NewFrontDoor(opts ...FrontDoorOption) (FrontDoor, error) { - fd := &frontDoor{ - accessor: DefaultAccessor(), - } - - var err error - for _, o := range opts { - err = multierr.Append(err, o.apply(fd)) - } - - if err != nil { - return nil, err - } - - return fd, nil -} - -type frontDoor struct { - challenges Challenges - forbidden func(http.ResponseWriter, *http.Request, error) // TODO - - accessor Accessor - tokenFactory bascule.TokenFactory -} - -func (fd *frontDoor) handleMissingCredentials(response http.ResponseWriter, err *bascule.MissingCredentialsError) { - var statusCode = http.StatusForbidden - if fd.challenges.WriteHeader(response.Header()) > 0 { - statusCode = http.StatusUnauthorized - } - - response.WriteHeader(statusCode) -} - -func (fd *frontDoor) handleInvalidCredentials(response http.ResponseWriter, err *bascule.InvalidCredentialsError) { - response.Header().Set("Content-Type", "text/plain") - response.WriteHeader(http.StatusBadRequest) - response.Write([]byte(err.Error())) -} - -func (fd *frontDoor) handleError(response http.ResponseWriter, request *http.Request, err error) { - { - var missing *bascule.MissingCredentialsError - if errors.As(err, &missing) { - fd.handleMissingCredentials(response, missing) - return - } - } - - { - var invalid *bascule.InvalidCredentialsError - if errors.As(err, &invalid) { - fd.handleInvalidCredentials(response, invalid) - return - } - } -} - -func (fd *frontDoor) Then(next http.Handler) http.Handler { - accessor := fd.accessor - if accessor == nil { - accessor = DefaultAccessor() - } - - return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - var token bascule.Token - raw, err := accessor.GetCredentials(request) - if err == nil { - token, err = fd.tokenFactory.NewToken(raw) - } - - if err != nil { - fd.handleError(response, request, err) - return - } - - request = request.WithContext( - bascule.WithToken(request.Context(), token), - ) - - next.ServeHTTP(response, request) - }) -} diff --git a/v2/basculehttp/jwtToken.go b/v2/basculehttp/jwtToken.go deleted file mode 100644 index 958c37a..0000000 --- a/v2/basculehttp/jwtToken.go +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/xmidt-org/bascule/v2" -) - -type jwtToken struct { - credentials bascule.Credentials - token jwt.Token -} - -func (jt *jwtToken) Credentials() bascule.Credentials { return jt.credentials } - -func (jt *jwtToken) Principal() string { return jt.token.Subject() } - -type jwtTokenParser struct { - options []jwt.ParseOption -} - -func (jtp jwtTokenParser) Parse(c bascule.Credentials) (t bascule.Token, err error) { - var token jwt.Token - token, err = jwt.Parse([]byte(c.Value), jtp.options...) - if err == nil { - t = &jwtToken{ - token: token, - } - } - - return -} - -func NewJwtTokenParser(opts ...jwt.ParseOption) (bascule.TokenParser, error) { - return &jwtTokenParser{ - options: append([]jwt.ParseOption{}, opts...), - }, nil -} diff --git a/v2/basculehttp/tokenFactory.go b/v2/basculehttp/tokenFactory.go deleted file mode 100644 index 5a1798d..0000000 --- a/v2/basculehttp/tokenFactory.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package basculehttp - -import ( - "strings" - - "github.com/xmidt-org/bascule/v2" -) - -type defaultCredentialsParser struct{} - -func (defaultCredentialsParser) Parse(serialized string) (c bascule.Credentials, err error) { - parts := strings.Split(serialized, " ") - if len(parts) != 2 { - err = &bascule.InvalidCredentialsError{ - Raw: serialized, - } - } else { - c.Scheme = bascule.Scheme(parts[0]) - c.Value = parts[1] - } - - return -} - -// NewTokenFactory builds a bascule.TokenFactory with useful defaults for an -// HTTP environment. -// -// A default CredentialParser and TokenParser schemes are prepended to the supplied -// option. This function will not return an error if those options are omitted. -// Any options supplied explicitly to this function can override those defaults. -func NewTokenFactory(opts ...bascule.TokenFactoryOption) (bascule.TokenFactory, error) { - opts = append( - // prepend defaults, allowing subsequent options to override - []bascule.TokenFactoryOption{ - bascule.WithCredentialsParser(defaultCredentialsParser{}), - bascule.WithTokenParser(BasicScheme, basicTokenParser{}), - // TODO: add Bearer - }, - opts..., - ) - - return bascule.NewTokenFactory(opts...) -} diff --git a/v2/capabilities.go b/v2/capabilities.go deleted file mode 100644 index 48bcc44..0000000 --- a/v2/capabilities.go +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -// GetCapabilities returns the set of security capabilities associated -// with the given Token. -// -// If the given Token has a Capabilities method that returns a []string, -// that method is used to determine the capabilities. Otherwise, this -// function returns an empty slice. -func GetCapabilities(t Token) (caps []string) { - type capabilities interface { - Capabilities() []string - } - - if c, ok := t.(capabilities); ok { - caps = c.Capabilities() - } - - return -} diff --git a/v2/context.go b/v2/context.go deleted file mode 100644 index c8ea4d6..0000000 --- a/v2/context.go +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -import "context" - -type tokenContextKey struct{} - -func GetToken[T Token](ctx context.Context, t *T) (found bool) { - *t, found = ctx.Value(tokenContextKey{}).(T) - return -} - -func WithToken[T Token](ctx context.Context, t T) context.Context { - return context.WithValue( - ctx, - tokenContextKey{}, - t, - ) -} diff --git a/v2/go.mod b/v2/go.mod deleted file mode 100644 index 9acb3ef..0000000 --- a/v2/go.mod +++ /dev/null @@ -1,23 +0,0 @@ -module github.com/xmidt-org/bascule/v2 - -go 1.21 - -require ( - github.com/lestrrat-go/jwx/v2 v2.0.19 - go.uber.org/multierr v1.11.0 -) - -require ( - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/lestrrat-go/blackmagic v1.0.2 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.4 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/segmentio/asm v1.2.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/sys v0.15.0 // indirect -) diff --git a/v2/go.sum b/v2/go.sum deleted file mode 100644 index e77632e..0000000 --- a/v2/go.sum +++ /dev/null @@ -1,39 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= -github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= -github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= -github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/token.go b/v2/token.go deleted file mode 100644 index d267071..0000000 --- a/v2/token.go +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -import ( - "errors" -) - -// Token is a runtime representation of credentials. This interface will be further -// customized by infrastructure. -type Token interface { - // Credentials returns the raw, unparsed information used to produce this Token. - Credentials() Credentials - - // Principal is the security subject of this token, e.g. the user name or other - // user identifier. - Principal() string -} - -// TokenParser produces tokens from credentials. -type TokenParser interface { - // Parse turns a Credentials into a Token. This method may validate parts - // of the credential's value, but should not perform any authentication itself. - Parse(Credentials) (Token, error) -} - -// TokenParsers is a registry of parsers based on credential schemes. -// The zero value of this type is valid and ready to use. -type TokenParsers map[Scheme]TokenParser - -// Register adds or replaces the parser associated with the given scheme. -func (tp *TokenParsers) Register(scheme Scheme, p TokenParser) { - if *tp == nil { - *tp = make(TokenParsers) - } - - (*tp)[scheme] = p -} - -// Parse chooses a TokenParser based on the Scheme and invokes that -// parser. If the credential scheme is unsupported, an error is returned. -func (tp TokenParsers) Parse(c Credentials) (Token, error) { - p, ok := tp[c.Scheme] - if !ok { - return nil, errors.New("TODO: unsupported credential scheme error") - } - - return p.Parse(c) -} diff --git a/v2/tokenFactory.go b/v2/tokenFactory.go deleted file mode 100644 index d72dcc3..0000000 --- a/v2/tokenFactory.go +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package bascule - -import ( - "errors" - - "go.uber.org/multierr" -) - -// TokenFactoryOption is a configurable option for building a TokenFactory. -type TokenFactoryOption interface { - apply(*tokenFactory) error -} - -type tokenFactoryOptionFunc func(*tokenFactory) error - -func (f tokenFactoryOptionFunc) apply(tf *tokenFactory) error { return f(tf) } - -// WithCredentialsParser establishes the strategy for parsing credentials for -// the TokenFactory being built. This option is required. -func WithCredentialsParser(cp CredentialsParser) TokenFactoryOption { - return tokenFactoryOptionFunc(func(tf *tokenFactory) error { - tf.credentialsParser = cp - return nil - }) -} - -// WithTokenParser registers a credential scheme with the TokenFactory. -// This option must be used at least once. -func WithTokenParser(scheme Scheme, tp TokenParser) TokenFactoryOption { - return tokenFactoryOptionFunc(func(tf *tokenFactory) error { - tf.tokenParsers.Register(scheme, tp) - return nil - }) -} - -// WithAuthenticators adds Authenticator rules to be used by the TokenFactory. -// Authenticator rules are optional. If omitted, then the TokenFactory will -// not perform authentication. -func WithAuthenticators(auth ...Authenticator) TokenFactoryOption { - return tokenFactoryOptionFunc(func(tf *tokenFactory) error { - tf.authenticators = append(tf.authenticators, auth...) - return nil - }) -} - -// TokenFactory brings together the entire authentication workflow. For typical -// code that uses bascule, this is the primary interface for obtaining Tokens. -type TokenFactory interface { - // NewToken accepts a raw, serialized set of credentials and turns it - // into a Token. This method executes the workflow of: - // - // - parsing the serialized credentials into a Credentials - // - parsing the Credentials into a Token - // - executing any Authenticator rules against the Token - NewToken(serialized string) (Token, error) -} - -// NewTokenFactory creates a TokenFactory using the supplied option. -// -// A CredentialParser and at least one (1) TokenParser is required. If -// either are not supplied, this function returns an error. -func NewTokenFactory(opts ...TokenFactoryOption) (TokenFactory, error) { - tf := &tokenFactory{} - - var err error - for _, o := range opts { - err = multierr.Append(err, o.apply(tf)) - } - - if tf.credentialsParser == nil { - err = multierr.Append(err, errors.New("A CredentialsParser is required")) - } - - if len(tf.tokenParsers) == 0 { - err = multierr.Append(err, errors.New("At least one (1) TokenParser is required")) - } - - return tf, err -} - -// tokenFactory is the internal implementation of TokenFactory. -type tokenFactory struct { - credentialsParser CredentialsParser - tokenParsers TokenParsers - authenticators Authenticators -} - -func (tf *tokenFactory) NewToken(serialized string) (t Token, err error) { - var c Credentials - c, err = tf.credentialsParser.Parse(serialized) - if err == nil { - t, err = tf.tokenParsers.Parse(c) - } - - if err == nil { - err = tf.authenticators.Authenticate(t) - } - - return -} diff --git a/validator.go b/validator.go index 98886e2..487f16b 100644 --- a/validator.go +++ b/validator.go @@ -1,46 +1,53 @@ // SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC // SPDX-License-Identifier: Apache-2.0 -// Validator provides tools for validating authorization tokens. Validation is -// done through running the rules provided. If a token is considered not valid, -// the validator will return an error. - package bascule -import "context" +import ( + "context" +) -// Validator is the rule type that determines if a Token is valid. Each rule should do exactly -// (1) thing, and then be composed by application-layer code. Validators are invoked for both -// authentication and authorization. We may need to have different rule types for those two things, -// but for now this works. +// Validator represents a general strategy for validating tokens. Token validation +// typically happens during authentication, but it can also happen during parsing +// if a caller uses NewValidatingTokenParser. type Validator interface { - Check(context.Context, Token) error + // Validate validates a token. If this validator needs to interact + // with external systems, the supplied context can be passed to honor + // cancelation semantics. + // + // This method may be passed a token that it doesn't support, e.g. a Basic + // validator can be passed a JWT token. In that case, this method should + // simply return nil. + Validate(context.Context, Token) error } -// ValidatorFunc is the Check function that a Validator has. +// ValidatorFunc is a closure type that implements Validator. type ValidatorFunc func(context.Context, Token) error -// Check runs the validatorFunc, making a ValidatorFunc also a Validator. -func (vf ValidatorFunc) Check(ctx context.Context, t Token) error { - return vf(ctx, t) +func (vf ValidatorFunc) Validate(ctx context.Context, token Token) error { + return vf(ctx, token) } -// Validators are a list of objects that implement the Validator interface. +// Validators is an aggregate Validator. type Validators []Validator -// Check runs through the list of validator Checks and adds any errors returned -// to the list of errors, which is an Errors type. -func (v Validators) Check(ctx context.Context, t Token) error { - // we want *all* rules to run, so we get a complete picture of the failure - var all Errors - for _, r := range v { - if err := r.Check(ctx, t); err != nil { - all = append(all, err) - } +// Add appends validators to this aggregate Validators. +func (vs *Validators) Add(v ...Validator) { + if *vs == nil { + *vs = make(Validators, 0, len(v)) } - if len(all) > 0 { - return all + *vs = append(*vs, v...) +} + +// Validate applies each validator in sequence. Execution stops at the first validator +// that returns an error, and that error is returned. If all validators return nil, +// this method returns nil, indicating the Token is valid. +func (vs Validators) Validate(ctx context.Context, token Token) error { + for _, v := range vs { + if err := v.Validate(ctx, token); err != nil { + return err + } } return nil diff --git a/validator_test.go b/validator_test.go index a493f52..4ea2601 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC +// SPDX-FileCopyrightText: 2020 Comcast Cable Communications Management, LLC // SPDX-License-Identifier: Apache-2.0 package bascule @@ -8,26 +8,77 @@ import ( "errors" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) +type ValidatorsTestSuite struct { + TestSuite +} + +func (suite *ValidatorsTestSuite) TestValidate() { + validateErr := errors.New("expected Validate error") + + testCases := []struct { + name string + results []error + expectedErr error + }{ + { + name: "EmptyValidators", + results: nil, + }, + { + name: "OneSuccess", + results: []error{nil}, + }, + { + name: "OneFailure", + results: []error{validateErr}, + expectedErr: validateErr, + }, + { + name: "FirstFailure", + results: []error{validateErr, errors.New("should not be called")}, + expectedErr: validateErr, + }, + { + name: "MiddleFailure", + results: []error{nil, validateErr, errors.New("should not be called")}, + expectedErr: validateErr, + }, + { + name: "AllSuccess", + results: []error{nil, nil, nil}, + }, + } + + for _, testCase := range testCases { + suite.Run(testCase.name, func() { + var ( + testCtx = suite.testContext() + testToken = suite.testToken() + vs Validators + ) + + for _, err := range testCase.results { + err := err + vs.Add( + ValidatorFunc(func(ctx context.Context, token Token) error { + suite.Same(testCtx, ctx) + suite.Same(testToken, token) + return err + }), + ) + } + + suite.Equal( + testCase.expectedErr, + vs.Validate(testCtx, testToken), + ) + }) + } +} + func TestValidators(t *testing.T) { - emptyAttributes := NewAttributes(map[string]interface{}{}) - testErr := errors.New("test err") - var ( - failFunc ValidatorFunc = func(_ context.Context, _ Token) error { - return testErr - } - successFunc ValidatorFunc = func(_ context.Context, _ Token) error { - return nil - } - ) - assert := assert.New(t) - validatorF := Validators([]Validator{successFunc, failFunc}) - validatorS := Validators([]Validator{successFunc, successFunc, successFunc}) - err := validatorS.Check(context.Background(), NewToken("type", "principal", emptyAttributes)) - assert.NoError(err) - errs := validatorF.Check(context.Background(), NewToken("", "", emptyAttributes)) - assert.NotNil(errs) - assert.True(errors.As(errs, &Errors{})) + suite.Run(t, new(ValidatorsTestSuite)) }