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:
ben-toogood
2020-03-23 16:19:30 +00:00
committed by GitHub
parent 9826ddbd64
commit e0e77f3983
23 changed files with 1842 additions and 649 deletions

72
auth/store/rules.go Normal file
View 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
}

View File

@@ -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
View 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)
}
})
}
}