feat: create api.Auth wrapping authorization management
This commit is contained in:
parent
e3d24ae1ef
commit
18d809c4ca
|
@ -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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue