308 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			308 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package service
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/micro/go-micro/v2/auth"
 | |
| 	pb "github.com/micro/go-micro/v2/auth/service/proto"
 | |
| 	"github.com/micro/go-micro/v2/auth/token"
 | |
| 	"github.com/micro/go-micro/v2/auth/token/jwt"
 | |
| 	"github.com/micro/go-micro/v2/client"
 | |
| 	log "github.com/micro/go-micro/v2/logger"
 | |
| 	"github.com/micro/go-micro/v2/util/jitter"
 | |
| )
 | |
| 
 | |
| // NewAuth returns a new instance of the Auth service
 | |
| func NewAuth(opts ...auth.Option) auth.Auth {
 | |
| 	return &svc{options: auth.NewOptions(opts...)}
 | |
| }
 | |
| 
 | |
| // svc is the service implementation of the Auth interface
 | |
| type svc struct {
 | |
| 	options auth.Options
 | |
| 	auth    pb.AuthService
 | |
| 	rule    pb.RulesService
 | |
| 	jwt     token.Provider
 | |
| 
 | |
| 	rules []*pb.Rule
 | |
| 	sync.Mutex
 | |
| }
 | |
| 
 | |
| func (s *svc) String() string {
 | |
| 	return "service"
 | |
| }
 | |
| 
 | |
| func (s *svc) Init(opts ...auth.Option) {
 | |
| 	for _, o := range opts {
 | |
| 		o(&s.options)
 | |
| 	}
 | |
| 
 | |
| 	if s.options.Client == nil {
 | |
| 		s.options.Client = client.DefaultClient
 | |
| 	}
 | |
| 	s.auth = pb.NewAuthService("go.micro.auth", s.options.Client)
 | |
| 	s.rule = pb.NewRulesService("go.micro.auth", s.options.Client)
 | |
| 
 | |
| 	// if we have a JWT public key passed as an option,
 | |
| 	// we can decode tokens with the type "JWT" locally
 | |
| 	// and not have to make an RPC call
 | |
| 	if key := s.options.PublicKey; len(key) > 0 {
 | |
| 		s.jwt = jwt.NewTokenProvider(token.WithPublicKey(key))
 | |
| 	}
 | |
| 
 | |
| 	// load rules periodically from the auth service
 | |
| 	go func() {
 | |
| 		ruleTimer := time.NewTicker(time.Second * 30)
 | |
| 
 | |
| 		// load rules immediately on startup
 | |
| 		s.loadRules()
 | |
| 
 | |
| 		for {
 | |
| 			<-ruleTimer.C
 | |
| 
 | |
| 			// jitter for up to 5 seconds, this stops
 | |
| 			// all the services calling the auth service
 | |
| 			// at the exact same time
 | |
| 			time.Sleep(jitter.Do(time.Second * 5))
 | |
| 			s.loadRules()
 | |
| 		}
 | |
| 	}()
 | |
| }
 | |
| 
 | |
| func (s *svc) Options() auth.Options {
 | |
| 	s.Lock()
 | |
| 	defer s.Unlock()
 | |
| 	return s.options
 | |
| }
 | |
| 
 | |
| // Generate a new account
 | |
| func (s *svc) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
 | |
| 	options := auth.NewGenerateOptions(opts...)
 | |
| 
 | |
| 	rsp, err := s.auth.Generate(context.TODO(), &pb.GenerateRequest{
 | |
| 		Id:        id,
 | |
| 		Type:      options.Type,
 | |
| 		Secret:    options.Secret,
 | |
| 		Roles:     options.Roles,
 | |
| 		Metadata:  options.Metadata,
 | |
| 		Provider:  options.Provider,
 | |
| 		Namespace: options.Namespace,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return serializeAccount(rsp.Account), nil
 | |
| }
 | |
| 
 | |
| // Grant access to a resource
 | |
| func (s *svc) Grant(role string, res *auth.Resource) error {
 | |
| 	_, err := s.rule.Create(context.TODO(), &pb.CreateRequest{
 | |
| 		Role:   role,
 | |
| 		Access: pb.Access_GRANTED,
 | |
| 		Resource: &pb.Resource{
 | |
| 			Namespace: res.Namespace,
 | |
| 			Type:      res.Type,
 | |
| 			Name:      res.Name,
 | |
| 			Endpoint:  res.Endpoint,
 | |
| 		},
 | |
| 	})
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // Revoke access to a resource
 | |
| func (s *svc) Revoke(role string, res *auth.Resource) error {
 | |
| 	_, err := s.rule.Delete(context.TODO(), &pb.DeleteRequest{
 | |
| 		Role:   role,
 | |
| 		Access: pb.Access_GRANTED,
 | |
| 		Resource: &pb.Resource{
 | |
| 			Namespace: res.Namespace,
 | |
| 			Type:      res.Type,
 | |
| 			Name:      res.Name,
 | |
| 			Endpoint:  res.Endpoint,
 | |
| 		},
 | |
| 	})
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // Verify an account has access to a resource
 | |
| func (s *svc) Verify(acc *auth.Account, res *auth.Resource) error {
 | |
| 	// set the namespace on the resource
 | |
| 	if len(res.Namespace) == 0 {
 | |
| 		res.Namespace = s.Options().Namespace
 | |
| 	}
 | |
| 
 | |
| 	queries := [][]string{
 | |
| 		{res.Namespace, res.Type, res.Name, res.Endpoint}, // check for specific role, e.g. service.foo.ListFoo:admin (role is checked in accessForRule)
 | |
| 		{res.Namespace, res.Type, res.Name, "*"},          // check for wildcard endpoint, e.g. service.foo*
 | |
| 		{res.Namespace, res.Type, "*"},                    // check for wildcard name, e.g. service.*
 | |
| 		{res.Namespace, "*"},                              // check for wildcard type, e.g. *
 | |
| 		{"*"},                                             // check for wildcard namespace
 | |
| 	}
 | |
| 
 | |
| 	// endpoint is a url which can have wildcard excludes, e.g.
 | |
| 	// "/foo/*" will allow "/foo/bar"
 | |
| 	if comps := strings.Split(res.Endpoint, "/"); len(comps) > 1 {
 | |
| 		for i := 1; i < len(comps); i++ {
 | |
| 			wildcard := fmt.Sprintf("%v/*", strings.Join(comps[0:i], "/"))
 | |
| 			queries = append(queries, []string{res.Type, res.Name, wildcard})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// set a default account id / namespace to log
 | |
| 	logID := acc.ID
 | |
| 	if len(logID) == 0 {
 | |
| 		logID = "[no account]"
 | |
| 	}
 | |
| 	logNamespace := acc.Namespace
 | |
| 	if len(logNamespace) == 0 {
 | |
| 		logNamespace = "[no namespace]"
 | |
| 	}
 | |
| 
 | |
| 	for _, q := range queries {
 | |
| 		for _, rule := range s.listRules(q...) {
 | |
| 			switch accessForRule(rule, acc, res) {
 | |
| 			case pb.Access_UNKNOWN:
 | |
| 				continue // rule did not specify access, check the next rule
 | |
| 			case pb.Access_GRANTED:
 | |
| 				log.Tracef("%v:%v granted access to %v:%v:%v:%v by rule %v", logNamespace, logID, res.Namespace, res.Type, res.Name, res.Endpoint, rule.Id)
 | |
| 				return nil // rule grants the account access to the resource
 | |
| 			case pb.Access_DENIED:
 | |
| 				log.Tracef("%v:%v denied access to %v:%v:%v:%v by rule %v", logNamespace, logID, res.Namespace, res.Type, res.Name, res.Endpoint, rule.Id)
 | |
| 				return auth.ErrForbidden // rule denies access to the resource
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// no rules were found for the resource, default to denying access
 | |
| 	log.Tracef("%v:%v denied access to %v:%v:%v:%v by lack of rule (%v rules found for namespace)", logNamespace, logID, res.Namespace, res.Type, res.Name, res.Endpoint, len(s.listRules(res.Namespace)))
 | |
| 	return auth.ErrForbidden
 | |
| }
 | |
| 
 | |
| // Inspect a token
 | |
| func (s *svc) Inspect(token string) (*auth.Account, error) {
 | |
| 	// try to decode JWT locally and fall back to srv if an error occurs
 | |
| 	if len(strings.Split(token, ".")) == 3 && s.jwt != nil {
 | |
| 		return s.jwt.Inspect(token)
 | |
| 	}
 | |
| 
 | |
| 	// the token is not a JWT or we do not have the keys to decode it,
 | |
| 	// fall back to the auth service
 | |
| 	rsp, err := s.auth.Inspect(context.TODO(), &pb.InspectRequest{Token: token})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return serializeAccount(rsp.Account), nil
 | |
| }
 | |
| 
 | |
| // Token generation using an account ID and secret
 | |
| func (s *svc) Token(opts ...auth.TokenOption) (*auth.Token, error) {
 | |
| 	options := auth.NewTokenOptions(opts...)
 | |
| 
 | |
| 	rsp, err := s.auth.Token(context.Background(), &pb.TokenRequest{
 | |
| 		Id:           options.ID,
 | |
| 		Secret:       options.Secret,
 | |
| 		RefreshToken: options.RefreshToken,
 | |
| 		TokenExpiry:  int64(options.Expiry.Seconds()),
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return serializeToken(rsp.Token), nil
 | |
| }
 | |
| 
 | |
| var ruleJoinKey = ":"
 | |
| 
 | |
| // accessForRule returns a rule status, indicating if a rule permits access to a
 | |
| // resource for a given account
 | |
| func accessForRule(rule *pb.Rule, acc *auth.Account, res *auth.Resource) pb.Access {
 | |
| 	if rule.Role == "*" {
 | |
| 		return rule.Access
 | |
| 	}
 | |
| 
 | |
| 	for _, role := range acc.Roles {
 | |
| 		if rule.Role == role {
 | |
| 			return rule.Access
 | |
| 		}
 | |
| 
 | |
| 		// allow user.anything if role is user.*
 | |
| 		if strings.HasSuffix(rule.Role, ".*") && strings.HasPrefix(rule.Role, role+".") {
 | |
| 			return rule.Access
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return pb.Access_UNKNOWN
 | |
| }
 | |
| 
 | |
| // listRules gets all the rules from the store which match the filters.
 | |
| // filters are namespace, type, name and then endpoint.
 | |
| func (s *svc) listRules(filters ...string) []*pb.Rule {
 | |
| 	s.Lock()
 | |
| 	defer s.Unlock()
 | |
| 
 | |
| 	var rules []*pb.Rule
 | |
| 	for _, r := range s.rules {
 | |
| 		if len(filters) > 0 && r.Resource.Namespace != filters[0] {
 | |
| 			continue
 | |
| 		}
 | |
| 		if len(filters) > 1 && r.Resource.Type != filters[1] {
 | |
| 			continue
 | |
| 		}
 | |
| 		if len(filters) > 2 && r.Resource.Name != filters[2] {
 | |
| 			continue
 | |
| 		}
 | |
| 		if len(filters) > 3 && r.Resource.Endpoint != filters[3] {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		rules = append(rules, r)
 | |
| 	}
 | |
| 
 | |
| 	// sort rules by priority
 | |
| 	sort.Slice(rules, func(i, j int) bool {
 | |
| 		return rules[i].Priority < rules[j].Priority
 | |
| 	})
 | |
| 
 | |
| 	return rules
 | |
| }
 | |
| 
 | |
| // loadRules retrieves the rules from the auth service
 | |
| func (s *svc) loadRules() {
 | |
| 	rsp, err := s.rule.List(context.TODO(), &pb.ListRequest{})
 | |
| 	s.Lock()
 | |
| 	defer s.Unlock()
 | |
| 
 | |
| 	if err != nil {
 | |
| 		log.Errorf("Error listing rules: %v", err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	s.rules = rsp.Rules
 | |
| }
 | |
| 
 | |
| func serializeToken(t *pb.Token) *auth.Token {
 | |
| 	return &auth.Token{
 | |
| 		AccessToken:  t.AccessToken,
 | |
| 		RefreshToken: t.RefreshToken,
 | |
| 		Created:      time.Unix(t.Created, 0),
 | |
| 		Expiry:       time.Unix(t.Expiry, 0),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func serializeAccount(a *pb.Account) *auth.Account {
 | |
| 	return &auth.Account{
 | |
| 		ID:        a.Id,
 | |
| 		Roles:     a.Roles,
 | |
| 		Secret:    a.Secret,
 | |
| 		Metadata:  a.Metadata,
 | |
| 		Provider:  a.Provider,
 | |
| 		Namespace: a.Namespace,
 | |
| 	}
 | |
| }
 |