Merge pull request #1496 from micro/namespace
Configurable Namespace & Public Suffix Domain Resolution
This commit is contained in:
commit
c5d085cff8
@ -1,181 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CombinedAuthHandler wraps a server and authenticates requests
|
|
||||||
func CombinedAuthHandler(namespace string, r resolver.Resolver, h http.Handler) http.Handler {
|
|
||||||
if r == nil {
|
|
||||||
r = path.NewResolver()
|
|
||||||
}
|
|
||||||
if len(namespace) == 0 {
|
|
||||||
namespace = "go.micro"
|
|
||||||
}
|
|
||||||
|
|
||||||
return authHandler{
|
|
||||||
handler: h,
|
|
||||||
resolver: r,
|
|
||||||
auth: auth.DefaultAuth,
|
|
||||||
namespace: namespace,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type authHandler struct {
|
|
||||||
handler http.Handler
|
|
||||||
auth auth.Auth
|
|
||||||
resolver resolver.Resolver
|
|
||||||
namespace string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
||||||
// Determine the namespace
|
|
||||||
namespace, err := namespaceFromRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
namespace = auth.DefaultNamespace
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the namespace in the header
|
|
||||||
req.Header.Set(auth.NamespaceKey, namespace)
|
|
||||||
|
|
||||||
// Extract the token from the request
|
|
||||||
var token string
|
|
||||||
if header := req.Header.Get("Authorization"); len(header) > 0 {
|
|
||||||
// Extract the auth token from the request
|
|
||||||
if strings.HasPrefix(header, auth.BearerScheme) {
|
|
||||||
token = header[len(auth.BearerScheme):]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Get the token out the cookies if not provided in headers
|
|
||||||
if c, err := req.Cookie("micro-token"); err == nil && c != nil {
|
|
||||||
token = strings.TrimPrefix(c.Value, auth.TokenCookieName+"=")
|
|
||||||
req.Header.Set("Authorization", auth.BearerScheme+token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the account using the token, fallback to a blank account
|
|
||||||
// since some endpoints can be unauthenticated, so the lack of an
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the name of the service being requested
|
|
||||||
endpoint, err := h.resolver.Resolve(req)
|
|
||||||
if err == resolver.ErrInvalidPath || err == resolver.ErrNotFound {
|
|
||||||
// a file not served by the resolver has been requested (e.g. favicon.ico)
|
|
||||||
endpoint = &resolver.Endpoint{Path: req.URL.Path}
|
|
||||||
} else if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
http.Error(w, err.Error(), 500)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// set the endpoint in the context so it can be used to resolve
|
|
||||||
// the request later
|
|
||||||
ctx := context.WithValue(req.Context(), resolver.Endpoint{}, endpoint)
|
|
||||||
*req = *req.Clone(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct the resource name, e.g. home => go.micro.web.home
|
|
||||||
resName := h.namespace
|
|
||||||
if len(endpoint.Name) > 0 {
|
|
||||||
resName = resName + "." + endpoint.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine the resource path. there is an inconsistency in how resolvers
|
|
||||||
// use method, some use it as Users.ReadUser (the rpc method), and others
|
|
||||||
// use it as the HTTP method, e.g GET. TODO: Refactor this to make it consistent.
|
|
||||||
resEndpoint := endpoint.Path
|
|
||||||
if len(endpoint.Path) == 0 {
|
|
||||||
resEndpoint = endpoint.Method
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the verification check to see if the account has access to
|
|
||||||
// the resource they're requesting
|
|
||||||
res := &auth.Resource{Type: "service", Name: resName, Endpoint: resEndpoint, Namespace: namespace}
|
|
||||||
if err := h.auth.Verify(acc, res); err == nil {
|
|
||||||
// The account has the necessary permissions to access the resource
|
|
||||||
h.handler.ServeHTTP(w, req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// The account is set, but they don't have enough permissions, hence
|
|
||||||
// we return a forbidden error.
|
|
||||||
if len(acc.ID) > 0 {
|
|
||||||
http.Error(w, "Forbidden request", 403)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no auth login url set, 401
|
|
||||||
loginURL := h.auth.Options().LoginURL
|
|
||||||
if loginURL == "" {
|
|
||||||
http.Error(w, "unauthorized request", 401)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to the login path
|
|
||||||
params := url.Values{"redirect_to": {req.URL.Path}}
|
|
||||||
loginWithRedirect := fmt.Sprintf("%v?%v", loginURL, params.Encode())
|
|
||||||
http.Redirect(w, req, loginWithRedirect, http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func namespaceFromRequest(req *http.Request) (string, error) {
|
|
||||||
// needed to tmp debug host in prod. will be removed.
|
|
||||||
logger.Infof("Host is '%v'; URL Host is '%v'; URL Hostname is '%v'", req.Host, req.URL.Host, req.URL.Hostname())
|
|
||||||
|
|
||||||
// determine the host, e.g. dev.micro.mu:8080
|
|
||||||
host := req.URL.Hostname()
|
|
||||||
if len(host) == 0 {
|
|
||||||
// fallback to req.Host
|
|
||||||
var err error
|
|
||||||
host, _, err = net.SplitHostPort(req.Host)
|
|
||||||
if err != nil && strings.Contains(err.Error(), "missing port in address") {
|
|
||||||
host = req.Host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for an ip address
|
|
||||||
if net.ParseIP(host) != nil {
|
|
||||||
return auth.DefaultNamespace, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for dev enviroment
|
|
||||||
if host == "localhost" || host == "127.0.0.1" {
|
|
||||||
return auth.DefaultNamespace, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this logic needs to be replaced with usage of publicsuffix
|
|
||||||
// if host is not a subdomain, deturn default namespace
|
|
||||||
comps := strings.Split(host, ".")
|
|
||||||
if len(comps) != 3 {
|
|
||||||
return auth.DefaultNamespace, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for the micro.mu domain
|
|
||||||
domain := fmt.Sprintf("%v.%v", comps[1], comps[2])
|
|
||||||
if domain == "micro.mu" {
|
|
||||||
return auth.DefaultNamespace, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the subdomain as the host
|
|
||||||
return comps[0], nil
|
|
||||||
}
|
|
@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/micro/go-micro/v2/api/server"
|
"github.com/micro/go-micro/v2/api/server"
|
||||||
"github.com/micro/go-micro/v2/api/server/auth"
|
|
||||||
"github.com/micro/go-micro/v2/api/server/cors"
|
"github.com/micro/go-micro/v2/api/server/cors"
|
||||||
"github.com/micro/go-micro/v2/logger"
|
"github.com/micro/go-micro/v2/logger"
|
||||||
)
|
)
|
||||||
@ -53,7 +52,11 @@ func (s *httpServer) Init(opts ...server.Option) error {
|
|||||||
|
|
||||||
func (s *httpServer) Handle(path string, handler http.Handler) {
|
func (s *httpServer) Handle(path string, handler http.Handler) {
|
||||||
h := handlers.CombinedLoggingHandler(os.Stdout, handler)
|
h := handlers.CombinedLoggingHandler(os.Stdout, handler)
|
||||||
h = auth.CombinedAuthHandler(s.opts.Namespace, s.opts.Resolver, handler)
|
|
||||||
|
// apply the wrappers, e.g. auth
|
||||||
|
for _, wrapper := range s.opts.Wrappers {
|
||||||
|
h = wrapper(h)
|
||||||
|
}
|
||||||
|
|
||||||
if s.opts.EnableCORS {
|
if s.opts.EnableCORS {
|
||||||
h = cors.CombinedCORSHandler(h)
|
h = cors.CombinedCORSHandler(h)
|
||||||
|
@ -2,6 +2,7 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/micro/go-micro/v2/api/resolver"
|
"github.com/micro/go-micro/v2/api/resolver"
|
||||||
"github.com/micro/go-micro/v2/api/server/acme"
|
"github.com/micro/go-micro/v2/api/server/acme"
|
||||||
@ -16,8 +17,16 @@ type Options struct {
|
|||||||
EnableTLS bool
|
EnableTLS bool
|
||||||
ACMEHosts []string
|
ACMEHosts []string
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
Namespace string
|
|
||||||
Resolver resolver.Resolver
|
Resolver resolver.Resolver
|
||||||
|
Wrappers []Wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
type Wrapper func(h http.Handler) http.Handler
|
||||||
|
|
||||||
|
func WrapHandler(w Wrapper) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Wrappers = append(o.Wrappers, w)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnableCORS(b bool) Option {
|
func EnableCORS(b bool) Option {
|
||||||
@ -56,12 +65,6 @@ func TLSConfig(t *tls.Config) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Namespace(n string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.Namespace = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Resolver(r resolver.Resolver) Option {
|
func Resolver(r resolver.Resolver) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Resolver = r
|
o.Resolver = r
|
||||||
|
@ -89,7 +89,7 @@ type Token struct {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// DefaultNamespace used for auth
|
// DefaultNamespace used for auth
|
||||||
DefaultNamespace = "micro"
|
DefaultNamespace = "go.micro"
|
||||||
// NamespaceKey is the key used when storing the namespace in metadata
|
// NamespaceKey is the key used when storing the namespace in metadata
|
||||||
NamespaceKey = "Micro-Namespace"
|
NamespaceKey = "Micro-Namespace"
|
||||||
// MetadataKey is the key used when storing the account in metadata
|
// MetadataKey is the key used when storing the account in metadata
|
||||||
|
@ -8,6 +8,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
// Namespace the service belongs to
|
||||||
|
Namespace string
|
||||||
// ID is the services auth ID
|
// ID is the services auth ID
|
||||||
ID string
|
ID string
|
||||||
// Secret is used to authenticate the service
|
// Secret is used to authenticate the service
|
||||||
@ -28,6 +30,13 @@ type Options struct {
|
|||||||
|
|
||||||
type Option func(o *Options)
|
type Option func(o *Options)
|
||||||
|
|
||||||
|
// Namespace the service belongs to
|
||||||
|
func Namespace(n string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Namespace = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store to back auth
|
// Store to back auth
|
||||||
func Store(s store.Store) Option {
|
func Store(s store.Store) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
|
@ -158,6 +158,11 @@ func (s *svc) Revoke(role string, res *auth.Resource) error {
|
|||||||
|
|
||||||
// Verify an account has access to a resource
|
// Verify an account has access to a resource
|
||||||
func (s *svc) Verify(acc *auth.Account, res *auth.Resource) error {
|
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{
|
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, 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, 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) {
|
func (s *svc) Inspect(token string) (*auth.Account, error) {
|
||||||
// try to decode JWT locally and fall back to srv if an error occurs
|
// try to decode JWT locally and fall back to srv if an error occurs
|
||||||
if len(strings.Split(token, ".")) == 3 && s.jwt != nil {
|
if len(strings.Split(token, ".")) == 3 && s.jwt != nil {
|
||||||
if acc, err := s.jwt.Inspect(token); err == nil {
|
return s.jwt.Inspect(token)
|
||||||
return acc, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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})
|
rsp, err := s.auth.Inspect(context.TODO(), &pb.InspectRequest{Token: token})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return serializeAccount(rsp.Account), nil
|
return serializeAccount(rsp.Account), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
go.sum
1
go.sum
@ -546,6 +546,7 @@ golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
|
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
|
||||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/micro/go-micro/v2/debug/stats"
|
"github.com/micro/go-micro/v2/debug/stats"
|
||||||
"github.com/micro/go-micro/v2/debug/trace"
|
"github.com/micro/go-micro/v2/debug/trace"
|
||||||
"github.com/micro/go-micro/v2/errors"
|
"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/metadata"
|
||||||
"github.com/micro/go-micro/v2/server"
|
"github.com/micro/go-micro/v2/server"
|
||||||
)
|
)
|
||||||
@ -145,11 +144,6 @@ func AuthHandler(fn func() auth.Auth) server.HandlerWrapper {
|
|||||||
return h(ctx, req, rsp)
|
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
|
// Extract the token if present. Note: if noop is being used
|
||||||
// then the token can be blank without erroring
|
// then the token can be blank without erroring
|
||||||
var token string
|
var token string
|
||||||
@ -162,25 +156,10 @@ func AuthHandler(fn func() auth.Auth) server.HandlerWrapper {
|
|||||||
token = header[len(auth.BearerScheme):]
|
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
|
// Inspect the token and get the account
|
||||||
account, err := a.Inspect(token)
|
account, err := a.Inspect(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
account = &auth.Account{Namespace: namespace}
|
account = &auth.Account{}
|
||||||
}
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// construct the resource
|
// construct the resource
|
||||||
@ -188,7 +167,6 @@ func AuthHandler(fn func() auth.Auth) server.HandlerWrapper {
|
|||||||
Type: "service",
|
Type: "service",
|
||||||
Name: req.Service(),
|
Name: req.Service(),
|
||||||
Endpoint: req.Endpoint(),
|
Endpoint: req.Endpoint(),
|
||||||
Namespace: namespace,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the caller has access to the resource
|
// Verify the caller has access to the resource
|
||||||
|
Loading…
Reference in New Issue
Block a user