Skip to content

Commit

Permalink
Merge pull request #22 from Comcast/error-response-reason
Browse files Browse the repository at this point in the history
Added Error Response Reason, sent to new function on decorator error
  • Loading branch information
schmidtw authored May 9, 2019
2 parents 53d4ab3 + 55e7140 commit 124fd7c
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 24 deletions.
41 changes: 28 additions & 13 deletions bascule/basculehttp/constructor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package basculehttp

import (
"context"
"errors"
"fmt"
"net/http"
"net/textproto"
"strings"
Expand All @@ -15,9 +17,10 @@ const (
)

type constructor struct {
headerName string
authorizations map[bascule.Authorization]TokenFactory
getLogger func(context.Context) bascule.Logger
headerName string
authorizations map[bascule.Authorization]TokenFactory
getLogger func(context.Context) bascule.Logger
onErrorResponse OnErrorResponse
}

func (c *constructor) decorate(next http.Handler) http.Handler {
Expand All @@ -28,15 +31,16 @@ func (c *constructor) decorate(next http.Handler) http.Handler {
}
authorization := request.Header.Get(c.headerName)
if len(authorization) == 0 {
logger.Log(level.Key(), level.ErrorValue(), bascule.ErrorKey, "no authorization header")
err := errors.New("no authorization header")
c.error(logger, MissingHeader, "", err)
response.WriteHeader(http.StatusForbidden)
return
}

i := strings.IndexByte(authorization, ' ')
if i < 1 {
logger.Log(level.Key(), level.ErrorValue(), bascule.ErrorKey, "unexpected authorization header value",
"auth", authorization)
err := errors.New("unexpected authorization header value")
c.error(logger, InvalidHeader, authorization, err)
response.WriteHeader(http.StatusBadRequest)
return
}
Expand All @@ -47,17 +51,16 @@ func (c *constructor) decorate(next http.Handler) http.Handler {

tf, supported := c.authorizations[key]
if !supported {
logger.Log(level.Key(), level.ErrorValue(), bascule.ErrorKey, "key not supported", "key", key,
"auth", authorization[i+1:])
err := fmt.Errorf("key not supported: [%v]", key)
c.error(logger, KeyNotSupported, authorization, err)
response.WriteHeader(http.StatusForbidden)
return
}

ctx := request.Context()
token, err := tf.ParseAndValidate(ctx, request, key, authorization[i+1:])
if err != nil {
logger.Log(level.Key(), level.ErrorValue(), bascule.ErrorKey, err.Error(), "key", key,
"auth", authorization[i+1:])
c.error(logger, ParseFailed, authorization, err)
WriteResponse(response, http.StatusForbidden, err)
return
}
Expand All @@ -80,6 +83,11 @@ func (c *constructor) decorate(next http.Handler) http.Handler {
})
}

func (c *constructor) error(logger bascule.Logger, e ErrorResponseReason, auth string, err error) {
logger.Log(level.Key(), level.ErrorValue(), bascule.ErrorKey, err.Error(), "auth", auth)
c.onErrorResponse(e, err)
}

type COption func(*constructor)

func WithHeaderName(headerName string) COption {
Expand All @@ -104,12 +112,19 @@ func WithCLogger(getLogger func(context.Context) bascule.Logger) COption {
}
}

func WithCErrorResponseFunc(f OnErrorResponse) COption {
return func(c *constructor) {
c.onErrorResponse = f
}
}

// New returns an Alice-style constructor which decorates HTTP handlers with security code
func NewConstructor(options ...COption) func(http.Handler) http.Handler {
c := &constructor{
headerName: DefaultHeaderName,
authorizations: make(map[bascule.Authorization]TokenFactory),
getLogger: bascule.GetDefaultLoggerFunc,
headerName: DefaultHeaderName,
authorizations: make(map[bascule.Authorization]TokenFactory),
getLogger: bascule.GetDefaultLoggerFunc,
onErrorResponse: DefaultOnErrorResponse,
}

for _, o := range options {
Expand Down
1 change: 1 addition & 0 deletions bascule/basculehttp/constructor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func TestConstructor(t *testing.T) {
WithCLogger(func(_ context.Context) bascule.Logger {
return bascule.Logger(log.NewJSONLogger(log.NewSyncWriter(os.Stdout)))
}),
WithCErrorResponseFunc(DefaultOnErrorResponse),
)
c2 := NewConstructor(
WithHeaderName(""),
Expand Down
31 changes: 20 additions & 11 deletions bascule/basculehttp/enforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package basculehttp

import (
"context"
"errors"
"net/http"

"github.com/goph/emperror"
Expand All @@ -24,6 +25,7 @@ type enforcer struct {
notFoundBehavior NotFoundBehavior
rules map[bascule.Authorization]bascule.Validators
getLogger func(context.Context) bascule.Logger
onErrorResponse OnErrorResponse
}

func (e *enforcer) decorate(next http.Handler) http.Handler {
Expand All @@ -35,35 +37,35 @@ func (e *enforcer) decorate(next http.Handler) http.Handler {
}
auth, ok := bascule.FromContext(ctx)
if !ok {
logger.Log(level.Key(), level.ErrorValue(), bascule.ErrorKey, "no authentication found")
err := errors.New("no authentication found")
logger.Log(level.Key(), level.ErrorValue(), bascule.ErrorKey, 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.Log(level.Key(), level.ErrorValue(),
bascule.ErrorKey, "no rules found for authorization", "rules", rules,
bascule.ErrorKey, err.Error(), "rules", rules,
"authorization", auth.Authorization, "behavior", 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 {
errs := []string{err.Error()}
if es, ok := err.(bascule.Errors); ok {
for _, e := range es {
errs = append(errs, e.Error())
}
}
logger.Log(append(emperror.Context(err), level.Key(), level.ErrorValue(), bascule.ErrorKey, errs)...)
logger.Log(append(emperror.Context(err), level.Key(), level.ErrorValue(), bascule.ErrorKey, err)...)
e.onErrorResponse(ChecksFailed, err)
WriteResponse(response, http.StatusForbidden, err)
return
}
Expand Down Expand Up @@ -93,10 +95,17 @@ func WithELogger(getLogger func(context.Context) bascule.Logger) EOption {
}
}

func WithEErrorResponseFunc(f OnErrorResponse) EOption {
return func(e *enforcer) {
e.onErrorResponse = f
}
}

func NewEnforcer(options ...EOption) func(http.Handler) http.Handler {
e := &enforcer{
rules: make(map[bascule.Authorization]bascule.Validators),
getLogger: bascule.GetDefaultLoggerFunc,
rules: make(map[bascule.Authorization]bascule.Validators),
getLogger: bascule.GetDefaultLoggerFunc,
onErrorResponse: DefaultOnErrorResponse,
}

for _, o := range options {
Expand Down
1 change: 1 addition & 0 deletions bascule/basculehttp/enforcer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func TestEnforcer(t *testing.T) {
WithELogger(func(_ context.Context) bascule.Logger {
return bascule.Logger(log.NewJSONLogger(log.NewSyncWriter(os.Stdout)))
}),
WithEErrorResponseFunc(DefaultOnErrorResponse),
)
tests := []struct {
description string
Expand Down
22 changes: 22 additions & 0 deletions bascule/basculehttp/errorResponseReason.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package basculehttp

//go:generate stringer -type=ErrorResponseReason
type ErrorResponseReason int

// Behavior on not found
const (
MissingHeader ErrorResponseReason = iota
InvalidHeader
KeyNotSupported
ParseFailed
MissingAuthentication
ChecksNotFound
ChecksFailed
)

type OnErrorResponse func(ErrorResponseReason, error)

// default function does nothing
func DefaultOnErrorResponse(_ ErrorResponseReason, _ error) {
return
}
16 changes: 16 additions & 0 deletions bascule/basculehttp/errorresponsereason_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions bascule/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ type Attributes map[string]interface{}

// TODO: Add dotted path support and support for common concrete types, e.g. GetString
func (a Attributes) Get(key string) (interface{}, bool) {
if a == nil {
return nil, false
}
v, ok := a[key]
return v, ok
}
Expand Down
5 changes: 5 additions & 0 deletions bascule/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ func TestGet(t *testing.T) {
val, ok = attributes.Get("noval")
assert.Empty(val)
assert.False(ok)

emptyAttributes := Attributes(nil)
val, ok = emptyAttributes.Get("test")
assert.Nil(val)
assert.False(ok)
}

0 comments on commit 124fd7c

Please sign in to comment.