Merge pull request 'feature: authentication middlewares' (#20) from feature/expose-scope into 0.3.0
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #20
This commit is contained in:
commit
214e2348aa
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
// Auth can be used by http middleware to
|
||||||
|
// 1) consult required roles in @Auth.Required
|
||||||
|
// 2) update active roles in @Auth.Active
|
||||||
|
type Auth struct {
|
||||||
|
// required roles for this request
|
||||||
|
// - the first dimension of the array reads as a OR
|
||||||
|
// - the second dimension reads as a AND
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// [ [A, B], [C, D] ] reads: roles (A and B) or (C and D) are required
|
||||||
|
//
|
||||||
|
// Warning: must not be mutated
|
||||||
|
Required [][]string
|
||||||
|
|
||||||
|
// active roles to be updated by authentication
|
||||||
|
// procedures (e.g. jwt)
|
||||||
|
Active []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Granted returns whether the authorization is granted
|
||||||
|
// i.e. Auth.Active fulfills Auth.Required
|
||||||
|
func (a Auth) Granted() bool {
|
||||||
|
var nothingRequired = true
|
||||||
|
|
||||||
|
// first dimension: OR ; at least one is valid
|
||||||
|
for _, required := range a.Required {
|
||||||
|
// empty list
|
||||||
|
if len(required) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nothingRequired = false
|
||||||
|
|
||||||
|
// second dimension: AND ; all required must be fulfilled
|
||||||
|
if a.fulfills(required) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nothingRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns whether Auth.Active fulfills (contains) all @required roles
|
||||||
|
func (a Auth) fulfills(required []string) bool {
|
||||||
|
for _, requiredRole := range required {
|
||||||
|
var found = false
|
||||||
|
for _, activeRole := range a.Active {
|
||||||
|
if activeRole == requiredRole {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// missing role -> fail
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// all @required are fulfilled
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCombination(t *testing.T) {
|
||||||
|
tcases := []struct {
|
||||||
|
Name string
|
||||||
|
Required [][]string
|
||||||
|
Active []string
|
||||||
|
Granted bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "no requirement none given",
|
||||||
|
Required: [][]string{},
|
||||||
|
Active: []string{},
|
||||||
|
Granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "no requirement 1 given",
|
||||||
|
Required: [][]string{},
|
||||||
|
Active: []string{"a"},
|
||||||
|
Granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "no requirement some given",
|
||||||
|
Required: [][]string{},
|
||||||
|
Active: []string{"a", "b"},
|
||||||
|
Granted: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "1 required none given",
|
||||||
|
Required: [][]string{{"a"}},
|
||||||
|
Active: []string{},
|
||||||
|
Granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "1 required fulfilled",
|
||||||
|
Required: [][]string{{"a"}},
|
||||||
|
Active: []string{"a"},
|
||||||
|
Granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "1 required mismatch",
|
||||||
|
Required: [][]string{{"a"}},
|
||||||
|
Active: []string{"b"},
|
||||||
|
Granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "2 required none gien",
|
||||||
|
Required: [][]string{{"a", "b"}},
|
||||||
|
Active: []string{},
|
||||||
|
Granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "2 required other given",
|
||||||
|
Required: [][]string{{"a", "b"}},
|
||||||
|
Active: []string{"c"},
|
||||||
|
Granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "2 required one given",
|
||||||
|
Required: [][]string{{"a", "b"}},
|
||||||
|
Active: []string{"a"},
|
||||||
|
Granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "2 required fulfilled",
|
||||||
|
Required: [][]string{{"a", "b"}},
|
||||||
|
Active: []string{"a", "b"},
|
||||||
|
Granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "2 or 2 required first fulfilled",
|
||||||
|
Required: [][]string{{"a", "b"}, {"c", "d"}},
|
||||||
|
Active: []string{"a", "b"},
|
||||||
|
Granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "2 or 2 required second fulfilled",
|
||||||
|
Required: [][]string{{"a", "b"}, {"c", "d"}},
|
||||||
|
Active: []string{"c", "d"},
|
||||||
|
Granted: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tcase := range tcases {
|
||||||
|
t.Run(tcase.Name, func(t *testing.T) {
|
||||||
|
|
||||||
|
auth := Auth{
|
||||||
|
Required: tcase.Required,
|
||||||
|
Active: tcase.Active,
|
||||||
|
}
|
||||||
|
|
||||||
|
// all right
|
||||||
|
if tcase.Granted == auth.Granted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tcase.Granted && !auth.Granted() {
|
||||||
|
t.Fatalf("expected granted authorization")
|
||||||
|
}
|
||||||
|
t.Fatalf("unexpected granted authorization")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
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
|
||||||
|
|
27
handler.go
27
handler.go
|
@ -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 {
|
||||||
|
@ -50,6 +50,29 @@ func (s Handler) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var auth = api.Auth{
|
||||||
|
Required: service.Scope,
|
||||||
|
Active: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. run auth-aware middlewares
|
||||||
|
var h = api.AuthHandlerFunc(func(a api.Auth, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !a.Granted() {
|
||||||
|
handleError(api.ErrPermission, w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.handle(input, handler, service, w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, adapter := range s.authAdapters {
|
||||||
|
h = adapter(h)
|
||||||
|
}
|
||||||
|
h(auth, w, r)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handler) handle(input *reqdata.T, handler *apiHandler, service *config.Service, w http.ResponseWriter, r *http.Request) {
|
||||||
// 5. pass execution to the handler
|
// 5. pass execution to the handler
|
||||||
ctx := api.Ctx{Res: w, Req: r}
|
ctx := api.Ctx{Res: w, Req: r}
|
||||||
var outData, outErr = handler.dyn.Handle(ctx, input.Data)
|
var outData, outErr = handler.dyn.Handle(ctx, input.Data)
|
||||||
|
|
|
@ -0,0 +1,241 @@
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAuth(t *testing.T) {
|
||||||
|
|
||||||
|
tt := []struct {
|
||||||
|
name string
|
||||||
|
manifest string
|
||||||
|
permissions []string
|
||||||
|
granted bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "provide only requirement A",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{"A"},
|
||||||
|
granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing requirement",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{},
|
||||||
|
granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing requirements",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{},
|
||||||
|
granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing some requirements",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{"A"},
|
||||||
|
granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provide requirements",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{"A", "B"},
|
||||||
|
granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing OR requirements",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"], ["B"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{"C"},
|
||||||
|
granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provide 1 OR requirement",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"], ["B"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{"A"},
|
||||||
|
granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provide both OR requirements",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"], ["B"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{"A", "B"},
|
||||||
|
granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing composite OR requirements",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{},
|
||||||
|
granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing partial composite OR requirements",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{"A", "C"},
|
||||||
|
granted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provide 1 composite OR requirement",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{"A", "B", "C"},
|
||||||
|
granted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provide both composite OR requirements",
|
||||||
|
manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`,
|
||||||
|
permissions: []string{"A", "B", "C", "D"},
|
||||||
|
granted: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
builder := &Builder{}
|
||||||
|
if err := addBuiltinTypes(builder); err != nil {
|
||||||
|
t.Fatalf("unexpected error <%v>", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tester middleware (last executed)
|
||||||
|
builder.WithAuth(func(next api.AuthHandlerFunc) api.AuthHandlerFunc {
|
||||||
|
return func(a api.Auth, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if a.Granted() == tc.granted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if a.Granted() {
|
||||||
|
t.Fatalf("unexpected granted auth")
|
||||||
|
} else {
|
||||||
|
t.Fatalf("expected granted auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.WithAuth(func(next api.AuthHandlerFunc) api.AuthHandlerFunc {
|
||||||
|
return func(a api.Auth, w http.ResponseWriter, r *http.Request) {
|
||||||
|
a.Active = tc.permissions
|
||||||
|
next(a, w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
err := builder.Setup(strings.NewReader(tc.manifest))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("setup: unexpected error <%v>", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathHandler := func(ctx api.Ctx) (*struct{}, api.Err) {
|
||||||
|
return nil, api.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue