Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/authorization tests #283

Merged
merged 2 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading