Skip to content

Commit

Permalink
Merge pull request #245 from xmidt-org/feature/error-interfaces
Browse files Browse the repository at this point in the history
Feature/error interfaces
  • Loading branch information
johnabass authored Mar 7, 2024
2 parents 1dc6042 + 0c5b72b commit 85e0935
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 84 deletions.
2 changes: 1 addition & 1 deletion basculehttp/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var defaultCredentialsParser bascule.CredentialsParser = bascule.CredentialsPars
Value: value,
}
} else {
err = &bascule.InvalidCredentialsError{
err = &bascule.BadCredentialsError{
Raw: raw,
}
}
Expand Down
9 changes: 2 additions & 7 deletions basculehttp/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type CredentialsTestSuite struct {
func (suite *CredentialsTestSuite) testDefaultCredentialsParserSuccess() {
const (
expectedScheme bascule.Scheme = "Test"
expectedValue = "credentialValue"
expectedValue string = "credentialValue"
)

testCases := []string{
Expand All @@ -43,11 +43,6 @@ func (suite *CredentialsTestSuite) testDefaultCredentialsParserSuccess() {
}

func (suite *CredentialsTestSuite) testDefaultCredentialsParserFailure() {
const (
expectedScheme bascule.Scheme = "Test"
expectedValue = "credentialValue"
)

testCases := []string{
"",
" ",
Expand All @@ -67,7 +62,7 @@ func (suite *CredentialsTestSuite) testDefaultCredentialsParserFailure() {
suite.Require().Error(err)
suite.Equal(bascule.Credentials{}, creds)

var ice *bascule.InvalidCredentialsError
var ice *bascule.BadCredentialsError
if suite.ErrorAs(err, &ice) {
suite.Equal(testCase, ice.Raw)
}
Expand Down
22 changes: 20 additions & 2 deletions basculehttp/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"encoding/json"
"errors"
"net/http"

"github.com/xmidt-org/bascule/v1"
)

// ErrorStatusCoder is a strategy for determining the HTTP response code for an error.
Expand All @@ -18,12 +20,28 @@ import (
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 first tries to see if the error implements bascule.Error, in which case
// the error's type will dictate the response code. Next, if the wrapper error provides
// a StatusCode() method, that code is used. Failing all of that, the defaultCode is
// returned.
//
// 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 {
switch bascule.GetErrorType(err) {
case bascule.ErrorTypeMissingCredentials:
return http.StatusUnauthorized

case bascule.ErrorTypeBadCredentials:
return http.StatusBadRequest

case bascule.ErrorTypeInvalidCredentials:
return http.StatusForbidden

case bascule.ErrorTypeForbidden:
return http.StatusForbidden
}

type statusCoder interface {
StatusCode() int
}
Expand Down
43 changes: 0 additions & 43 deletions credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,6 @@

package bascule

import "strings"

// InvalidCredentialsError is returned typically by CredentialsParser.Parse
// to indicate that a raw, serialized credentials were badly formatted.
type InvalidCredentialsError struct {
// Cause represents any lower-level error that occurred, if any.
Cause error

// Raw represents the raw credentials that couldn't be parsed.
Raw string
}

func (err *InvalidCredentialsError) Unwrap() error { return err.Cause }

func (err *InvalidCredentialsError) Error() string {
var o strings.Builder
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()
}

// Scheme represents how a security token should be parsed. For HTTP, examples
// of a scheme are "Bearer" and "Basic".
type Scheme string
Expand Down
31 changes: 0 additions & 31 deletions credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,6 @@ 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")
Expand Down
109 changes: 109 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: 2021 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package bascule

import (
"errors"
"strings"
)

// ErrorType is an enumeration type for various types of security errors.
// This type can be used to determine more detail about the context of an error.
type ErrorType int

const (
// ErrorTypeUnknown indicates an error that didn't specify an ErrorType,
// possibly because the error didn't implement the Error interface in this package.
ErrorTypeUnknown ErrorType = iota

// ErrorTypeMissingCredentials indicates that no credentials could be found.
// For example, this is the type used when no credentials are present in an HTTP request.
ErrorTypeMissingCredentials

// ErrorTypeBadCredentials indcates that credentials exist, but they were badly formatted.
// In other words, bascule could not parse the credentials.
ErrorTypeBadCredentials

// ErrorTypeInvalidCredentials indicates that credentials exist and are properly formatted,
// but they failed validation. Typically, this is due to failed authentication. It can also
// mean that a token's fields are invalid, such as the exp field of a JWT.
ErrorTypeInvalidCredentials

// ErrorTypeForbidden indicates that a token did not have sufficient privileges to
// perform an operation.
ErrorTypeForbidden
)

// Error is an optional interface that errors may implement to expose security
// metadata about the error.
type Error interface {
// Type is the ErrorType describing this error.
Type() ErrorType
}

type typedError struct {
error
et ErrorType
}

func (te *typedError) Unwrap() error { return te.error }

func (te *typedError) Type() ErrorType { return te.et }

// NewTypedError wraps a given error and associates an ErrorType with it.
// The returned error will implement the Error interface in this package,
// and will have an Unwrap method that returns err.
func NewTypedError(err error, et ErrorType) error {
return &typedError{
error: err,
et: et,
}
}

// GetErrorType examines err to determine its associated metadata type. If err
// does not implement Error, this function returns ErrorTypeUnknown.
func GetErrorType(err error) ErrorType {
var e Error
if errors.As(err, &e) {
return e.Type()
}

return ErrorTypeUnknown
}

// UnsupportedSchemeError indicates that a credentials scheme was not supported
// by a TokenParser.
type UnsupportedSchemeError struct {
// Scheme is the unsupported credential scheme.
Scheme Scheme
}

// Type tags errors of this type as ErrorTypeBadCredentials.
func (err *UnsupportedSchemeError) Type() ErrorType { return ErrorTypeBadCredentials }

func (err *UnsupportedSchemeError) Error() string {
var o strings.Builder
o.WriteString(`Unsupported scheme: "`)
o.WriteString(string(err.Scheme))
o.WriteRune('"')
return o.String()
}

// BadCredentialsError is a general-purpose error indicating that credentials
// could not be parsed.
type BadCredentialsError struct {
// Raw is the raw value of the credentials that could not be parsed.
Raw string
}

// Type tags errors of this type as ErrorTypeBadCredentials.
func (err *BadCredentialsError) Type() ErrorType { return ErrorTypeBadCredentials }

func (err *BadCredentialsError) Error() string {
var o strings.Builder
o.WriteString(`Bad credentials: "`)
o.WriteString(err.Raw)
o.WriteRune('"')
return o.String()
}
65 changes: 65 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package bascule

import (
"errors"
"testing"

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

type ErrorSuite struct {
suite.Suite
}

func (suite *ErrorSuite) TestUnsupportedSchemeError() {
err := UnsupportedSchemeError{
Scheme: Scheme("scheme"),
}

suite.Contains(err.Error(), "scheme")
suite.Equal(ErrorTypeBadCredentials, err.Type())
}

func (suite *ErrorSuite) TestBadCredentialsError() {
err := BadCredentialsError{
Raw: "these are an unparseable, raw credentials",
}

suite.Contains(err.Error(), "these are an unparseable, raw credentials")
suite.Equal(ErrorTypeBadCredentials, err.Type())
}

func (suite *ErrorSuite) TestNewTypedError() {
original := errors.New("original error")
typed := NewTypedError(original, ErrorTypeBadCredentials)

suite.ErrorIs(typed, original)
suite.Require().Implements((*Error)(nil), typed)

var e Error
suite.Require().ErrorAs(typed, &e)
suite.Equal(
ErrorTypeBadCredentials,
e.Type(),
)
}

func (suite *ErrorSuite) TestGetErrorType() {
suite.Run("Unknown", func() {
suite.Equal(
ErrorTypeUnknown,
GetErrorType(errors.New("this is an error that is unknown to bascule")),
)
})

suite.Run("ImplementsError", func() {
suite.Equal(
ErrorTypeBadCredentials,
GetErrorType(new(BadCredentialsError)),
)
})
}

func TestError(t *testing.T) {
suite.Run(t, new(ErrorSuite))
}

0 comments on commit 85e0935

Please sign in to comment.