feature: authentication middlewares #20
|
@ -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
|
||||||
|
|
22
builder.go
22
builder.go
|
@ -13,9 +13,10 @@ import (
|
||||||
|
|
||||||
// Builder for an aicra server
|
// Builder for an aicra server
|
||||||
type Builder struct {
|
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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue