feature: authentication middlewares #20

Merged
xdrm-brackets merged 3 commits from feature/expose-scope into 0.3.0 2021-05-18 13:57:57 +00:00
5 changed files with 125 additions and 97 deletions
Showing only changes of commit 4f55302e8a - Show all commits

View File

@ -4,3 +4,10 @@ import "net/http"
// Adapter to encapsulate incoming requests // Adapter to encapsulate incoming requests
type Adapter func(http.HandlerFunc) http.HandlerFunc type Adapter func(http.HandlerFunc) http.HandlerFunc
// AuthHandlerFunc is http.HandlerFunc with additional Authorization information
type AuthHandlerFunc func(Auth, http.ResponseWriter, *http.Request)
// AuthAdapter to encapsulate incoming request with access to api.Auth
// to manage permissions
type AuthAdapter func(AuthHandlerFunc) AuthHandlerFunc

View File

@ -16,6 +16,7 @@ type Builder struct {
conf *config.Server conf *config.Server
handlers []*apiHandler handlers []*apiHandler
adapters []api.Adapter adapters []api.Adapter
authAdapters []api.AuthAdapter
} }
// represents an api handler (method-pattern combination) // represents an api handler (method-pattern combination)
@ -40,8 +41,8 @@ func (b *Builder) AddType(t datatype.T) error {
return nil return nil
} }
// Use adds an http adapter (middleware) // With adds an http adapter (middleware)
func (b *Builder) Use(adapter api.Adapter) { func (b *Builder) With(adapter api.Adapter) {
if b.conf == nil { if b.conf == nil {
b.conf = &config.Server{} b.conf = &config.Server{}
} }
@ -51,6 +52,17 @@ func (b *Builder) Use(adapter api.Adapter) {
b.adapters = append(b.adapters, adapter) b.adapters = append(b.adapters, adapter)
} }
// WithAuth adds an http adapter with auth capabilities (middleware)
func (b *Builder) WithAuth(adapter api.AuthAdapter) {
if b.conf == nil {
b.conf = &config.Server{}
}
if b.authAdapters == nil {
b.authAdapters = make([]api.AuthAdapter, 0)
}
b.authAdapters = append(b.authAdapters, adapter)
}
// Setup the builder with its api definition file // Setup the builder with its api definition file
// panics if already setup // panics if already setup
func (b *Builder) Setup(r io.Reader) error { func (b *Builder) Setup(r io.Reader) error {

View File

@ -1,12 +1,8 @@
package aicra package aicra
import ( import (
"bytes"
"context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"net/http/httptest"
"strings" "strings"
"testing" "testing"
@ -52,92 +48,6 @@ func TestAddType(t *testing.T) {
} }
} }
func TestUse(t *testing.T) {
builder := &Builder{}
if err := addBuiltinTypes(builder); err != nil {
t.Fatalf("unexpected error <%v>", err)
}
// build @n middlewares that take data from context and increment it
n := 1024
type ckey int
const key ckey = 0
middleware := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
newr := r
// first time -> store 1
value := r.Context().Value(key)
if value == nil {
newr = r.WithContext(context.WithValue(r.Context(), key, int(1)))
next(w, newr)
return
}
// get value and increment
cast, ok := value.(int)
if !ok {
t.Fatalf("value is not an int")
}
cast++
newr = r.WithContext(context.WithValue(r.Context(), key, cast))
next(w, newr)
}
}
// add middleware @n times
for i := 0; i < n; i++ {
builder.Use(middleware)
}
config := strings.NewReader(`[ { "method": "GET", "path": "/path", "scope": [[]], "info": "info", "in": {}, "out": {} } ]`)
err := builder.Setup(config)
if err != nil {
t.Fatalf("setup: unexpected error <%v>", err)
}
pathHandler := func(ctx api.Ctx) (*struct{}, api.Err) {
// write value from middlewares into response
value := ctx.Req.Context().Value(key)
if value == nil {
t.Fatalf("nothing found in context")
}
cast, ok := value.(int)
if !ok {
t.Fatalf("cannot cast context data to int")
}
// write to response
ctx.Res.Write([]byte(fmt.Sprintf("#%d#", cast)))
return nil, api.ErrSuccess
}
if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil {
t.Fatalf("bind: unexpected error <%v>", err)
}
handler, err := builder.Build()
if err != nil {
t.Fatalf("build: unexpected error <%v>", err)
}
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{})
// test request
handler.ServeHTTP(response, request)
if response.Body == nil {
t.Fatalf("response has no body")
}
token := fmt.Sprintf("#%d#", n)
if !strings.Contains(response.Body.String(), token) {
t.Fatalf("expected '%s' to be in response <%s>", token, response.Body.String())
}
}
func TestBind(t *testing.T) { func TestBind(t *testing.T) {
tcases := []struct { tcases := []struct {
Name string Name string

View File

@ -13,7 +13,7 @@ type Handler Builder
// ServeHTTP implements http.Handler and wraps it in middlewares (adapters) // ServeHTTP implements http.Handler and wraps it in middlewares (adapters)
func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var h = http.HandlerFunc(s.handleRequest) var h = http.HandlerFunc(s.resolve)
for _, adapter := range s.adapters { for _, adapter := range s.adapters {
h = adapter(h) h = adapter(h)
@ -21,7 +21,7 @@ func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h(w, r) h(w, r)
} }
func (s Handler) handleRequest(w http.ResponseWriter, r *http.Request) { func (s Handler) resolve(w http.ResponseWriter, r *http.Request) {
// 1. find a matching service from config // 1. find a matching service from config
var service = s.conf.Find(r) var service = s.conf.Find(r)
if service == nil { if service == nil {

99
handler_test.go Normal file
View File

@ -0,0 +1,99 @@
package aicra
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.xdrm.io/go/aicra/api"
)
func TestWith(t *testing.T) {
builder := &Builder{}
if err := addBuiltinTypes(builder); err != nil {
t.Fatalf("unexpected error <%v>", err)
}
// build @n middlewares that take data from context and increment it
n := 1024
type ckey int
const key ckey = 0
middleware := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
newr := r
// first time -> store 1
value := r.Context().Value(key)
if value == nil {
newr = r.WithContext(context.WithValue(r.Context(), key, int(1)))
next(w, newr)
return
}
// get value and increment
cast, ok := value.(int)
if !ok {
t.Fatalf("value is not an int")
}
cast++
newr = r.WithContext(context.WithValue(r.Context(), key, cast))
next(w, newr)
}
}
// add middleware @n times
for i := 0; i < n; i++ {
builder.With(middleware)
}
config := strings.NewReader(`[ { "method": "GET", "path": "/path", "scope": [[]], "info": "info", "in": {}, "out": {} } ]`)
err := builder.Setup(config)
if err != nil {
t.Fatalf("setup: unexpected error <%v>", err)
}
pathHandler := func(ctx api.Ctx) (*struct{}, api.Err) {
// write value from middlewares into response
value := ctx.Req.Context().Value(key)
if value == nil {
t.Fatalf("nothing found in context")
}
cast, ok := value.(int)
if !ok {
t.Fatalf("cannot cast context data to int")
}
// write to response
ctx.Res.Write([]byte(fmt.Sprintf("#%d#", cast)))
return nil, api.ErrSuccess
}
if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil {
t.Fatalf("bind: unexpected error <%v>", err)
}
handler, err := builder.Build()
if err != nil {
t.Fatalf("build: unexpected error <%v>", err)
}
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{})
// test request
handler.ServeHTTP(response, request)
if response.Body == nil {
t.Fatalf("response has no body")
}
token := fmt.Sprintf("#%d#", n)
if !strings.Contains(response.Body.String(), token) {
t.Fatalf("expected '%s' to be in response <%s>", token, response.Body.String())
}
}