328 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			328 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package service
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"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 {
 | |
| 	svc := new(svc)
 | |
| 	svc.Init(opts...)
 | |
| 	return svc
 | |
| }
 | |
| 
 | |
| // 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)
 | |
| 	}
 | |
| 
 | |
| 	dc := client.DefaultClient
 | |
| 	s.auth = pb.NewAuthService("go.micro.auth", dc)
 | |
| 	s.rule = pb.NewRulesService("go.micro.auth", dc)
 | |
| 
 | |
| 	// 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
 | |
| 	ruleTimer := time.NewTicker(time.Second * 30)
 | |
| 	go func() {
 | |
| 		// 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()
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	// we have client credentials and must load a new token
 | |
| 	// periodically
 | |
| 	if len(s.options.ID) > 0 || len(s.options.Secret) > 0 {
 | |
| 		tokenTimer := time.NewTicker(time.Minute)
 | |
| 
 | |
| 		go func() {
 | |
| 			s.loadToken()
 | |
| 
 | |
| 			for {
 | |
| 				<-tokenTimer.C
 | |
| 
 | |
| 				// Do not get a new token if the current one has more than three
 | |
| 				// minutes remaining. We do 3 minutes to allow multiple retires in
 | |
| 				// the case one request fails
 | |
| 				t := s.Options().Token
 | |
| 				if t != nil && t.Expiry.Unix() > time.Now().Add(time.Minute*3).Unix() {
 | |
| 					continue
 | |
| 				}
 | |
| 
 | |
| 				// 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.loadToken()
 | |
| 			}
 | |
| 		}()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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,
 | |
| 		Roles:     options.Roles,
 | |
| 		Metadata:  options.Metadata,
 | |
| 		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{
 | |
| 			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{
 | |
| 			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 {
 | |
| 	queries := [][]string{
 | |
| 		{res.Type, res.Name, res.Endpoint}, // check for specific role, e.g. service.foo.ListFoo:admin (role is checked in accessForRule)
 | |
| 		{res.Type, res.Name, "*"},          // check for wildcard endpoint, e.g. service.foo*
 | |
| 		{res.Type, "*"},                    // check for wildcard name, e.g. service.*
 | |
| 		{"*"},                              // check for wildcard type, e.g. *
 | |
| 	}
 | |
| 
 | |
| 	// 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})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	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.Infof("%v granted access to %v:%v:%v by rule %v", acc.ID, res.Type, res.Name, res.Endpoint, rule.Id)
 | |
| 				return nil // rule grants the account access to the resource
 | |
| 			case pb.Access_DENIED:
 | |
| 				log.Infof("%v denied access to %v:%v:%v by rule %v", acc.ID, 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.Infof("%v denied access to %v:%v:%v by lack of rule (%v rules found)", acc.ID, res.Type, res.Name, res.Endpoint, len(s.rules))
 | |
| 	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, TODO: find a better way of determining if the token
 | |
| 	// is a JWT, possibly update the interface to take an auth.Token
 | |
| 	// and not just the string
 | |
| 	if len(strings.Split(token, ".")) == 3 && s.jwt != nil {
 | |
| 		if tok, err := s.jwt.Inspect(token); err == nil {
 | |
| 			return &auth.Account{
 | |
| 				ID:       tok.Subject,
 | |
| 				Roles:    tok.Roles,
 | |
| 				Metadata: tok.Metadata,
 | |
| 			}, nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	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(id, secret string, opts ...auth.TokenOption) (*auth.Token, error) {
 | |
| 	options := auth.NewTokenOptions(opts...)
 | |
| 
 | |
| 	rsp, err := s.auth.Token(context.Background(), &pb.TokenRequest{
 | |
| 		Id:          id,
 | |
| 		Secret:      secret,
 | |
| 		TokenExpiry: int64(options.TokenExpiry.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 have an id
 | |
| // prefix matching the filters
 | |
| func (s *svc) listRules(filters ...string) []*pb.Rule {
 | |
| 	s.Lock()
 | |
| 	defer s.Unlock()
 | |
| 
 | |
| 	prefix := strings.Join(filters, ruleJoinKey)
 | |
| 
 | |
| 	var rules []*pb.Rule
 | |
| 	for _, r := range s.rules {
 | |
| 		if strings.HasPrefix(r.Id, prefix) {
 | |
| 			rules = append(rules, r)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	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
 | |
| }
 | |
| 
 | |
| // loadToken generates a new token for the service to use when making calls
 | |
| func (s *svc) loadToken() {
 | |
| 	rsp, err := s.auth.Token(context.TODO(), &pb.TokenRequest{
 | |
| 		Id:          s.Options().ID,
 | |
| 		Secret:      s.Options().Secret,
 | |
| 		TokenExpiry: int64((time.Minute * 15).Seconds()),
 | |
| 	})
 | |
| 	s.Lock()
 | |
| 	defer s.Unlock()
 | |
| 
 | |
| 	if err != nil {
 | |
| 		log.Errorf("Error generating token: %v", err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	s.options.Token = serializeToken(rsp.Token)
 | |
| }
 | |
| 
 | |
| func serializeToken(t *pb.Token) *auth.Token {
 | |
| 	return &auth.Token{
 | |
| 		Token:    t.Token,
 | |
| 		Type:     t.Type,
 | |
| 		Created:  time.Unix(t.Created, 0),
 | |
| 		Expiry:   time.Unix(t.Expiry, 0),
 | |
| 		Subject:  t.Subject,
 | |
| 		Roles:    t.Roles,
 | |
| 		Metadata: t.Metadata,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func serializeAccount(a *pb.Account) *auth.Account {
 | |
| 	return &auth.Account{
 | |
| 		ID:        a.Id,
 | |
| 		Roles:     a.Roles,
 | |
| 		Metadata:  a.Metadata,
 | |
| 		Namespace: a.Namespace,
 | |
| 		Secret:    a.Secret,
 | |
| 	}
 | |
| }
 |