mirror of
				https://github.com/go-micro/go-micro.git
				synced 2025-10-30 23:27:41 +02:00 
			
		
		
		
	Refactor Namespace Resolver
This commit is contained in:
		| @@ -11,6 +11,12 @@ var ( | ||||
| 	ErrInvalidPath = errors.New("invalid path") | ||||
| ) | ||||
|  | ||||
| // NamespaceResolver resolves request to the namespace | ||||
| type NamespaceResolver interface { | ||||
| 	Resolve(r *http.Request) string | ||||
| 	String() string | ||||
| } | ||||
|  | ||||
| // Resolver resolves requests to endpoints | ||||
| type Resolver interface { | ||||
| 	Resolve(r *http.Request) (*Endpoint, error) | ||||
| @@ -27,6 +33,8 @@ type Endpoint struct { | ||||
| 	Method string | ||||
| 	// HTTP Path e.g /greeter. | ||||
| 	Path string | ||||
| 	// Namespace, g.g. go.micro | ||||
| 	Namespace string | ||||
| } | ||||
|  | ||||
| type Options struct { | ||||
|   | ||||
| @@ -3,44 +3,35 @@ package auth | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/micro/go-micro/v2/api/resolver" | ||||
| 	"github.com/micro/go-micro/v2/api/resolver/path" | ||||
| 	"github.com/micro/go-micro/v2/auth" | ||||
| 	"github.com/micro/go-micro/v2/logger" | ||||
| 	"golang.org/x/net/publicsuffix" | ||||
| ) | ||||
|  | ||||
| // CombinedAuthHandler wraps a server and authenticates requests | ||||
| func CombinedAuthHandler(prefix, namespace string, r resolver.Resolver, h http.Handler) http.Handler { | ||||
| 	if r == nil { | ||||
| 		r = path.NewResolver() | ||||
| 	} | ||||
|  | ||||
| func CombinedAuthHandler(r resolver.Resolver, nr resolver.NamespaceResolver, h http.Handler) http.Handler { | ||||
| 	return authHandler{ | ||||
| 		handler:       h, | ||||
| 		resolver:      r, | ||||
| 		auth:          auth.DefaultAuth, | ||||
| 		servicePrefix: prefix, | ||||
| 		namespace:     namespace, | ||||
| 		handler:    h, | ||||
| 		resolver:   r, | ||||
| 		nsResolver: nr, | ||||
| 		auth:       auth.DefaultAuth, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type authHandler struct { | ||||
| 	handler       http.Handler | ||||
| 	auth          auth.Auth | ||||
| 	resolver      resolver.Resolver | ||||
| 	namespace     string | ||||
| 	servicePrefix string | ||||
| 	handler    http.Handler | ||||
| 	auth       auth.Auth | ||||
| 	resolver   resolver.Resolver | ||||
| 	nsResolver resolver.NamespaceResolver | ||||
| } | ||||
|  | ||||
| func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	// Determine the namespace and set it in the header | ||||
| 	namespace := h.NamespaceFromRequest(req) | ||||
| 	namespace := h.nsResolver.Resolve(req) | ||||
| 	req.Header.Set(auth.NamespaceKey, namespace) | ||||
|  | ||||
| 	// Extract the token from the request | ||||
| @@ -63,14 +54,7 @@ func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	// account doesn't necesserially mean a forbidden request | ||||
| 	acc, err := h.auth.Inspect(token) | ||||
| 	if err != nil { | ||||
| 		acc = &auth.Account{Namespace: namespace} | ||||
| 	} | ||||
|  | ||||
| 	// Check the accounts namespace matches the namespace we're operating | ||||
| 	// within. If not forbid the request and log the occurance. | ||||
| 	if acc.Namespace != namespace { | ||||
| 		logger.Debugf("Cross namespace request warning: account %v (%v) requested access to %v in the %v namespace", acc.ID, acc.Namespace, req.URL.Path, namespace) | ||||
| 		// http.Error(w, "Forbidden namespace", 403) | ||||
| 		acc = &auth.Account{} | ||||
| 	} | ||||
|  | ||||
| 	// Determine the name of the service being requested | ||||
| @@ -90,9 +74,9 @@ func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	} | ||||
|  | ||||
| 	// construct the resource name, e.g. home => go.micro.web.home | ||||
| 	resName := h.servicePrefix | ||||
| 	resName := namespace | ||||
| 	if len(endpoint.Name) > 0 { | ||||
| 		resName = resName + "." + endpoint.Name | ||||
| 		resName = namespace + "." + endpoint.Name | ||||
| 	} | ||||
|  | ||||
| 	// determine the resource path. there is an inconsistency in how resolvers | ||||
| @@ -127,59 +111,7 @@ func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	} | ||||
|  | ||||
| 	// Redirect to the login path | ||||
| 	params := url.Values{"redirect_to": {req.URL.Path}} | ||||
| 	params := url.Values{"redirect_to": {req.URL.String()}} | ||||
| 	loginWithRedirect := fmt.Sprintf("%v?%v", loginURL, params.Encode()) | ||||
| 	http.Redirect(w, req, loginWithRedirect, http.StatusTemporaryRedirect) | ||||
| } | ||||
|  | ||||
| func (h authHandler) NamespaceFromRequest(req *http.Request) string { | ||||
| 	// check to see what the provided namespace is, we only do | ||||
| 	// domain mapping if the namespace is set to 'domain' | ||||
| 	if h.namespace != "domain" { | ||||
| 		return h.namespace | ||||
| 	} | ||||
|  | ||||
| 	// determine the host, e.g. dev.micro.mu:8080 | ||||
| 	host := req.URL.Hostname() | ||||
| 	if len(host) == 0 { | ||||
| 		if h, _, err := net.SplitHostPort(req.Host); err == nil { | ||||
| 			host = h // host does contain a port | ||||
| 		} else if strings.Contains(err.Error(), "missing port in address") { | ||||
| 			host = req.Host // host does not contain a port | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// check for an ip address | ||||
| 	if net.ParseIP(host) != nil { | ||||
| 		return auth.DefaultNamespace | ||||
| 	} | ||||
|  | ||||
| 	// check for dev enviroment | ||||
| 	if host == "localhost" || host == "127.0.0.1" { | ||||
| 		return auth.DefaultNamespace | ||||
| 	} | ||||
|  | ||||
| 	// extract the top level domain plus one (e.g. 'myapp.com') | ||||
| 	domain, err := publicsuffix.EffectiveTLDPlusOne(host) | ||||
| 	if err != nil { | ||||
| 		logger.Debugf("Unable to extract domain from %v", host) | ||||
| 		return auth.DefaultNamespace | ||||
| 	} | ||||
|  | ||||
| 	// check to see if the domain matches the host of micro.mu, in | ||||
| 	// these cases we return the default namespace | ||||
| 	if domain == host || domain == "micro.mu" { | ||||
| 		return auth.DefaultNamespace | ||||
| 	} | ||||
|  | ||||
| 	// remove the domain from the host, leaving the subdomain | ||||
| 	subdomain := strings.TrimSuffix(host, "."+domain) | ||||
|  | ||||
| 	// return the reversed subdomain as the namespace | ||||
| 	comps := strings.Split(subdomain, ".") | ||||
| 	for i := len(comps)/2 - 1; i >= 0; i-- { | ||||
| 		opp := len(comps) - 1 - i | ||||
| 		comps[i], comps[opp] = comps[opp], comps[i] | ||||
| 	} | ||||
| 	return strings.Join(comps, ".") | ||||
| } | ||||
|   | ||||
| @@ -1,38 +0,0 @@ | ||||
| package auth | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/micro/go-micro/v2/auth" | ||||
| ) | ||||
|  | ||||
| func TestNamespaceFromRequest(t *testing.T) { | ||||
| 	tt := []struct { | ||||
| 		Host      string | ||||
| 		Namespace string | ||||
| 	}{ | ||||
| 		{Host: "micro.mu", Namespace: auth.DefaultNamespace}, | ||||
| 		{Host: "micro.com.au", Namespace: auth.DefaultNamespace}, | ||||
| 		{Host: "web.micro.mu", Namespace: auth.DefaultNamespace}, | ||||
| 		{Host: "api.micro.mu", Namespace: auth.DefaultNamespace}, | ||||
| 		{Host: "myapp.com", Namespace: auth.DefaultNamespace}, | ||||
| 		{Host: "staging.myapp.com", Namespace: "staging"}, | ||||
| 		{Host: "staging.myapp.m3o.app", Namespace: "myapp.staging"}, | ||||
| 		{Host: "127.0.0.1", Namespace: auth.DefaultNamespace}, | ||||
| 		{Host: "localhost", Namespace: auth.DefaultNamespace}, | ||||
| 		{Host: "81.151.101.146", Namespace: auth.DefaultNamespace}, | ||||
| 	} | ||||
|  | ||||
| 	h := &authHandler{namespace: "domain"} | ||||
|  | ||||
| 	for _, tc := range tt { | ||||
| 		t.Run(tc.Host, func(t *testing.T) { | ||||
| 			ns := h.NamespaceFromRequest(&http.Request{Host: tc.Host, URL: &url.URL{Host: tc.Host}}) | ||||
| 			if ns != tc.Namespace { | ||||
| 				t.Errorf("Expected namespace %v for host %v, actually got %v", tc.Namespace, tc.Host, ns) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -53,7 +53,7 @@ func (s *httpServer) Init(opts ...server.Option) error { | ||||
|  | ||||
| func (s *httpServer) Handle(path string, handler http.Handler) { | ||||
| 	h := handlers.CombinedLoggingHandler(os.Stdout, handler) | ||||
| 	h = auth.CombinedAuthHandler(s.opts.ServicePrefix, s.opts.Namespace, s.opts.Resolver, handler) | ||||
| 	h = auth.CombinedAuthHandler(s.opts.Resolver, s.opts.NamespaceResolver, handler) | ||||
|  | ||||
| 	if s.opts.EnableCORS { | ||||
| 		h = cors.CombinedCORSHandler(h) | ||||
|   | ||||
| @@ -10,15 +10,14 @@ import ( | ||||
| type Option func(o *Options) | ||||
|  | ||||
| type Options struct { | ||||
| 	EnableACME    bool | ||||
| 	EnableCORS    bool | ||||
| 	ACMEProvider  acme.Provider | ||||
| 	EnableTLS     bool | ||||
| 	ACMEHosts     []string | ||||
| 	TLSConfig     *tls.Config | ||||
| 	Resolver      resolver.Resolver | ||||
| 	Namespace     string | ||||
| 	ServicePrefix string | ||||
| 	EnableACME        bool | ||||
| 	EnableCORS        bool | ||||
| 	ACMEProvider      acme.Provider | ||||
| 	EnableTLS         bool | ||||
| 	ACMEHosts         []string | ||||
| 	TLSConfig         *tls.Config | ||||
| 	Resolver          resolver.Resolver | ||||
| 	NamespaceResolver resolver.NamespaceResolver | ||||
| } | ||||
|  | ||||
| func EnableCORS(b bool) Option { | ||||
| @@ -57,20 +56,14 @@ func TLSConfig(t *tls.Config) Option { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ServicePrefix(n string) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.ServicePrefix = n | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func Namespace(n string) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.Namespace = n | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func Resolver(r resolver.Resolver) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.Resolver = r | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func NamespaceResolver(r resolver.NamespaceResolver) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.NamespaceResolver = r | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -158,6 +158,11 @@ func (s *svc) Revoke(role string, res *auth.Resource) error { | ||||
|  | ||||
| // 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* | ||||
| @@ -205,16 +210,15 @@ func (s *svc) Verify(acc *auth.Account, res *auth.Resource) error { | ||||
| 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 { | ||||
| 		if acc, err := s.jwt.Inspect(token); err == nil { | ||||
| 			return acc, 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 | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @@ -399,6 +399,7 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= | ||||
| go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= | ||||
| go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= | ||||
| go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= | ||||
| go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= | ||||
| go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= | ||||
| go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= | ||||
| go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= | ||||
| @@ -432,6 +433,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl | ||||
| golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||
| golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||
| golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||
| golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= | ||||
| golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= | ||||
| golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= | ||||
| golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= | ||||
| @@ -529,6 +531,7 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn | ||||
| golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= | ||||
| golang.org/x/tools v0.0.0-20191216173652-a0e659d51361 h1:RIIXAeV6GvDBuADKumTODatUqANFZ+5BPMnzsy4hulY= | ||||
| golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| @@ -600,6 +603,7 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh | ||||
| honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= | ||||
| honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= | ||||
| k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= | ||||
| rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import ( | ||||
| 	"github.com/micro/go-micro/v2/debug/stats" | ||||
| 	"github.com/micro/go-micro/v2/debug/trace" | ||||
| 	"github.com/micro/go-micro/v2/errors" | ||||
| 	"github.com/micro/go-micro/v2/logger" | ||||
| 	"github.com/micro/go-micro/v2/metadata" | ||||
| 	"github.com/micro/go-micro/v2/server" | ||||
| ) | ||||
| @@ -155,11 +154,6 @@ func AuthHandler(fn func() auth.Auth) server.HandlerWrapper { | ||||
| 				return h(ctx, req, rsp) | ||||
| 			} | ||||
|  | ||||
| 			// Check for auth service endpoints which should be excluded from auth | ||||
| 			if strings.HasPrefix(req.Endpoint(), "Auth.") { | ||||
| 				return h(ctx, req, rsp) | ||||
| 			} | ||||
|  | ||||
| 			// Extract the token if present. Note: if noop is being used | ||||
| 			// then the token can be blank without erroring | ||||
| 			var token string | ||||
| @@ -172,33 +166,17 @@ func AuthHandler(fn func() auth.Auth) server.HandlerWrapper { | ||||
| 				token = header[len(auth.BearerScheme):] | ||||
| 			} | ||||
|  | ||||
| 			// Get the namespace for the request | ||||
| 			namespace, ok := metadata.Get(ctx, auth.NamespaceKey) | ||||
| 			if !ok { | ||||
| 				logger.Debugf("Missing request namespace") | ||||
| 				namespace = auth.DefaultNamespace | ||||
| 			} | ||||
|  | ||||
| 			// Inspect the token and get the account | ||||
| 			account, err := a.Inspect(token) | ||||
| 			if err != nil { | ||||
| 				account = &auth.Account{Namespace: namespace} | ||||
| 			} | ||||
|  | ||||
| 			// Check the accounts namespace matches the namespace we're operating | ||||
| 			// within. If not forbid the request and log the occurance. | ||||
| 			if account.Namespace != namespace { | ||||
| 				logger.Debugf("Cross namespace request forbidden: account %v (%v) requested access to %v %v in the %v namespace", | ||||
| 					account.ID, account.Namespace, req.Service(), req.Endpoint(), namespace) | ||||
| 				// return errors.Forbidden(req.Service(), "cross namespace request") | ||||
| 				account = &auth.Account{} | ||||
| 			} | ||||
|  | ||||
| 			// construct the resource | ||||
| 			res := &auth.Resource{ | ||||
| 				Type:      "service", | ||||
| 				Name:      req.Service(), | ||||
| 				Endpoint:  req.Endpoint(), | ||||
| 				Namespace: namespace, | ||||
| 				Type:     "service", | ||||
| 				Name:     req.Service(), | ||||
| 				Endpoint: req.Endpoint(), | ||||
| 			} | ||||
|  | ||||
| 			// Verify the caller has access to the resource | ||||
|   | ||||
		Reference in New Issue
	
	Block a user