Updated auth interface (#1384)
* Updated auth interface * Add Rule * Remove Rule * Return token from Renew * Renew => Refresh * Implement Tokens & Default Auth Implementation * Change default auth to noop * Change default auth to noop * Move token.Token to auth.Token * Remove Token from Account * Auth service implementation * Decode JWT locally * Cookie for secret * Move string to bottom of interface definition * Depricate auth_exclude * Update auth wrappers * Update go.sum Co-authored-by: Ben Toogood <ben@micro.mu>
This commit is contained in:
72
auth/store/rules.go
Normal file
72
auth/store/rules.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/auth"
|
||||
"github.com/micro/go-micro/v2/store"
|
||||
)
|
||||
|
||||
// Rule is an access control rule
|
||||
type Rule struct {
|
||||
Role string `json:"rule"`
|
||||
Resource *auth.Resource `json:"resource"`
|
||||
}
|
||||
|
||||
// Key to be used when written to the store
|
||||
func (r *Rule) Key() string {
|
||||
comps := []string{r.Resource.Type, r.Resource.Name, r.Resource.Endpoint, r.Role}
|
||||
return strings.Join(comps, "/")
|
||||
}
|
||||
|
||||
// Bytes returns json encoded bytes
|
||||
func (r *Rule) Bytes() []byte {
|
||||
bytes, _ := json.Marshal(r)
|
||||
return bytes
|
||||
}
|
||||
|
||||
// isValidRule returns a bool, indicating if a rule permits access to a
|
||||
// resource for a given account
|
||||
func isValidRule(rule Rule, acc *auth.Account, res *auth.Resource) bool {
|
||||
if rule.Role == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, role := range acc.Roles {
|
||||
if rule.Role == role {
|
||||
return true
|
||||
}
|
||||
|
||||
// allow user.anything if role is user.*
|
||||
if strings.HasSuffix(rule.Role, ".*") && strings.HasPrefix(rule.Role, role+".") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// listRules gets all the rules from the store which have a key
|
||||
// prefix matching the filters
|
||||
func (s *Store) listRules(filters ...string) ([]Rule, error) {
|
||||
// get the records from the store
|
||||
prefix := strings.Join(filters, "/")
|
||||
recs, err := s.opts.Store.Read(prefix, store.ReadPrefix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// unmarshal the records
|
||||
rules := make([]Rule, 0, len(recs))
|
||||
for _, rec := range recs {
|
||||
var r Rule
|
||||
if err := json.Unmarshal(rec.Value, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rules = append(rules, r)
|
||||
}
|
||||
|
||||
// return the rules
|
||||
return rules, nil
|
||||
}
|
@@ -1,130 +1,159 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/micro/go-micro/v2/auth"
|
||||
"github.com/micro/go-micro/v2/errors"
|
||||
"github.com/micro/go-micro/v2/auth/token"
|
||||
"github.com/micro/go-micro/v2/auth/token/basic"
|
||||
"github.com/micro/go-micro/v2/store"
|
||||
memStore "github.com/micro/go-micro/v2/store/memory"
|
||||
)
|
||||
|
||||
type Auth struct {
|
||||
store store.Store
|
||||
opts auth.Options
|
||||
}
|
||||
|
||||
// NewAuth returns an instance of store auth
|
||||
// NewAuth returns a new default registry which is store
|
||||
func NewAuth(opts ...auth.Option) auth.Auth {
|
||||
var options auth.Options
|
||||
var s Store
|
||||
s.Init(opts...)
|
||||
return &s
|
||||
}
|
||||
|
||||
// Store implementation of auth
|
||||
type Store struct {
|
||||
secretProvider token.Provider
|
||||
tokenProvider token.Provider
|
||||
opts auth.Options
|
||||
}
|
||||
|
||||
// String returns store
|
||||
func (s *Store) String() string {
|
||||
return "store"
|
||||
}
|
||||
|
||||
// Init the auth
|
||||
func (s *Store) Init(opts ...auth.Option) {
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
o(&s.opts)
|
||||
}
|
||||
|
||||
return &Auth{
|
||||
store: store.DefaultStore,
|
||||
opts: options,
|
||||
// use the default store as a fallback
|
||||
if s.opts.Store == nil {
|
||||
s.opts.Store = store.DefaultStore
|
||||
}
|
||||
|
||||
// noop will not work for auth
|
||||
if s.opts.Store.String() == "noop" {
|
||||
s.opts.Store = memStore.NewStore()
|
||||
}
|
||||
|
||||
if s.tokenProvider == nil {
|
||||
s.tokenProvider = basic.NewTokenProvider(token.WithStore(s.opts.Store))
|
||||
}
|
||||
if s.secretProvider == nil {
|
||||
s.secretProvider = basic.NewTokenProvider(token.WithStore(s.opts.Store))
|
||||
}
|
||||
}
|
||||
|
||||
// Init the auth package
|
||||
func (a *Auth) Init(opts ...auth.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&a.opts)
|
||||
}
|
||||
return nil
|
||||
// Options returns the options
|
||||
func (s *Store) Options() auth.Options {
|
||||
return s.opts
|
||||
}
|
||||
|
||||
// Options returns the options set
|
||||
func (a *Auth) Options() auth.Options {
|
||||
return a.opts
|
||||
}
|
||||
|
||||
// Generate a new auth Account
|
||||
func (a *Auth) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
|
||||
// generate the token
|
||||
token, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate a new account
|
||||
func (s *Store) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
|
||||
// parse the options
|
||||
options := auth.NewGenerateOptions(opts...)
|
||||
|
||||
// construct the account
|
||||
sa := auth.Account{
|
||||
Id: id,
|
||||
Token: token.String(),
|
||||
Created: time.Now(),
|
||||
Metadata: options.Metadata,
|
||||
Roles: options.Roles,
|
||||
// Generate a long-lived secret
|
||||
secretOpts := []token.GenerateOption{
|
||||
token.WithExpiry(options.SecretExpiry),
|
||||
token.WithMetadata(options.Metadata),
|
||||
token.WithRoles(options.Roles),
|
||||
}
|
||||
|
||||
// encode the data to bytes
|
||||
// TODO: replace with json
|
||||
buf := &bytes.Buffer{}
|
||||
e := gob.NewEncoder(buf)
|
||||
if err := e.Encode(sa); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// write to the store
|
||||
err = a.store.Write(&store.Record{
|
||||
Key: token.String(),
|
||||
Value: buf.Bytes(),
|
||||
})
|
||||
secret, err := s.secretProvider.Generate(id, secretOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return the result
|
||||
return &sa, nil
|
||||
// return the account
|
||||
return &auth.Account{
|
||||
ID: id,
|
||||
Roles: options.Roles,
|
||||
Metadata: options.Metadata,
|
||||
Secret: secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Revoke an authorization Account
|
||||
func (a *Auth) Revoke(token string) error {
|
||||
records, err := a.store.Read(token, store.ReadSuffix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return errors.BadRequest("go.micro.auth", "token not found")
|
||||
// Grant access to a resource
|
||||
func (s *Store) Grant(role string, res *auth.Resource) error {
|
||||
r := Rule{role, res}
|
||||
return s.opts.Store.Write(&store.Record{Key: r.Key(), Value: r.Bytes()})
|
||||
}
|
||||
|
||||
// Revoke access to a resource
|
||||
func (s *Store) Revoke(role string, res *auth.Resource) error {
|
||||
r := Rule{role, res}
|
||||
|
||||
err := s.opts.Store.Delete(r.Key())
|
||||
if err == store.ErrNotFound {
|
||||
return auth.ErrNotFound
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
if err := a.store.Delete(r.Key); err != nil {
|
||||
return errors.InternalServerError("go.micro.auth", "error deleting from store")
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify an account has access to a resource
|
||||
func (s *Store) Verify(acc *auth.Account, res *auth.Resource) error {
|
||||
queries := [][]string{
|
||||
{res.Type, "*"}, // check for wildcard resource type, e.g. service.*
|
||||
{res.Type, res.Name, "*"}, // check for wildcard name, e.g. service.foo*
|
||||
{res.Type, res.Name, res.Endpoint, "*"}, // check for wildcard endpoints, e.g. service.foo.ListFoo:*
|
||||
{res.Type, res.Name, res.Endpoint}, // check for specific role, e.g. service.foo.ListFoo:admin
|
||||
}
|
||||
|
||||
for _, q := range queries {
|
||||
rules, err := s.listRules(q...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if isValidRule(rule, acc, res) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return auth.ErrForbidden
|
||||
}
|
||||
|
||||
// Verify an account token
|
||||
func (a *Auth) Verify(token string) (*auth.Account, error) {
|
||||
// lookup the record by token
|
||||
records, err := a.store.Read(token, store.ReadSuffix())
|
||||
if err == store.ErrNotFound || len(records) == 0 {
|
||||
return nil, errors.Unauthorized("go.micro.auth", "invalid token")
|
||||
// Inspect a token
|
||||
func (s *Store) Inspect(t string) (*auth.Account, error) {
|
||||
tok, err := s.tokenProvider.Inspect(t)
|
||||
if err == token.ErrInvalidToken || err == token.ErrNotFound {
|
||||
return nil, auth.ErrInvalidToken
|
||||
} else if err != nil {
|
||||
return nil, errors.InternalServerError("go.micro.auth", "error reading store")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// decode the result
|
||||
// TODO: replace with json
|
||||
b := bytes.NewBuffer(records[0].Value)
|
||||
decoder := gob.NewDecoder(b)
|
||||
var sa auth.Account
|
||||
err = decoder.Decode(&sa)
|
||||
|
||||
// return the result
|
||||
return &sa, err
|
||||
return &auth.Account{
|
||||
ID: tok.Subject,
|
||||
Roles: tok.Roles,
|
||||
Metadata: tok.Metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String returns the implementation
|
||||
func (a *Auth) String() string {
|
||||
return "store"
|
||||
// Refresh an account using a secret
|
||||
func (s *Store) Refresh(secret string, opts ...auth.RefreshOption) (*auth.Token, error) {
|
||||
sec, err := s.secretProvider.Inspect(secret)
|
||||
if err == token.ErrInvalidToken || err == token.ErrNotFound {
|
||||
return nil, auth.ErrInvalidToken
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
options := auth.NewRefreshOptions(opts...)
|
||||
|
||||
return s.tokenProvider.Generate(sec.Subject,
|
||||
token.WithExpiry(options.TokenExpiry),
|
||||
token.WithMetadata(sec.Metadata),
|
||||
token.WithRoles(sec.Roles),
|
||||
)
|
||||
}
|
||||
|
287
auth/store/store_test.go
Normal file
287
auth/store/store_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/v2/auth"
|
||||
memStore "github.com/micro/go-micro/v2/store/memory"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
s := memStore.NewStore()
|
||||
a := NewAuth(auth.Store(s))
|
||||
|
||||
id := "test"
|
||||
roles := []string{"admin"}
|
||||
metadata := map[string]string{"foo": "bar"}
|
||||
|
||||
opts := []auth.GenerateOption{
|
||||
auth.WithRoles(roles),
|
||||
auth.WithMetadata(metadata),
|
||||
}
|
||||
|
||||
// generate the account
|
||||
acc, err := a.Generate(id, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate returned an error: %v, expected nil", err)
|
||||
}
|
||||
// validate the account attributes were set correctly
|
||||
if acc.ID != id {
|
||||
t.Errorf("Generate returned %v as the ID, expected %v", acc.ID, id)
|
||||
}
|
||||
if len(acc.Roles) != len(roles) {
|
||||
t.Errorf("Generate returned %v as the roles, expected %v", acc.Roles, roles)
|
||||
}
|
||||
if len(acc.Metadata) != len(metadata) {
|
||||
t.Errorf("Generate returned %v as the metadata, expected %v", acc.Metadata, metadata)
|
||||
}
|
||||
|
||||
// validate the secret is valid
|
||||
if _, err := a.Refresh(acc.Secret.Token); err != nil {
|
||||
t.Errorf("Generate returned an invalid secret, error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrant(t *testing.T) {
|
||||
s := memStore.NewStore()
|
||||
a := NewAuth(auth.Store(s))
|
||||
|
||||
res := &auth.Resource{Type: "service", Name: "Test", Endpoint: "Foo.Bar"}
|
||||
if err := a.Grant("users.*", res); err != nil {
|
||||
t.Fatalf("Grant returned an error: %v, expected nil", err)
|
||||
}
|
||||
|
||||
recs, err := s.List()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read from the store: %v", err)
|
||||
}
|
||||
if len(recs) != 1 {
|
||||
t.Errorf("Expected Grant to write 1 record, actually wrote %v", len(recs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
s := memStore.NewStore()
|
||||
a := NewAuth(auth.Store(s))
|
||||
|
||||
res := &auth.Resource{Type: "service", Name: "Test", Endpoint: "Foo.Bar"}
|
||||
if err := a.Grant("users.*", res); err != nil {
|
||||
t.Fatalf("Grant returned an error: %v, expected nil", err)
|
||||
}
|
||||
|
||||
recs, err := s.List()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read from the store: %v", err)
|
||||
}
|
||||
if len(recs) != 1 {
|
||||
t.Fatalf("Expected Grant to write 1 record, actually wrote %v", len(recs))
|
||||
}
|
||||
|
||||
if err := a.Revoke("users.*", res); err != nil {
|
||||
t.Fatalf("Revoke returned an error: %v, expected nil", err)
|
||||
}
|
||||
|
||||
recs, err = s.List()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read from the store: %v", err)
|
||||
}
|
||||
if len(recs) != 0 {
|
||||
t.Fatalf("Expected Revoke to delete 1 record, actually deleted %v", 1-len(recs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspect(t *testing.T) {
|
||||
a := NewAuth()
|
||||
|
||||
t.Run("Valid Token", func(t *testing.T) {
|
||||
id := "test"
|
||||
roles := []string{"admin"}
|
||||
metadata := map[string]string{"foo": "bar"}
|
||||
|
||||
opts := []auth.GenerateOption{
|
||||
auth.WithRoles(roles),
|
||||
auth.WithMetadata(metadata),
|
||||
}
|
||||
|
||||
// generate and inspect the token
|
||||
acc, err := a.Generate("test", opts...)
|
||||
if err != nil {
|
||||
log.Fatalf("Generate returned an error: %v, expected nil", err)
|
||||
}
|
||||
tok, err := a.Refresh(acc.Secret.Token)
|
||||
if err != nil {
|
||||
log.Fatalf("Refresh returned an error: %v, expected nil", err)
|
||||
}
|
||||
acc2, err := a.Inspect(tok.Token)
|
||||
if err != nil {
|
||||
log.Fatalf("Inspect returned an error: %v, expected nil", err)
|
||||
}
|
||||
|
||||
// validate the account attributes were retrieved correctly
|
||||
if acc2.ID != id {
|
||||
t.Errorf("Generate returned %v as the ID, expected %v", acc.ID, id)
|
||||
}
|
||||
if len(acc2.Roles) != len(roles) {
|
||||
t.Errorf("Generate returned %v as the roles, expected %v", acc.Roles, roles)
|
||||
}
|
||||
if len(acc2.Metadata) != len(metadata) {
|
||||
t.Errorf("Generate returned %v as the metadata, expected %v", acc.Metadata, metadata)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Invalid Token", func(t *testing.T) {
|
||||
_, err := a.Inspect("invalid token")
|
||||
if err != auth.ErrInvalidToken {
|
||||
t.Errorf("Inspect returned %v error, expected %v", err, auth.ErrInvalidToken)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
a := NewAuth()
|
||||
|
||||
t.Run("Valid Secret", func(t *testing.T) {
|
||||
roles := []string{"admin"}
|
||||
metadata := map[string]string{"foo": "bar"}
|
||||
|
||||
opts := []auth.GenerateOption{
|
||||
auth.WithRoles(roles),
|
||||
auth.WithMetadata(metadata),
|
||||
}
|
||||
|
||||
// generate the account
|
||||
acc, err := a.Generate("test", opts...)
|
||||
if err != nil {
|
||||
log.Fatalf("Generate returned an error: %v, expected nil", err)
|
||||
}
|
||||
|
||||
// refresh the token
|
||||
tok, err := a.Refresh(acc.Secret.Token)
|
||||
if err != nil {
|
||||
log.Fatalf("Refresh returned an error: %v, expected nil", err)
|
||||
}
|
||||
|
||||
// validate the account attributes were set correctly
|
||||
if acc.ID != tok.Subject {
|
||||
t.Errorf("Refresh returned %v as the ID, expected %v", acc.ID, tok.Subject)
|
||||
}
|
||||
if len(acc.Roles) != len(tok.Roles) {
|
||||
t.Errorf("Refresh returned %v as the roles, expected %v", acc.Roles, tok.Subject)
|
||||
}
|
||||
if len(acc.Metadata) != len(tok.Metadata) {
|
||||
t.Errorf("Refresh returned %v as the metadata, expected %v", acc.Metadata, tok.Metadata)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Invalid Secret", func(t *testing.T) {
|
||||
_, err := a.Refresh("invalid secret")
|
||||
if err != auth.ErrInvalidToken {
|
||||
t.Errorf("Inspect returned %v error, expected %v", err, auth.ErrInvalidToken)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
testRules := []struct {
|
||||
Role string
|
||||
Resource *auth.Resource
|
||||
}{
|
||||
{
|
||||
Role: "*",
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.PublicList"},
|
||||
},
|
||||
{
|
||||
Role: "user.*",
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.List"},
|
||||
},
|
||||
{
|
||||
Role: "user.developer",
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.Update"},
|
||||
},
|
||||
{
|
||||
Role: "admin",
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.Delete"},
|
||||
},
|
||||
{
|
||||
Role: "admin",
|
||||
Resource: &auth.Resource{Type: "service", Name: "*", Endpoint: "*"},
|
||||
},
|
||||
}
|
||||
|
||||
a := NewAuth()
|
||||
for _, r := range testRules {
|
||||
if err := a.Grant(r.Role, r.Resource); err != nil {
|
||||
t.Fatalf("Grant returned an error: %v, expected nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
testTable := []struct {
|
||||
Name string
|
||||
Roles []string
|
||||
Resource *auth.Resource
|
||||
Error error
|
||||
}{
|
||||
{
|
||||
Name: "An account with no roles accessing a public endpoint",
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.PublicList"},
|
||||
},
|
||||
{
|
||||
Name: "An account with no roles accessing a private endpoint",
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.Update"},
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "An account with the user role accessing a user* endpoint",
|
||||
Roles: []string{"user"},
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.List"},
|
||||
},
|
||||
{
|
||||
Name: "An account with the user role accessing a user.admin endpoint",
|
||||
Roles: []string{"user"},
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.Delete"},
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "An account with the developer role accessing a user.developer endpoint",
|
||||
Roles: []string{"user.developer"},
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.Update"},
|
||||
},
|
||||
{
|
||||
Name: "An account with the developer role accessing an admin endpoint",
|
||||
Roles: []string{"user.developer"},
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.Delete"},
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "An admin account accessing an admin endpoint",
|
||||
Roles: []string{"admin"},
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.apps", Endpoint: "Apps.Delete"},
|
||||
},
|
||||
{
|
||||
Name: "An admin account accessing a generic service endpoint",
|
||||
Roles: []string{"admin"},
|
||||
Resource: &auth.Resource{Type: "service", Name: "go.micro.foo", Endpoint: "Foo.Bar"},
|
||||
},
|
||||
{
|
||||
Name: "An admin account accessing an unauthorised endpoint",
|
||||
Roles: []string{"admin"},
|
||||
Resource: &auth.Resource{Type: "infra", Name: "go.micro.foo", Endpoint: "Foo.Bar"},
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
{
|
||||
Name: "A account with no roles accessing an unauthorised endpoint",
|
||||
Resource: &auth.Resource{Type: "infra", Name: "go.micro.foo", Endpoint: "Foo.Bar"},
|
||||
Error: auth.ErrForbidden,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testTable {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
acc := &auth.Account{Roles: tc.Roles}
|
||||
if err := a.Verify(acc, tc.Resource); err != tc.Error {
|
||||
t.Errorf("Verify returned %v error, expected %v", err, tc.Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user