Skip to content

Commit

Permalink
Merge pull request #283 from xmidt-org/feature/authorization-tests
Browse files Browse the repository at this point in the history
Feature/authorization tests
  • Loading branch information
johnabass authored Aug 19, 2024
2 parents 5927844 + 16a52f3 commit 5379021
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 60 deletions.
14 changes: 8 additions & 6 deletions basculehttp/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ const (
var (
// ErrInvalidAuthorization indicates an authorization header value did not
// correspond to the standard.
ErrInvalidAuthorization = errors.New("invalidation authorization")

// ErrMissingAuthorization indicates that no authorization header was
// present in the source HTTP request.
ErrMissingAuthorization = errors.New("missing authorization")
ErrInvalidAuthorization = errors.New("invalid authorization")
)

// ParseAuthorization parses an authorization value typically passed in
Expand All @@ -48,6 +44,7 @@ func ParseAuthorization(raw string) (s Scheme, v string, err error) {
return
}

// AuthorizationParserOption is a configurable option for an AuthorizationParser.
type AuthorizationParserOption interface {
apply(*AuthorizationParser) error
}
Expand Down Expand Up @@ -83,6 +80,9 @@ func WithBasic() AuthorizationParserOption {
}

// AuthorizationParsers is a bascule.TokenParser that handles the Authorization header.
//
// By default, this parser will use the standard Authorization header, which can be
// changed via with WithAuthorizationHeader option.
type AuthorizationParser struct {
header string
parsers map[Scheme]bascule.TokenParser[string]
Expand Down Expand Up @@ -111,6 +111,8 @@ func NewAuthorizationParser(opts ...AuthorizationParserOption) (*AuthorizationPa
// Parse extracts the appropriate header, Authorization by default, and parses the
// scheme and value. Schemes are case-insensitive, e.g. BASIC and Basic are the same scheme.
//
// If no authorization header is found in the request, this method returns ErrMissingCredentials.
//
// If a token parser is registered for the given scheme, that token parser is invoked.
// Otherwise, UnsupportedSchemeError is returned, indicating the scheme in question.
func (ap *AuthorizationParser) Parse(ctx context.Context, source *http.Request) (bascule.Token, error) {
Expand All @@ -121,7 +123,7 @@ func (ap *AuthorizationParser) Parse(ctx context.Context, source *http.Request)

scheme, value, err := ParseAuthorization(authValue)
if err != nil {
return nil, err
return nil, bascule.ErrInvalidCredentials
}

p, registered := ap.parsers[scheme.lower()]
Expand Down
124 changes: 124 additions & 0 deletions basculehttp/authorization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehttp

import (
"context"
"errors"
"testing"

"github.com/stretchr/testify/suite"
"github.com/xmidt-org/bascule/v1"
)

// withAuthorizationParserOptionErr is an option that returns an error.
// These tests need this since no options currently return errors
// for an AuthorizationParser.
func withAuthorizationParserOptionErr(err error) AuthorizationParserOption {
return authorizationParserOptionFunc(func(*AuthorizationParser) error {
return err
})
}

type AuthorizationTestSuite struct {
TestSuite
}

// newAuthorizationParser produces a parser from a set of options that must assert as valid.
func (suite *AuthorizationTestSuite) newAuthorizationParser(opts ...AuthorizationParserOption) *AuthorizationParser {
ap, err := NewAuthorizationParser(opts...)
suite.Require().NoError(err)
suite.Require().NotNil(ap)
return ap
}

func (suite *AuthorizationTestSuite) TestBasicAuthSuccess() {
suite.Run("DefaultHeader", func() {
var (
ap = suite.newAuthorizationParser(
WithBasic(),
)

request = suite.newBasicAuthRequest()
)

token, err := ap.Parse(context.Background(), request)
suite.NoError(err)
suite.assertBasicToken(token)
})

suite.Run("Custom", func() {
var (
ap = suite.newAuthorizationParser(
WithAuthorizationHeader("Auth-Custom"),
WithScheme(Scheme("Custom"), BasicTokenParser{}),
)

request = suite.newRequest()
)

request.Header.Set("Auth-Custom", "Custom "+suite.basicAuth())
token, err := ap.Parse(context.Background(), request)
suite.NoError(err)
suite.assertBasicToken(token)
})
}

func (suite *AuthorizationTestSuite) TestMissingCredentials() {
var (
ap = suite.newAuthorizationParser(
WithBasic(),
)

request = suite.newRequest()
)

token, err := ap.Parse(context.Background(), request)
suite.ErrorIs(err, bascule.ErrMissingCredentials)
suite.Nil(token)
}

func (suite *AuthorizationTestSuite) TestInvalidCredentials() {
var (
ap = suite.newAuthorizationParser(
WithBasic(),
)

request = suite.newRequest()
)

request.Header.Set(DefaultAuthorizationHeader, "\t")
token, err := ap.Parse(context.Background(), request)
suite.ErrorIs(err, bascule.ErrInvalidCredentials)
suite.Nil(token)
}

func (suite *AuthorizationTestSuite) TestUnsupportedScheme() {
var (
ap = suite.newAuthorizationParser(
WithBasic(),
)

request = suite.newRequest()
)

request.Header.Set(DefaultAuthorizationHeader, "Unsupported xyz")
token, err := ap.Parse(context.Background(), request)
suite.Nil(token)

var use *UnsupportedSchemeError
suite.Require().ErrorAs(err, &use)
suite.Equal(Scheme("Unsupported"), use.Scheme)
}

func (suite *AuthorizationTestSuite) TestOptionError() {
expectedErr := errors.New("expected")
ap, err := NewAuthorizationParser(withAuthorizationParserOptionErr(expectedErr))
suite.ErrorIs(err, expectedErr)
suite.Nil(ap)
}

func TestAuthorization(t *testing.T) {
suite.Run(t, new(AuthorizationTestSuite))
}
11 changes: 11 additions & 0 deletions basculehttp/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package basculehttp

import (
"bytes"
"context"
"encoding/base64"
"strings"
Expand Down Expand Up @@ -62,3 +63,13 @@ func (BasicTokenParser) Parse(_ context.Context, value string) (bascule.Token, e

return nil, bascule.ErrInvalidCredentials
}

// BasicAuth produces the basic authorization string described by RFC 2617.
func BasicAuth(userName, password string) string {
var b bytes.Buffer
b.WriteString(userName)
b.WriteRune(':')
b.WriteString(password)

return base64.StdEncoding.EncodeToString(b.Bytes())
}
62 changes: 62 additions & 0 deletions basculehttp/basic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehttp

import (
"context"
"encoding/base64"
"testing"

"github.com/stretchr/testify/suite"
"github.com/xmidt-org/bascule/v1"
)

type BasicTestSuite struct {
suite.Suite
}

func (suite *BasicTestSuite) TestBasicAuth() {
encoded := BasicAuth("Aladdin", "open sesame")

// ripped right from RFC 2617 ...
suite.Equal("QWxhZGRpbjpvcGVuIHNlc2FtZQ==", encoded)
}

func (suite *BasicTestSuite) TestBasicTokenParser() {
suite.Run("Invalid", func() {
p := BasicTokenParser{}
token, err := p.Parse(context.Background(), "^%$@!()$kldfj34729$(&fhd") // hopelessly invalid ...

suite.ErrorIs(err, bascule.ErrInvalidCredentials)
suite.Nil(token)
})

suite.Run("ImproperlyFormatted", func() {
p := BasicTokenParser{}
token, err := p.Parse(
context.Background(),
base64.StdEncoding.EncodeToString([]byte("missing colon")),
)

suite.ErrorIs(err, bascule.ErrInvalidCredentials)
suite.Nil(token)
})

suite.Run("Success", func() {
p := BasicTokenParser{}
token, err := p.Parse(context.Background(), "QWxhZGRpbjpvcGVuIHNlc2FtZQ==")

suite.NoError(err)
suite.Require().NotNil(token)
suite.Equal("Aladdin", token.Principal())

suite.Require().Implements((*BasicToken)(nil), token)
suite.Equal("Aladdin", token.(BasicToken).UserName())
suite.Equal("open sesame", token.(BasicToken).Password())
})
}

func TestBasic(t *testing.T) {
suite.Run(t, new(BasicTestSuite))
}
58 changes: 4 additions & 54 deletions basculehttp/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,57 +16,7 @@ import (
)

type MiddlewareTestSuite struct {
suite.Suite

expectedPrincipal string
expectedPassword string
expectedToken bascule.Token
}

func (suite *MiddlewareTestSuite) SetupSuite() {
suite.expectedPrincipal = "testPrincipal"
suite.expectedPassword = "test_password"
suite.expectedToken = basicToken{
userName: suite.expectedPrincipal,
password: suite.expectedPassword,
}
}

// newRequest creates a standardized test request, devoid of any authorization.
func (suite *MiddlewareTestSuite) newRequest() *http.Request {
return httptest.NewRequest("GET", "/test", nil)
}

// newBasicAuthRequest creates a new test request configured with valid basic auth.
func (suite *MiddlewareTestSuite) newBasicAuthRequest() *http.Request {
request := suite.newRequest()
request.SetBasicAuth(suite.expectedPrincipal, suite.expectedPassword)
return request
}

// assertBasicAuthRequest asserts that the given request matches this suite's expectations.
func (suite *MiddlewareTestSuite) assertBasicAuthRequest(request *http.Request) {
suite.Require().NotNil(request)
suite.Equal("GET", request.Method)
suite.Equal("/test", request.URL.String())
}

// assertBasicAuthToken asserts that the token matches this suite's expectations.
func (suite *MiddlewareTestSuite) assertBasicAuthToken(token bascule.Token) {
suite.Require().NotNil(token)
suite.Equal(suite.expectedPrincipal, token.Principal())
suite.Require().Implements((*BasicToken)(nil), token)
suite.Equal(suite.expectedPrincipal, token.(BasicToken).UserName())
suite.Equal(suite.expectedPassword, token.(BasicToken).Password())
}

// newAuthorizationParser creates an AuthorizationParser that is expected to be valid.
// Assertions as to validity are made prior to returning.
func (suite *MiddlewareTestSuite) newAuthorizationParser(opts ...AuthorizationParserOption) *AuthorizationParser {
ap, err := NewAuthorizationParser(opts...)
suite.Require().NoError(err)
suite.Require().NotNil(ap)
return ap
TestSuite
}

// newAuthenticator creates a bascule.Authenticator that is expected to be valid.
Expand Down Expand Up @@ -325,14 +275,14 @@ func (suite *MiddlewareTestSuite) testBasicAuthSuccess() {
bascule.WithApproverFuncs(
func(_ context.Context, request *http.Request, token bascule.Token) error {
suite.assertBasicAuthRequest(request)
suite.assertBasicAuthToken(token)
suite.assertBasicToken(token)
return nil
},
),
bascule.WithAuthorizeListenerFuncs(
func(e bascule.AuthorizeEvent[*http.Request]) {
suite.assertBasicAuthRequest(e.Resource)
suite.assertBasicAuthToken(e.Token)
suite.assertBasicToken(e.Token)
authorizeEvent = true
},
),
Expand Down Expand Up @@ -434,7 +384,7 @@ func (suite *MiddlewareTestSuite) testBasicAuthAuthorizerError() {
bascule.WithApproverFuncs(
func(_ context.Context, resource *http.Request, token bascule.Token) error {
suite.assertBasicAuthRequest(resource)
suite.assertBasicAuthToken(token)
suite.assertBasicToken(token)
return expectedErr
},
),
Expand Down
28 changes: 28 additions & 0 deletions basculehttp/scheme_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehttp

import (
"net/http"
"testing"

"github.com/stretchr/testify/suite"
)

type SchemeTestSuite struct {
suite.Suite
}

func (suite *SchemeTestSuite) TestUnsupportedSchemeError() {
use := &UnsupportedSchemeError{
Scheme: Scheme("Unsupported"),
}

suite.Equal(http.StatusUnauthorized, use.StatusCode())
suite.Contains(use.Error(), "Unsupported")
}

func TestScheme(t *testing.T) {
suite.Run(t, new(SchemeTestSuite))
}
Loading

0 comments on commit 5379021

Please sign in to comment.