From dccc7cd2ffe3fc6d3fb4a9864b33494955ba3ef6 Mon Sep 17 00:00:00 2001 From: Bobby DeSimone Date: Mon, 20 Jan 2020 18:25:34 -0800 Subject: [PATCH] cache : add cache service (#457) Signed-off-by: Bobby DeSimone --- .gitignore | 1 + .golangci.yml | 1 - authenticate/authenticate.go | 31 +- authenticate/authenticate_test.go | 4 + authenticate/handlers.go | 12 +- authorize/grpc.go | 12 +- authorize/grpc_test.go | 4 +- cache/cache.go | 68 ++++ cache/grpc.go | 34 ++ cmd/pomerium/main.go | 60 +++- cmd/pomerium/main_test.go | 45 +++ config/helpers.go | 18 +- config/helpers_test.go | 27 ++ config/options.go | 33 +- config/options_test.go | 5 +- go.mod | 8 +- go.sum | 97 +++++- .../grpc}/authorize/authorize.pb.go | 0 .../grpc}/authorize/authorize.proto | 0 .../authorize/client}/authorize_client.go | 45 +-- .../authorize/client/authorize_client_test.go | 78 +++++ .../grpc/authorize/client/mock.go | 4 +- .../client}/mock_authorize/mock_authorize.go | 2 +- internal/grpc/cache/cache.pb.go | 329 ++++++++++++++++++ internal/grpc/cache/cache.proto | 20 ++ internal/grpc/cache/client/cache_client.go | 57 +++ .../clients.go => internal/grpc/client.go | 42 +-- internal/grpc/client_test.go | 78 +++++ internal/{grpcutil/grpc.go => grpc/server.go} | 30 +- internal/grpc/server_test.go | 84 +++++ .../grpc}/testdata/example.crt | 0 internal/grpcutil/middleware.go | 54 --- internal/kv/autocache/autocache.go | 235 +++++++++++++ internal/kv/bolt/bolt.go | 109 ++++++ internal/kv/redis/redis.go | 89 +++++ internal/kv/store.go | 10 + internal/sessions/cache/cache_store.go | 122 +++---- internal/sessions/cache/cache_store_test.go | 220 +++++++----- internal/urlutil/query_params.go | 2 + proxy/clients/authorize_client_test.go | 122 ------- proxy/clients/clients_test.go | 38 -- proxy/forward_auth_test.go | 46 +-- proxy/handlers_test.go | 54 +-- proxy/middleware.go | 17 +- proxy/middleware_test.go | 46 +-- proxy/proxy.go | 31 +- 46 files changed, 1837 insertions(+), 587 deletions(-) create mode 100644 cache/cache.go create mode 100644 cache/grpc.go rename {proto => internal/grpc}/authorize/authorize.pb.go (100%) rename {proto => internal/grpc}/authorize/authorize.proto (100%) rename {proxy/clients => internal/grpc/authorize/client}/authorize_client.go (52%) create mode 100644 internal/grpc/authorize/client/authorize_client_test.go rename proxy/clients/mock_clients.go => internal/grpc/authorize/client/mock.go (91%) rename {proto/authorize => internal/grpc/authorize/client}/mock_authorize/mock_authorize.go (98%) create mode 100644 internal/grpc/cache/cache.pb.go create mode 100644 internal/grpc/cache/cache.proto create mode 100644 internal/grpc/cache/client/cache_client.go rename proxy/clients/clients.go => internal/grpc/client.go (72%) create mode 100644 internal/grpc/client_test.go rename internal/{grpcutil/grpc.go => grpc/server.go} (69%) create mode 100644 internal/grpc/server_test.go rename {proxy/clients => internal/grpc}/testdata/example.crt (100%) delete mode 100644 internal/grpcutil/middleware.go create mode 100644 internal/kv/autocache/autocache.go create mode 100644 internal/kv/bolt/bolt.go create mode 100644 internal/kv/redis/redis.go create mode 100644 internal/kv/store.go delete mode 100644 proxy/clients/authorize_client_test.go delete mode 100644 proxy/clients/clients_test.go diff --git a/.gitignore b/.gitignore index 3bb0c717c..cc7d48b43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.db .*.yml .*.yaml pem diff --git a/.golangci.yml b/.golangci.yml index 19dbb46d5..20f0472f0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -120,7 +120,6 @@ linters-settings: - initClause - methodExprCall - nilValReturn - - octalLiteral - offBy1 - rangeExprCopy - regexpMust diff --git a/authenticate/authenticate.go b/authenticate/authenticate.go index e3cac9982..b7bb9f451 100644 --- a/authenticate/authenticate.go +++ b/authenticate/authenticate.go @@ -14,6 +14,8 @@ import ( "github.com/pomerium/pomerium/internal/encoding/ecjson" "github.com/pomerium/pomerium/internal/encoding/jws" "github.com/pomerium/pomerium/internal/frontend" + "github.com/pomerium/pomerium/internal/grpc" + "github.com/pomerium/pomerium/internal/grpc/cache/client" "github.com/pomerium/pomerium/internal/identity" "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions/cache" @@ -82,6 +84,9 @@ type Authenticate struct { // provider is the interface to interacting with the identity provider (IdP) provider identity.Authenticator + // cacheClient is the interface for setting and getting sessions from a cache + cacheClient client.Cacher + templates *template.Template } @@ -115,7 +120,29 @@ func New(opts config.Options) (*Authenticate, error) { if err != nil { return nil, err } - cacheStore := cache.NewStore(encryptedEncoder, cookieStore, opts.CookieName) + + cacheConn, err := grpc.NewGRPCClientConn( + &grpc.Options{ + Addr: opts.CacheURL, + OverrideCertificateName: opts.OverrideCertificateName, + CA: opts.CA, + CAFile: opts.CAFile, + RequestTimeout: opts.GRPCClientTimeout, + ClientDNSRoundRobin: opts.GRPCClientDNSRoundRobin, + WithInsecure: opts.GRPCInsecure, + }) + if err != nil { + return nil, err + } + + cacheClient := client.New(cacheConn) + + cacheStore := cache.NewStore(&cache.Options{ + Cache: cacheClient, + Encoder: encryptedEncoder, + QueryParam: urlutil.QueryAccessTokenID, + WrappedStore: cookieStore}) + qpStore := queryparam.NewStore(encryptedEncoder, "pomerium_programmatic_token") headerStore := header.NewStore(encryptedEncoder, "Pomerium") @@ -152,6 +179,8 @@ func New(opts config.Options) (*Authenticate, error) { sessionLoaders: []sessions.SessionLoader{cacheStore, qpStore, headerStore, cookieStore}, // IdP provider: provider, + // grpc client for cache + cacheClient: cacheClient, templates: template.Must(frontend.NewTemplates()), }, nil diff --git a/authenticate/authenticate_test.go b/authenticate/authenticate_test.go index f1f0bed53..a28387cda 100644 --- a/authenticate/authenticate_test.go +++ b/authenticate/authenticate_test.go @@ -84,6 +84,9 @@ func TestNew(t *testing.T) { badProvider := newTestOptions(t) badProvider.Provider = "" badProvider.CookieName = "C" + badGRPCConn := newTestOptions(t) + badGRPCConn.CacheURL = nil + badGRPCConn.CookieName = "D" tests := []struct { name string @@ -96,6 +99,7 @@ func TestNew(t *testing.T) { {"fails to validate", badRedirectURL, true}, {"bad cookie name", badCookieName, true}, {"bad provider", badProvider, true}, + {"bad cache url", badGRPCConn, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/authenticate/handlers.go b/authenticate/handlers.go index 8b39e71f1..535d9ab54 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -150,9 +150,11 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error { // user impersonation if impersonate := r.FormValue(urlutil.QueryImpersonateAction); impersonate != "" { s.SetImpersonation(r.FormValue(urlutil.QueryImpersonateEmail), r.FormValue(urlutil.QueryImpersonateGroups)) - if err := a.sessionStore.SaveSession(w, r, s); err != nil { - return httputil.NewError(http.StatusBadRequest, err) - } + } + + // re-persist the session, useful when session was evicted from session + if err := a.sessionStore.SaveSession(w, r, s); err != nil { + return httputil.NewError(http.StatusBadRequest, err) } newSession := s.NewSession(a.RedirectURL.Host, jwtAudience) @@ -353,7 +355,7 @@ func (a *Authenticate) RefreshAPI(w http.ResponseWriter, r *http.Request) error // Refresh is called by the proxy service to handle backend session refresh. // -// NOTE: The actual refresh is actually handled as part of the "VerifySession" +// NOTE: The actual refresh is handled as part of the "VerifySession" // middleware. This handler is responsible for creating a new route scoped // session and returning it. func (a *Authenticate) Refresh(w http.ResponseWriter, r *http.Request) error { @@ -362,7 +364,7 @@ func (a *Authenticate) Refresh(w http.ResponseWriter, r *http.Request) error { return httputil.NewError(http.StatusBadRequest, err) } - routeSession := s.NewSession(r.Host, []string{r.Host, r.FormValue("aud")}) + routeSession := s.NewSession(r.Host, []string{r.Host, r.FormValue(urlutil.QueryAudience)}) routeSession.AccessTokenID = s.AccessTokenID signedJWT, err := a.sharedEncoder.Marshal(routeSession.RouteSession()) diff --git a/authorize/grpc.go b/authorize/grpc.go index e878bf7cf..93ed7af10 100644 --- a/authorize/grpc.go +++ b/authorize/grpc.go @@ -1,16 +1,16 @@ -//go:generate protoc -I ../proto/authorize --go_out=plugins=grpc:../proto/authorize ../proto/authorize/authorize.proto +//go:generate protoc -I ../internal/grpc/authorize/ --go_out=plugins=grpc:../internal/grpc/authorize/ ../internal/grpc/authorize/authorize.proto package authorize // import "github.com/pomerium/pomerium/authorize" import ( "context" + "github.com/pomerium/pomerium/internal/grpc/authorize" "github.com/pomerium/pomerium/internal/telemetry/trace" - pb "github.com/pomerium/pomerium/proto/authorize" ) // Authorize validates the user identity, device, and context of a request for // a given route. Currently only checks identity. -func (a *Authorize) Authorize(ctx context.Context, in *pb.Identity) (*pb.AuthorizeReply, error) { +func (a *Authorize) Authorize(ctx context.Context, in *authorize.Identity) (*authorize.AuthorizeReply, error) { _, span := trace.StartSpan(ctx, "authorize.grpc.Authorize") defer span.End() @@ -22,11 +22,11 @@ func (a *Authorize) Authorize(ctx context.Context, in *pb.Identity) (*pb.Authori ImpersonateEmail: in.ImpersonateEmail, ImpersonateGroups: in.ImpersonateGroups, }) - return &pb.AuthorizeReply{IsValid: ok}, nil + return &authorize.AuthorizeReply{IsValid: ok}, nil } // IsAdmin validates the user is an administrative user. -func (a *Authorize) IsAdmin(ctx context.Context, in *pb.Identity) (*pb.IsAdminReply, error) { +func (a *Authorize) IsAdmin(ctx context.Context, in *authorize.Identity) (*authorize.IsAdminReply, error) { _, span := trace.StartSpan(ctx, "authorize.grpc.IsAdmin") defer span.End() ok := a.identityAccess.IsAdmin( @@ -34,5 +34,5 @@ func (a *Authorize) IsAdmin(ctx context.Context, in *pb.Identity) (*pb.IsAdminRe Email: in.Email, Groups: in.Groups, }) - return &pb.IsAdminReply{IsAdmin: ok}, nil + return &authorize.IsAdminReply{IsAdmin: ok}, nil } diff --git a/authorize/grpc_test.go b/authorize/grpc_test.go index f11909126..310740c67 100644 --- a/authorize/grpc_test.go +++ b/authorize/grpc_test.go @@ -1,5 +1,3 @@ -//go:generate protoc -I ../proto/authorize --go_out=plugins=grpc:../proto/authorize ../proto/authorize/authorize.proto - package authorize import ( @@ -7,7 +5,7 @@ import ( "reflect" "testing" - pb "github.com/pomerium/pomerium/proto/authorize" + pb "github.com/pomerium/pomerium/internal/grpc/authorize" ) func TestAuthorize_Authorize(t *testing.T) { diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 000000000..5d14a219a --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,68 @@ +package cache // import "github.com/pomerium/pomerium/cache" + +import ( + "context" + "errors" + "fmt" + stdlog "log" + + "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/internal/kv" + "github.com/pomerium/pomerium/internal/kv/autocache" + "github.com/pomerium/pomerium/internal/kv/bolt" + "github.com/pomerium/pomerium/internal/kv/redis" + "github.com/pomerium/pomerium/internal/log" +) + +// Cache represents the cache service. The cache service is a simple interface +// for storing keyed blobs (bytes) of unstructured data. +type Cache struct { + cache kv.Store +} + +// New creates a new cache service. +func New(opts config.Options) (*Cache, error) { + cache, err := NewCacheStore(opts.CacheStore, &opts) + if err != nil { + return nil, err + } + return &Cache{ + cache: cache, + }, nil +} + +// NewCacheStore creates a new cache store by name and given a set of +// configuration options. +func NewCacheStore(name string, o *config.Options) (s kv.Store, err error) { + switch name { + case bolt.Name: + s, err = bolt.New(&bolt.Options{Path: o.CacheStorePath}) + case redis.Name: + // todo(bdd): make path configurable in config.Options + s, err = redis.New(&redis.Options{ + Addr: o.CacheStoreAddr, + Password: o.CacheStorePassword, + }) + case autocache.Name: + acLog := log.Logger.With().Str("service", "autocache").Logger() + s, err = autocache.New(&autocache.Options{ + SharedKey: o.SharedKey, + Log: stdlog.New(acLog, "", 0), + ClusterDomain: o.CacheURL.Hostname(), + }) + default: + return nil, fmt.Errorf("cache: unknown store: %s", name) + } + if err != nil { + return nil, err + } + return s, nil +} + +// Close shuts down the underlying cache store, services, or both -- if any. +func (c *Cache) Close() error { + if c.cache == nil { + return errors.New("cache: cannot close nil cache") + } + return c.cache.Close(context.TODO()) +} diff --git a/cache/grpc.go b/cache/grpc.go new file mode 100644 index 000000000..be9fc114d --- /dev/null +++ b/cache/grpc.go @@ -0,0 +1,34 @@ +//go:generate protoc -I ../internal/grpc/cache/ --go_out=plugins=grpc:../internal/grpc/cache/ ../internal/grpc/cache/cache.proto + +package cache // import "github.com/pomerium/pomerium/cache" +import ( + "context" + + "github.com/pomerium/pomerium/internal/grpc/cache" + "github.com/pomerium/pomerium/internal/telemetry/trace" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Get retrieves a key the cache store and returns the value, if found. +func (c *Cache) Get(ctx context.Context, in *cache.GetRequest) (*cache.GetReply, error) { + ctx, span := trace.StartSpan(ctx, "cache.grpc.Get") + defer span.End() + exists, value, err := c.cache.Get(ctx, in.GetKey()) + if err != nil { + return nil, status.Errorf(codes.Unknown, "cache.grpc.Get error: %v", err) + } + return &cache.GetReply{Exists: exists, Value: value}, nil +} + +// Set persists a key value pair in the cache store. +func (c *Cache) Set(ctx context.Context, in *cache.SetRequest) (*cache.SetReply, error) { + ctx, span := trace.StartSpan(ctx, "cache.grpc.Set") + defer span.End() + err := c.cache.Set(ctx, in.GetKey(), in.GetValue()) + if err != nil { + return nil, status.Errorf(codes.Unknown, "cache.grpc.Set error: %v", err) + } + return &cache.SetReply{}, nil +} diff --git a/cmd/pomerium/main.go b/cmd/pomerium/main.go index eb363a30d..fe4106274 100644 --- a/cmd/pomerium/main.go +++ b/cmd/pomerium/main.go @@ -13,9 +13,12 @@ import ( "github.com/pomerium/pomerium/authenticate" "github.com/pomerium/pomerium/authorize" + "github.com/pomerium/pomerium/cache" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/frontend" - "github.com/pomerium/pomerium/internal/grpcutil" + pgrpc "github.com/pomerium/pomerium/internal/grpc" + pbAuthorize "github.com/pomerium/pomerium/internal/grpc/authorize" + pbCache "github.com/pomerium/pomerium/internal/grpc/cache" "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/middleware" @@ -23,7 +26,6 @@ import ( "github.com/pomerium/pomerium/internal/telemetry/trace" "github.com/pomerium/pomerium/internal/urlutil" "github.com/pomerium/pomerium/internal/version" - pbAuthorize "github.com/pomerium/pomerium/proto/authorize" "github.com/pomerium/pomerium/proxy" ) @@ -64,11 +66,19 @@ func run() error { if err != nil { return err } - authz, err := newAuthorizeService(*opt, &wg) + authz, err := newAuthorizeService(*opt) if err != nil { return err } + cacheSvc, err := newCacheService(*opt) + if err != nil { + return err + } + if cacheSvc != nil { + defer cacheSvc.Close() + } + proxy, err := newProxyService(*opt, r) if err != nil { return err @@ -82,6 +92,10 @@ func run() error { opt = config.HandleConfigUpdate(*configFile, opt, []config.OptionsUpdater{authz, proxy}) }) + if err := newGRPCServer(*opt, authz, cacheSvc, &wg); err != nil { + return err + } + srv, err := httputil.NewServer(httpServerOptions(opt), r, &wg) if err != nil { return err @@ -106,27 +120,43 @@ func newAuthenticateService(opt config.Options, r *mux.Router) (*authenticate.Au return service, nil } -func newAuthorizeService(opt config.Options, wg *sync.WaitGroup) (*authorize.Authorize, error) { +func newAuthorizeService(opt config.Options) (*authorize.Authorize, error) { if !config.IsAuthorize(opt.Services) { return nil, nil } - service, err := authorize.New(opt) - if err != nil { - return nil, err + return authorize.New(opt) +} + +func newCacheService(opt config.Options) (*cache.Cache, error) { + if !config.IsCache(opt.Services) { + return nil, nil + } + return cache.New(opt) +} + +func newGRPCServer(opt config.Options, as *authorize.Authorize, cs *cache.Cache, wg *sync.WaitGroup) error { + if as == nil && cs == nil { + return nil } regFn := func(s *grpc.Server) { - pbAuthorize.RegisterAuthorizerServer(s, service) - } - so := &grpcutil.ServerOptions{ - Addr: opt.GRPCAddr, - SharedKey: opt.SharedKey, + if as != nil { + pbAuthorize.RegisterAuthorizerServer(s, as) + } + if cs != nil { + pbCache.RegisterCacheServer(s, cs) + + } } + so := &pgrpc.ServerOptions{Addr: opt.GRPCAddr} if !opt.GRPCInsecure { so.TLSCertificate = opt.TLSCertificate } - grpcSrv := grpcutil.NewServer(so, regFn, wg) - go grpcutil.Shutdown(grpcSrv) - return service, nil + grpcSrv, err := pgrpc.NewServer(so, regFn, wg) + if err != nil { + return err + } + go pgrpc.Shutdown(grpcSrv) + return nil } func newProxyService(opt config.Options, r *mux.Router) (*proxy.Proxy, error) { diff --git a/cmd/pomerium/main_test.go b/cmd/pomerium/main_test.go index 95697055e..14ffa0b55 100644 --- a/cmd/pomerium/main_test.go +++ b/cmd/pomerium/main_test.go @@ -277,6 +277,51 @@ func Test_run(t *testing.T) { "policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }] } `, true}, + // {"simple cache", false, ` + // { + // "address": ":9433", + // "grpc_address": ":9444", + // "grpc_insecure": false, + // "insecure_server": true, + // "cache_service_url": "https://authorize.corp.example", + // "authenticate_service_url": "https://authenticate.corp.example", + // "shared_secret": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", + // "cookie_secret": "zixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", + // "services": "cache", + // "cache_store": "bolt", + // "policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }] + // } + // `, false}, + // {"malformed cache", false, ` + // { + // "address": ":9433", + // "grpc_address": ":9444", + // "grpc_insecure": false, + // "insecure_server": true, + // "cache_service_url": "https://authorize.corp.example", + // "authenticate_service_url": "https://authenticate.corp.example", + // "shared_secret": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", + // "cookie_secret": "zixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", + // "services": "cache", + // "cache_store": "bad bolt", + // "policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }] + // } + // `, true}, + // {"bad cache port", false, ` + // { + // "address": ":9433", + // "grpc_address": ":9999999", + // "grpc_insecure": false, + // "insecure_server": true, + // "cache_service_url": "https://authorize.corp.example", + // "authenticate_service_url": "https://authenticate.corp.example", + // "shared_secret": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", + // "cookie_secret": "zixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", + // "services": "cache", + // "cache_store": "bolt", + // "policy": [{ "from": "https://pomerium.io", "to": "https://httpbin.org" }] + // } + // `, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/config/helpers.go b/config/helpers.go index 4d68fc7fb..f1bd2f9e0 100644 --- a/config/helpers.go +++ b/config/helpers.go @@ -9,6 +9,8 @@ const ( ServiceAuthorize = "authorize" // ServiceAuthenticate represents running the authenticate service component ServiceAuthenticate = "authenticate" + // ServiceCache represents running the cache service component + ServiceCache = "cache" ) // IsValidService checks to see if a service is a valid service mode @@ -16,9 +18,10 @@ func IsValidService(s string) bool { switch s { case ServiceAll, - ServiceProxy, + ServiceAuthenticate, ServiceAuthorize, - ServiceAuthenticate: + ServiceCache, + ServiceProxy: return true } return false @@ -57,6 +60,17 @@ func IsProxy(s string) bool { return false } +// IsCache checks to see if we should be running the proxy service +func IsCache(s string) bool { + switch s { + case + ServiceAll, + ServiceCache: + return true + } + return false +} + // IsAll checks to see if we should be running all services func IsAll(s string) bool { return s == ServiceAll diff --git a/config/helpers_test.go b/config/helpers_test.go index ed7e2b28f..c1ef3165c 100644 --- a/config/helpers_test.go +++ b/config/helpers_test.go @@ -16,6 +16,7 @@ func Test_isValidService(t *testing.T) { {"authenticate bad case", "AuThenticate", false}, {"authorize implemented", "authorize", true}, {"jiberish", "xd23", false}, + {"cache", "cache", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -27,6 +28,7 @@ func Test_isValidService(t *testing.T) { } func Test_isAuthenticate(t *testing.T) { + t.Parallel() tests := []struct { name string service string @@ -50,6 +52,7 @@ func Test_isAuthenticate(t *testing.T) { } func Test_isAuthorize(t *testing.T) { + t.Parallel() tests := []struct { name string service string @@ -92,3 +95,27 @@ func Test_IsProxy(t *testing.T) { }) } } + +func Test_IsCache(t *testing.T) { + t.Parallel() + tests := []struct { + name string + service string + want bool + }{ + {"proxy", "proxy", false}, + {"all", "all", true}, + {"authorize", "authorize", false}, + {"proxy bad case", "PrOxY", false}, + {"jiberish", "xd23", false}, + {"cache", "cache", true}, + } + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + if got := IsCache(tt.service); got != tt.want { + t.Errorf("IsCache() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/config/options.go b/config/options.go index e884e80d2..8dbf5372c 100644 --- a/config/options.go +++ b/config/options.go @@ -174,6 +174,25 @@ type Options struct { ForwardAuthURLString string `mapstructure:"forward_auth_url" yaml:"forward_auth_url,omitempty"` ForwardAuthURL *url.URL `yaml:",omitempty"` + // CacheStore is the name of session cache backend to use. + // Options are : "bolt", "redis", and "autocache". + // Default is "autocache". + CacheStore string `mapstructure:"cache_store" yaml:"cache_store,omitempty"` + + // CacheURL is the routable destination of the cache service's + // gRPC endpoint. NOTE: As many load balancers do not support + // externally routed gRPC so this may be an internal location. + CacheURLString string `mapstructure:"cache_service_url" yaml:"cache_service_url,omitempty"` + CacheURL *url.URL `yaml:",omitempty"` + + // CacheStoreAddr specifies the host and port on which the cache store + // should connect to. e.g. (localhost:6379) + CacheStoreAddr string `mapstructure:"cache_store_address" yaml:"cache_store_address,omitempty"` + // CacheStorePassword is the password used to connect to the cache store. + CacheStorePassword string `mapstructure:"cache_store_password" yaml:"cache_store_password,omitempty"` + // CacheStorePath is the path to use for a given cache store. e.g. /etc/bolt.db + CacheStorePath string `mapstructure:"cache_store_path" yaml:"cache_store_path,omitempty"` + viper *viper.Viper } @@ -201,6 +220,7 @@ var defaultOptions = Options{ GRPCAddr: ":443", GRPCClientTimeout: 10 * time.Second, // Try to withstand transient service failures for a single request GRPCClientDNSRoundRobin: true, + CacheStore: "autocache", } // NewDefaultOptions returns a copy the default options. It's the caller's @@ -397,9 +417,12 @@ func (o *Options) Validate() error { if o.AuthorizeURLString == "" { o.AuthorizeURLString = "https://localhost" + DefaultAlternativeAddr } + if o.CacheURLString == "" { + o.CacheURLString = "https://localhost" + DefaultAlternativeAddr + } } - if IsAuthorize(o.Services) { + if IsAuthorize(o.Services) || IsCache(o.Services) { // if authorize is set, we don't really need a http server // but we'll still set one up incase the user wants to use // the HTTP health check api @@ -433,6 +456,14 @@ func (o *Options) Validate() error { o.AuthorizeURL = u } + if o.CacheURLString != "" { + u, err := urlutil.ParseAndValidateURL(o.CacheURLString) + if err != nil { + return fmt.Errorf("config: bad cache service url %s : %w", o.CacheURLString, err) + } + o.CacheURL = u + } + if o.ForwardAuthURLString != "" { u, err := urlutil.ParseAndValidateURL(o.ForwardAuthURLString) if err != nil { diff --git a/config/options_test.go b/config/options_test.go index 92929dea0..4586bce75 100644 --- a/config/options_test.go +++ b/config/options_test.go @@ -205,7 +205,7 @@ func Test_Checksum(t *testing.T) { func TestOptionsFromViper(t *testing.T) { t.Parallel() opts := []cmp.Option{ - cmpopts.IgnoreFields(Options{}, "CookieSecret", "GRPCInsecure", "GRPCAddr", "AuthorizeURL", "AuthorizeURLString", "DefaultUpstreamTimeout", "CookieExpire", "Services", "Addr", "RefreshCooldown", "LogLevel", "KeyFile", "CertFile", "SharedKey", "ReadTimeout", "ReadHeaderTimeout", "IdleTimeout", "GRPCClientTimeout", "GRPCClientDNSRoundRobin"), + cmpopts.IgnoreFields(Options{}, "CacheStore", "CookieSecret", "GRPCInsecure", "GRPCAddr", "CacheURLString", "CacheURL", "AuthorizeURL", "AuthorizeURLString", "DefaultUpstreamTimeout", "CookieExpire", "Services", "Addr", "RefreshCooldown", "LogLevel", "KeyFile", "CertFile", "SharedKey", "ReadTimeout", "ReadHeaderTimeout", "IdleTimeout", "GRPCClientTimeout", "GRPCClientDNSRoundRobin"), cmpopts.IgnoreFields(Policy{}, "Source", "Destination"), cmpOptIgnoreUnexported, } @@ -280,6 +280,9 @@ func Test_NewOptionsFromConfigEnvVar(t *testing.T) { {"bad no certs no insecure mode set", map[string]string{"SHARED_SECRET": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="}, true}, {"good disable headers ", map[string]string{"HEADERS": "disable:true", "INSECURE_SERVER": "true", "SHARED_SECRET": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="}, false}, {"bad whitespace in secret", map[string]string{"INSECURE_SERVER": "true", "SERVICES": "authenticate", "SHARED_SECRET": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=\n"}, true}, + {"bad cache url", map[string]string{"CACHE_SERVICE_URL": "cache.example", "INSECURE_SERVER": "true", "SHARED_SECRET": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="}, true}, + {"bad forward auth url", map[string]string{"FORWARD_AUTH_URL": "cache.example", "INSECURE_SERVER": "true", "SHARED_SECRET": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="}, true}, + {"same addr and grpc addr", map[string]string{"SERVICES": "cache", "ADDRESS": "0", "GRPC_ADDRESS": "0", "INSECURE_SERVER": "true", "SHARED_SECRET": "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/go.mod b/go.mod index 833cd9067..4bb964150 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,17 @@ require ( contrib.go.opencensus.io/exporter/prometheus v0.1.0 github.com/cespare/xxhash/v2 v2.1.1 github.com/fsnotify/fsnotify v1.4.7 + github.com/go-redis/redis v6.15.6+incompatible github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 github.com/golang/mock v1.3.1 github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.4.0 github.com/gorilla/mux v1.7.3 github.com/mitchellh/hashstructure v1.0.0 + github.com/onsi/ginkgo v1.11.0 // indirect + github.com/onsi/gomega v1.8.1 // indirect github.com/pelletier/go-toml v1.6.0 // indirect + github.com/pomerium/autocache v0.0.0-20200117194553-700b15fea434 github.com/pomerium/csrf v1.6.2-0.20190918035251-f3318380bad3 github.com/pomerium/go-oidc v2.0.0+incompatible github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect @@ -28,15 +32,15 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.6.2 github.com/uber/jaeger-client-go v2.20.1+incompatible // indirect + go.etcd.io/bbolt v1.3.2 go.opencensus.io v0.22.2 golang.org/x/crypto v0.0.0-20200117160349-530e935923ad - golang.org/x/net v0.0.0-20191125084936-ffdde1057850 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e // indirect google.golang.org/api v0.14.0 google.golang.org/appengine v1.6.5 // indirect google.golang.org/genproto v0.0.0-20191115221424-83cc0476cb11 // indirect google.golang.org/grpc v1.26.0 + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/square/go-jose.v2 v2.4.1 gopkg.in/yaml.v2 v2.2.7 ) diff --git a/go.sum b/go.sum index 5543f9622..2801e6035 100644 --- a/go.sum +++ b/go.sum @@ -20,17 +20,24 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.0 h1:B7AQgHi8QSEi4uHu7Sbsga+IJDU+CENgjxoo81vDUqU= +github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -38,6 +45,8 @@ github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReG github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -52,6 +61,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -60,6 +70,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg= +github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= @@ -82,12 +94,14 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -108,11 +122,34 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmo github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc= +github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/memberlist v0.1.5 h1:AYBsgJOW9gab/toO5tEB8lWetVgDKZycqkebJ8xxpqM= +github.com/hashicorp/memberlist v0.1.5/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -131,8 +168,15 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= +github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y= github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -143,6 +187,14 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= +github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= @@ -152,10 +204,13 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pomerium/autocache v0.0.0-20200117194553-700b15fea434 h1:gC7WWP2nvqkLKP8nkeyLdwUx97SBSHGLahivmB4tXIY= +github.com/pomerium/autocache v0.0.0-20200117194553-700b15fea434/go.mod h1:6eBAk305Ex3fI8KjDC0+5SpZUnPE09uQNosrUQxcOaE= github.com/pomerium/csrf v1.6.2-0.20190918035251-f3318380bad3 h1:FmzFXnCAepHZwl6QPhTFqBHcbcGevdiEQjutK+M5bj4= github.com/pomerium/csrf v1.6.2-0.20190918035251-f3318380bad3/go.mod h1:UE2U4JOsjXNeq+MX/lqhZpUFsNAxbXERuYsWK2iULh0= github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF44mDNx5nayBhC0o= github.com/pomerium/go-oidc v2.0.0+incompatible/go.mod h1:DRsGVw6MOgxbfq4Y57jKOE8lbEfayxeiY0A8/4vxjBM= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -195,6 +250,9 @@ github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo= github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -217,8 +275,7 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= -github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -229,6 +286,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber/jaeger-client-go v2.15.0+incompatible h1:NP3qsSqNxh8VYr956ur1N/1C1PjvOJnJykCzcD5QHbk= github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.20.1+incompatible h1:HgqpYBng0n7tLJIlyT4kPCIv5XgCsF+kai1NnnrJzEU= @@ -237,6 +295,7 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= @@ -247,13 +306,11 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= -golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 h1:nVJ3guKA9qdkEQ3TUdXI9QSINo2CUPM/cySEvw2w8I0= -golang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg= golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -274,8 +331,11 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -290,15 +350,15 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0O golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191125084936-ffdde1057850 h1:Vq85/r8R9IdcUHmZ0/nQlUg1v15rzvQ2sHdnZAj/x7s= -golang.org/x/net v0.0.0-20191125084936-ffdde1057850/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -309,8 +369,11 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEha golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= @@ -322,9 +385,10 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c h1:gUYreENmqtjZb2brVfUas1sC6UivSY8XwKwPo8tloLs= +golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -352,7 +416,10 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2 h1:EtTFh6h4SAKemS+CURDMTDIANuduG5zKEXShyy18bGA= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/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= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -390,12 +457,18 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y= gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/proto/authorize/authorize.pb.go b/internal/grpc/authorize/authorize.pb.go similarity index 100% rename from proto/authorize/authorize.pb.go rename to internal/grpc/authorize/authorize.pb.go diff --git a/proto/authorize/authorize.proto b/internal/grpc/authorize/authorize.proto similarity index 100% rename from proto/authorize/authorize.proto rename to internal/grpc/authorize/authorize.proto diff --git a/proxy/clients/authorize_client.go b/internal/grpc/authorize/client/authorize_client.go similarity index 52% rename from proxy/clients/authorize_client.go rename to internal/grpc/authorize/client/authorize_client.go index 85470099d..b6ee82096 100644 --- a/proxy/clients/authorize_client.go +++ b/internal/grpc/authorize/client/authorize_client.go @@ -1,12 +1,12 @@ -package clients // import "github.com/pomerium/pomerium/proxy/clients" +package client import ( "context" "errors" + pb "github.com/pomerium/pomerium/internal/grpc/authorize" "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/telemetry/trace" - pb "github.com/pomerium/pomerium/proto/authorize" "google.golang.org/grpc" ) @@ -22,38 +22,27 @@ type Authorizer interface { Close() error } -// NewAuthorizeClient returns a new authorize service client. -func NewAuthorizeClient(name string, opts *Options) (a Authorizer, err error) { - // Only gRPC is supported and is always returned so name is ignored - return NewGRPCAuthorizeClient(opts) -} - -// NewGRPCAuthorizeClient returns a new authorize service client. -func NewGRPCAuthorizeClient(opts *Options) (p *AuthorizeGRPC, err error) { - conn, err := NewGRPCClientConn(opts) - if err != nil { - return nil, err - } - client := pb.NewAuthorizerClient(conn) - return &AuthorizeGRPC{Conn: conn, client: client}, nil -} - -// AuthorizeGRPC is a gRPC implementation of an authenticator (authorize client) -type AuthorizeGRPC struct { +// Client is a gRPC implementation of an authenticator (authorize client) +type Client struct { Conn *grpc.ClientConn client pb.AuthorizerClient } +// New returns a new authorize service client. +func New(conn *grpc.ClientConn) (p *Client, err error) { + return &Client{Conn: conn, client: pb.NewAuthorizerClient(conn)}, nil +} + // Authorize takes a route and user session and returns whether the // request is valid per access policy -func (a *AuthorizeGRPC) Authorize(ctx context.Context, route string, s *sessions.State) (bool, error) { - ctx, span := trace.StartSpan(ctx, "proxy.client.grpc.Authorize") +func (c *Client) Authorize(ctx context.Context, route string, s *sessions.State) (bool, error) { + ctx, span := trace.StartSpan(ctx, "grpc.authorize.client.Authorize") defer span.End() if s == nil { return false, errors.New("session cannot be nil") } - response, err := a.client.Authorize(ctx, &pb.Identity{ + response, err := c.client.Authorize(ctx, &pb.Identity{ Route: route, User: s.User, Email: s.Email, @@ -65,18 +54,18 @@ func (a *AuthorizeGRPC) Authorize(ctx context.Context, route string, s *sessions } // IsAdmin takes a session and returns whether the user is an administrator -func (a *AuthorizeGRPC) IsAdmin(ctx context.Context, s *sessions.State) (bool, error) { - ctx, span := trace.StartSpan(ctx, "proxy.client.grpc.IsAdmin") +func (c *Client) IsAdmin(ctx context.Context, s *sessions.State) (bool, error) { + ctx, span := trace.StartSpan(ctx, "grpc.authorize.client.IsAdmin") defer span.End() if s == nil { return false, errors.New("session cannot be nil") } - response, err := a.client.IsAdmin(ctx, &pb.Identity{Email: s.Email, Groups: s.Groups}) + response, err := c.client.IsAdmin(ctx, &pb.Identity{Email: s.Email, Groups: s.Groups}) return response.GetIsAdmin(), err } // Close tears down the ClientConn and all underlying connections. -func (a *AuthorizeGRPC) Close() error { - return a.Conn.Close() +func (c *Client) Close() error { + return c.Conn.Close() } diff --git a/internal/grpc/authorize/client/authorize_client_test.go b/internal/grpc/authorize/client/authorize_client_test.go new file mode 100644 index 000000000..437dd21bf --- /dev/null +++ b/internal/grpc/authorize/client/authorize_client_test.go @@ -0,0 +1,78 @@ +package client + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/pomerium/pomerium/internal/grpc/authorize" + "github.com/pomerium/pomerium/internal/grpc/authorize/client/mock_authorize" + "github.com/pomerium/pomerium/internal/sessions" +) + +func TestClient_Authorize(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mock_authorize.NewMockAuthorizerClient(ctrl) + client.EXPECT().Authorize( + gomock.Any(), + gomock.Any(), + ).Return(&authorize.AuthorizeReply{IsValid: true}, nil).AnyTimes() + + tests := []struct { + name string + route string + s *sessions.State + want bool + wantErr bool + }{ + {"good", "hello.pomerium.io", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false}, + {"impersonate request", "hello.pomerium.io", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io", ImpersonateEmail: "other@other.example"}, true, false}, + {"session cannot be nil", "hello.pomerium.io", nil, false, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Client{client: client} + got, err := a.Authorize(context.Background(), tt.route, tt.s) + if (err != nil) != tt.wantErr { + t.Errorf("Client.Authorize() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Client.Authorize() = %v, want %v", got, tt.want) + } + }) + } +} +func TestClient_IsAdmin(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mock_authorize.NewMockAuthorizerClient(ctrl) + client.EXPECT().IsAdmin( + gomock.Any(), + gomock.Any(), + ).Return(&authorize.IsAdminReply{IsAdmin: true}, nil).AnyTimes() + + tests := []struct { + name string + s *sessions.State + want bool + wantErr bool + }{ + {"good", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false}, + {"session cannot be nil", nil, false, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Client{client: client} + got, err := a.IsAdmin(context.Background(), tt.s) + if (err != nil) != tt.wantErr { + t.Errorf("Client.IsAdmin() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Client.IsAdmin() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/proxy/clients/mock_clients.go b/internal/grpc/authorize/client/mock.go similarity index 91% rename from proxy/clients/mock_clients.go rename to internal/grpc/authorize/client/mock.go index 9acac4594..c2ffa242a 100644 --- a/proxy/clients/mock_clients.go +++ b/internal/grpc/authorize/client/mock.go @@ -1,4 +1,4 @@ -package clients // import "github.com/pomerium/pomerium/proxy/clients" +package client import ( "context" @@ -6,6 +6,8 @@ import ( "github.com/pomerium/pomerium/internal/sessions" ) +var _ Authorizer = &MockAuthorize{} + // MockAuthorize provides a mocked implementation of the authorizer interface. type MockAuthorize struct { AuthorizeResponse bool diff --git a/proto/authorize/mock_authorize/mock_authorize.go b/internal/grpc/authorize/client/mock_authorize/mock_authorize.go similarity index 98% rename from proto/authorize/mock_authorize/mock_authorize.go rename to internal/grpc/authorize/client/mock_authorize/mock_authorize.go index d9a0d38c4..31eab0edd 100644 --- a/proto/authorize/mock_authorize/mock_authorize.go +++ b/internal/grpc/authorize/client/mock_authorize/mock_authorize.go @@ -9,7 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - authorize "github.com/pomerium/pomerium/proto/authorize" + authorize "github.com/pomerium/pomerium/internal/grpc/authorize" grpc "google.golang.org/grpc" ) diff --git a/internal/grpc/cache/cache.pb.go b/internal/grpc/cache/cache.pb.go new file mode 100644 index 000000000..47a0230e2 --- /dev/null +++ b/internal/grpc/cache/cache.pb.go @@ -0,0 +1,329 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: cache.proto + +package cache + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type GetRequest struct { + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetRequest) Reset() { *m = GetRequest{} } +func (m *GetRequest) String() string { return proto.CompactTextString(m) } +func (*GetRequest) ProtoMessage() {} +func (*GetRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_5fca3b110c9bbf3a, []int{0} +} + +func (m *GetRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetRequest.Unmarshal(m, b) +} +func (m *GetRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetRequest.Marshal(b, m, deterministic) +} +func (m *GetRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetRequest.Merge(m, src) +} +func (m *GetRequest) XXX_Size() int { + return xxx_messageInfo_GetRequest.Size(m) +} +func (m *GetRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetRequest proto.InternalMessageInfo + +func (m *GetRequest) GetKey() string { + if m != nil { + return m.Key + } + return "" +} + +type GetReply struct { + Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetReply) Reset() { *m = GetReply{} } +func (m *GetReply) String() string { return proto.CompactTextString(m) } +func (*GetReply) ProtoMessage() {} +func (*GetReply) Descriptor() ([]byte, []int) { + return fileDescriptor_5fca3b110c9bbf3a, []int{1} +} + +func (m *GetReply) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetReply.Unmarshal(m, b) +} +func (m *GetReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetReply.Marshal(b, m, deterministic) +} +func (m *GetReply) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetReply.Merge(m, src) +} +func (m *GetReply) XXX_Size() int { + return xxx_messageInfo_GetReply.Size(m) +} +func (m *GetReply) XXX_DiscardUnknown() { + xxx_messageInfo_GetReply.DiscardUnknown(m) +} + +var xxx_messageInfo_GetReply proto.InternalMessageInfo + +func (m *GetReply) GetExists() bool { + if m != nil { + return m.Exists + } + return false +} + +func (m *GetReply) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +type SetRequest struct { + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SetRequest) Reset() { *m = SetRequest{} } +func (m *SetRequest) String() string { return proto.CompactTextString(m) } +func (*SetRequest) ProtoMessage() {} +func (*SetRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_5fca3b110c9bbf3a, []int{2} +} + +func (m *SetRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SetRequest.Unmarshal(m, b) +} +func (m *SetRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SetRequest.Marshal(b, m, deterministic) +} +func (m *SetRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_SetRequest.Merge(m, src) +} +func (m *SetRequest) XXX_Size() int { + return xxx_messageInfo_SetRequest.Size(m) +} +func (m *SetRequest) XXX_DiscardUnknown() { + xxx_messageInfo_SetRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_SetRequest proto.InternalMessageInfo + +func (m *SetRequest) GetKey() string { + if m != nil { + return m.Key + } + return "" +} + +func (m *SetRequest) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +type SetReply struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SetReply) Reset() { *m = SetReply{} } +func (m *SetReply) String() string { return proto.CompactTextString(m) } +func (*SetReply) ProtoMessage() {} +func (*SetReply) Descriptor() ([]byte, []int) { + return fileDescriptor_5fca3b110c9bbf3a, []int{3} +} + +func (m *SetReply) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SetReply.Unmarshal(m, b) +} +func (m *SetReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SetReply.Marshal(b, m, deterministic) +} +func (m *SetReply) XXX_Merge(src proto.Message) { + xxx_messageInfo_SetReply.Merge(m, src) +} +func (m *SetReply) XXX_Size() int { + return xxx_messageInfo_SetReply.Size(m) +} +func (m *SetReply) XXX_DiscardUnknown() { + xxx_messageInfo_SetReply.DiscardUnknown(m) +} + +var xxx_messageInfo_SetReply proto.InternalMessageInfo + +func init() { + proto.RegisterType((*GetRequest)(nil), "cache.GetRequest") + proto.RegisterType((*GetReply)(nil), "cache.GetReply") + proto.RegisterType((*SetRequest)(nil), "cache.SetRequest") + proto.RegisterType((*SetReply)(nil), "cache.SetReply") +} + +func init() { proto.RegisterFile("cache.proto", fileDescriptor_5fca3b110c9bbf3a) } + +var fileDescriptor_5fca3b110c9bbf3a = []byte{ + // 176 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4e, 0x4e, 0x4c, 0xce, + 0x48, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x73, 0x94, 0xe4, 0xb8, 0xb8, 0xdc, + 0x53, 0x4b, 0x82, 0x52, 0x0b, 0x4b, 0x53, 0x8b, 0x4b, 0x84, 0x04, 0xb8, 0x98, 0xb3, 0x53, 0x2b, + 0x25, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0x40, 0x4c, 0x25, 0x0b, 0x2e, 0x0e, 0xb0, 0x7c, 0x41, + 0x4e, 0xa5, 0x90, 0x18, 0x17, 0x5b, 0x6a, 0x45, 0x66, 0x71, 0x49, 0x31, 0x58, 0x01, 0x47, 0x10, + 0x94, 0x27, 0x24, 0xc2, 0xc5, 0x5a, 0x96, 0x98, 0x53, 0x9a, 0x2a, 0xc1, 0xa4, 0xc0, 0xa8, 0xc1, + 0x13, 0x04, 0xe1, 0x28, 0x99, 0x70, 0x71, 0x05, 0xe3, 0x31, 0x19, 0x87, 0x2e, 0x2e, 0x2e, 0x8e, + 0x60, 0xa8, 0x7d, 0x46, 0x89, 0x5c, 0xac, 0xce, 0x20, 0x47, 0x0a, 0x69, 0x73, 0x31, 0xbb, 0xa7, + 0x96, 0x08, 0x09, 0xea, 0x41, 0x3c, 0x80, 0x70, 0xb0, 0x14, 0x3f, 0xb2, 0x50, 0x41, 0x4e, 0xa5, + 0x12, 0x03, 0x48, 0x71, 0x30, 0x92, 0xe2, 0x60, 0x4c, 0xc5, 0xc1, 0x70, 0xc5, 0x49, 0x6c, 0xe0, + 0xc0, 0x30, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x0e, 0xef, 0x5f, 0x9e, 0x1b, 0x01, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// CacheClient is the client API for Cache service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type CacheClient interface { + Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetReply, error) + Set(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*SetReply, error) +} + +type cacheClient struct { + cc *grpc.ClientConn +} + +func NewCacheClient(cc *grpc.ClientConn) CacheClient { + return &cacheClient{cc} +} + +func (c *cacheClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetReply, error) { + out := new(GetReply) + err := c.cc.Invoke(ctx, "/cache.Cache/Get", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *cacheClient) Set(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*SetReply, error) { + out := new(SetReply) + err := c.cc.Invoke(ctx, "/cache.Cache/Set", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CacheServer is the server API for Cache service. +type CacheServer interface { + Get(context.Context, *GetRequest) (*GetReply, error) + Set(context.Context, *SetRequest) (*SetReply, error) +} + +// UnimplementedCacheServer can be embedded to have forward compatible implementations. +type UnimplementedCacheServer struct { +} + +func (*UnimplementedCacheServer) Get(ctx context.Context, req *GetRequest) (*GetReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") +} +func (*UnimplementedCacheServer) Set(ctx context.Context, req *SetRequest) (*SetReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Set not implemented") +} + +func RegisterCacheServer(s *grpc.Server, srv CacheServer) { + s.RegisterService(&_Cache_serviceDesc, srv) +} + +func _Cache_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CacheServer).Get(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/cache.Cache/Get", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CacheServer).Get(ctx, req.(*GetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Cache_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CacheServer).Set(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/cache.Cache/Set", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CacheServer).Set(ctx, req.(*SetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Cache_serviceDesc = grpc.ServiceDesc{ + ServiceName: "cache.Cache", + HandlerType: (*CacheServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Get", + Handler: _Cache_Get_Handler, + }, + { + MethodName: "Set", + Handler: _Cache_Set_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "cache.proto", +} diff --git a/internal/grpc/cache/cache.proto b/internal/grpc/cache/cache.proto new file mode 100644 index 000000000..c5ebb3670 --- /dev/null +++ b/internal/grpc/cache/cache.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package cache; + +service Cache { + rpc Get(GetRequest) returns (GetReply) {} + rpc Set(SetRequest) returns (SetReply) {} +} + +message GetRequest { string key = 1; } +message GetReply { + bool exists = 1; + bytes value = 2; +} + +message SetRequest { + string key = 1; + bytes value = 2; +} +message SetReply {} diff --git a/internal/grpc/cache/client/cache_client.go b/internal/grpc/cache/client/cache_client.go new file mode 100644 index 000000000..41c636afb --- /dev/null +++ b/internal/grpc/cache/client/cache_client.go @@ -0,0 +1,57 @@ +package client + +import ( + "context" + + "github.com/pomerium/pomerium/internal/grpc/cache" + "github.com/pomerium/pomerium/internal/telemetry/trace" + + "google.golang.org/grpc" +) + +// Cacher specifies an interface for remote clients connecting to the cache service. +type Cacher interface { + Get(ctx context.Context, key string) (keyExists bool, value []byte, err error) + Set(ctx context.Context, key string, value []byte) error + Close() error +} + +// Client represents a gRPC cache service client. +type Client struct { + conn *grpc.ClientConn + client cache.CacheClient +} + +// New returns a new gRPC cache service client. +func New(conn *grpc.ClientConn) (p *Client) { + return &Client{conn: conn, client: cache.NewCacheClient(conn)} +} + +// Get retrieves a value from the cache service. +func (a *Client) Get(ctx context.Context, key string) (keyExists bool, value []byte, err error) { + ctx, span := trace.StartSpan(ctx, "grpc.cache.client.Get") + defer span.End() + + response, err := a.client.Get(ctx, &cache.GetRequest{Key: key}) + if err != nil { + return false, nil, err + } + return response.GetExists(), response.GetValue(), nil +} + +// Set stores a key value pair in the cache service. +func (a *Client) Set(ctx context.Context, key string, value []byte) error { + ctx, span := trace.StartSpan(ctx, "grpc.cache.client.Set") + defer span.End() + + _, err := a.client.Set(ctx, &cache.SetRequest{Key: key, Value: value}) + if err != nil { + return err + } + return nil +} + +// Close tears down the ClientConn and all underlying connections. +func (a *Client) Close() error { + return a.conn.Close() +} diff --git a/proxy/clients/clients.go b/internal/grpc/client.go similarity index 72% rename from proxy/clients/clients.go rename to internal/grpc/client.go index 813b8cb15..84eb69b86 100644 --- a/proxy/clients/clients.go +++ b/internal/grpc/client.go @@ -1,4 +1,4 @@ -package clients // import "github.com/pomerium/pomerium/proxy/clients" +package grpc // import "github.com/pomerium/pomerium/internal/grpc" import ( "context" @@ -12,7 +12,6 @@ import ( "strings" "time" - "github.com/pomerium/pomerium/internal/grpcutil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/telemetry/metrics" @@ -28,15 +27,10 @@ const defaultGRPCPort = 443 type Options struct { // Addr is the location of the service. e.g. "service.corp.example:8443" Addr *url.URL - // InternalAddr is the internal (behind the ingress) address to use when - // making a connection. If empty, Addr is used. - InternalAddr *url.URL // OverrideCertificateName overrides the server name used to verify the hostname on the - // returned certificates from the server. gRPC internals also use it to override the virtual + // returned certificates from the server. gRPC internals also use it to override the virtual // hosting name if it is set. OverrideCertificateName string - // Shared secret is used to mutually authenticate a client and server. - SharedSecret string // CA specifies the base64 encoded TLS certificate authority to use. CA string // CAFile specifies the TLS certificate authority file to use. @@ -46,46 +40,35 @@ type Options struct { // ClientDNSRoundRobin enables or disables DNS resolver based load balancing ClientDNSRoundRobin bool - // WithInsecure disables transport security for this ClientConn. + // WithInsecure disables transport security for this ClientConn. // Note that transport security is required unless WithInsecure is set. WithInsecure bool } // NewGRPCClientConn returns a new gRPC pomerium service client connection. func NewGRPCClientConn(opts *Options) (*grpc.ClientConn, error) { - // gRPC uses a pre-shared secret middleware to establish authentication b/w server and client - if opts.SharedSecret == "" { - return nil, errors.New("proxy/clients: grpc client requires shared secret") + if opts.Addr == nil { + return nil, errors.New("internal/grpc: connection address required") } - if opts.InternalAddr == nil && opts.Addr == nil { - return nil, errors.New("proxy/clients: connection address required") + connAddr := opts.Addr.Host - } - - var connAddr string - if opts.InternalAddr != nil { - connAddr = opts.InternalAddr.Host - } else { - connAddr = opts.Addr.Host - } // no colon exists in the connection string, assume one must be added manually if !strings.Contains(connAddr, ":") { connAddr = fmt.Sprintf("%s:%d", connAddr, defaultGRPCPort) } dialOptions := []grpc.DialOption{ - grpc.WithPerRPCCredentials(grpcutil.NewSharedSecretCred(opts.SharedSecret)), - grpc.WithChainUnaryInterceptor(metrics.GRPCClientInterceptor("proxy"), grpcTimeoutInterceptor(opts.RequestTimeout)), + grpc.WithChainUnaryInterceptor(metrics.GRPCClientInterceptor(connAddr), grpcTimeoutInterceptor(opts.RequestTimeout)), grpc.WithStatsHandler(&ocgrpc.ClientHandler{}), grpc.WithDefaultCallOptions([]grpc.CallOption{grpc.WaitForReady(true)}...), } if opts.WithInsecure { - log.Info().Str("addr", connAddr).Msg("proxy/clients: grpc with insecure") + log.Info().Str("addr", connAddr).Msg("internal/grpc: grpc with insecure") dialOptions = append(dialOptions, grpc.WithInsecure()) } else { rootCAs, err := x509.SystemCertPool() if err != nil { - log.Warn().Msg("proxy/clients: failed getting system cert pool making new one") + log.Warn().Msg("internal/grpc: failed getting system cert pool making new one") rootCAs = x509.NewCertPool() } if opts.CA != "" || opts.CAFile != "" { @@ -105,14 +88,14 @@ func NewGRPCClientConn(opts *Options) (*grpc.ClientConn, error) { if ok := rootCAs.AppendCertsFromPEM(ca); !ok { return nil, fmt.Errorf("failed to append CA cert to certPool") } - log.Debug().Msg("proxy/clients: added custom certificate authority") + log.Debug().Msg("internal/grpc: added custom certificate authority") } cert := credentials.NewTLS(&tls.Config{RootCAs: rootCAs}) // override allowed certificate name string, typically used when doing behind ingress connection if opts.OverrideCertificateName != "" { - log.Debug().Str("cert-override-name", opts.OverrideCertificateName).Msg("proxy/clients: grpc") + log.Debug().Str("cert-override-name", opts.OverrideCertificateName).Msg("internal/grpc: grpc") err := cert.OverrideServerName(opts.OverrideCertificateName) if err != nil { return nil, err @@ -120,7 +103,6 @@ func NewGRPCClientConn(opts *Options) (*grpc.ClientConn, error) { } // finally add our credential dialOptions = append(dialOptions, grpc.WithTransportCredentials(cert)) - } if opts.ClientDNSRoundRobin { @@ -139,10 +121,8 @@ func grpcTimeoutInterceptor(timeout time.Duration) grpc.UnaryClientInterceptor { if timeout <= 0 { return invoker(ctx, method, req, reply, cc, opts...) } - ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() return invoker(ctx, method, req, reply, cc, opts...) - } } diff --git a/internal/grpc/client_test.go b/internal/grpc/client_test.go new file mode 100644 index 000000000..a011d6e73 --- /dev/null +++ b/internal/grpc/client_test.go @@ -0,0 +1,78 @@ +package grpc // import "github.com/pomerium/pomerium/internal/grpc" + +import ( + "context" + "net/url" + "strings" + "testing" + "time" + + "google.golang.org/grpc" +) + +func Test_grpcTimeoutInterceptor(t *testing.T) { + + mockInvoker := func(sleepTime time.Duration, wantFail bool) grpc.UnaryInvoker { + return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { + time.Sleep(sleepTime) + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("No deadline set") + } + + now := time.Now() + + if ok && now.After(deadline) && !wantFail { + t.Errorf("Deadline exceeded, but should not have. now=%v, deadline=%v", now, deadline) + } else if now.Before(deadline) && wantFail { + t.Errorf("Deadline not exceeded, but should have. now=%v, deadline=%v", now, deadline) + } + return nil + } + } + + timeOut := 300 * time.Millisecond + to := grpcTimeoutInterceptor(timeOut) + + to(context.Background(), "test", nil, nil, nil, mockInvoker(timeOut*2, true)) + to(context.Background(), "test", nil, nil, nil, mockInvoker(timeOut/2, false)) + +} + +func TestNewGRPC(t *testing.T) { + t.Parallel() + tests := []struct { + name string + opts *Options + wantErr bool + wantErrStr string + wantTarget string + }{ + {"empty connection", &Options{Addr: nil}, true, "proxy/authenticator: connection address required", ""}, + {"both internal and addr empty", &Options{Addr: nil}, true, "proxy/authenticator: connection address required", ""}, + {"addr with port", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}}, false, "", "localhost.example:8443"}, + {"addr without port", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example"}}, false, "", "localhost.example:443"}, + {"cert override", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:443"}, OverrideCertificateName: "*.local"}, false, "", "localhost.example:443"}, + {"custom ca", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:443"}, OverrideCertificateName: "*.local", CA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZrQ0ZBWHhneFg5K0hjWlBVVVBEK0laV0NGNUEvVTdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1FVXgKQ3pBSkJnTlZCQVlUQWtGVk1STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbApjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTk1Ua3dNakk0TVRnMU1EQTNXaGNOTWprd01qSTFNVGcxCk1EQTNXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNBd0tVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUUKQ2d3WVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBOVRFMEFiaTdnMHhYeURkVUtEbDViNTBCT05ZVVVSc3F2THQrSWkwdlpjMzRRTHhOClJrT0hrOFZEVUgzcUt1N2UrNGVubUdLVVNUdzRPNFlkQktiSWRJTFpnb3o0YitNL3FVOG5adVpiN2pBVTdOYWkKajMzVDVrbXB3L2d4WHNNUzNzdUpXUE1EUDB3Z1BUZUVRK2J1bUxVWmpLdUVIaWNTL0l5dmtaVlBzRlE4NWlaUwpkNXE2a0ZGUUdjWnFXeFg0dlhDV25Sd3E3cHY3TThJd1RYc1pYSVRuNXB5Z3VTczNKb29GQkg5U3ZNTjRKU25GCmJMK0t6ekduMy9ScXFrTXpMN3FUdkMrNWxVT3UxUmNES21mZXBuVGVaN1IyVnJUQm42NndWMjVHRnBkSDIzN00KOXhJVkJrWEd1U2NvWHVPN1lDcWFrZkt6aXdoRTV4UmRaa3gweXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFCaHRWUEI0OCs4eFZyVmRxM1BIY3k5QkxtVEtrRFl6N2Q0ODJzTG1HczBuVUdGSTFZUDdmaFJPV3ZxCktCTlpkNEI5MUpwU1NoRGUrMHpoNno4WG5Ha01mYnRSYWx0NHEwZ3lKdk9hUWhqQ3ZCcSswTFk5d2NLbXpFdnMKcTRiNUZ5NXNpRUZSekJLTmZtTGwxTTF2cW1hNmFCVnNYUUhPREdzYS83dE5MalZ2ay9PYm52cFg3UFhLa0E3cQpLMTQvV0tBRFBJWm9mb00xMzB4Q1RTYXVpeXROajlnWkx1WU9leEZhblVwNCt2MHBYWS81OFFSNTk2U0ROVTlKClJaeDhwTzBTaUYvZXkxVUZXbmpzdHBjbTQzTFVQKzFwU1hFeVhZOFJrRTI2QzNvdjNaTFNKc2pMbC90aXVqUlgKZUJPOWorWDdzS0R4amdtajBPbWdpVkpIM0YrUAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}, false, "", "localhost.example:443"}, + {"bad ca encoding", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:443"}, OverrideCertificateName: "*.local", CA: "^"}, true, "", "localhost.example:443"}, + {"custom ca file", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:443"}, OverrideCertificateName: "*.local", CAFile: "testdata/example.crt"}, false, "", "localhost.example:443"}, + {"bad custom ca file", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:443"}, OverrideCertificateName: "*.local", CAFile: "testdata/example.crt2"}, true, "", "localhost.example:443"}, + {"valid with insecure", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, WithInsecure: true}, false, "", "localhost.example:8443"}, + {"valid client round robin", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, ClientDNSRoundRobin: true}, false, "", "dns:///localhost.example:8443"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewGRPCClientConn(tt.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + if !strings.EqualFold(err.Error(), tt.wantErrStr) { + t.Errorf("New() error = %v did not contain wantErr %v", err, tt.wantErrStr) + } + } + if got != nil && got.Target() != tt.wantTarget { + t.Errorf("New() target = %v expected %v", got.Target(), tt.wantTarget) + + } + }) + } +} diff --git a/internal/grpcutil/grpc.go b/internal/grpc/server.go similarity index 69% rename from internal/grpcutil/grpc.go rename to internal/grpc/server.go index 103b65b87..50e5be187 100644 --- a/internal/grpcutil/grpc.go +++ b/internal/grpc/server.go @@ -1,4 +1,4 @@ -package grpcutil // import "github.com/pomerium/pomerium/internal/grpcutil" +package grpc // import "github.com/pomerium/pomerium/internal/grpc" import ( "crypto/tls" @@ -10,13 +10,14 @@ import ( "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/telemetry/metrics" + "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) // NewServer creates a new gRPC serve. // It is the callers responsibility to close the resturned server. -func NewServer(opt *ServerOptions, registrationFn func(s *grpc.Server), wg *sync.WaitGroup) *grpc.Server { +func NewServer(opt *ServerOptions, registrationFn func(s *grpc.Server), wg *sync.WaitGroup) (*grpc.Server, error) { if opt == nil { opt = defaultServerOptions } else { @@ -24,33 +25,31 @@ func NewServer(opt *ServerOptions, registrationFn func(s *grpc.Server), wg *sync } ln, err := net.Listen("tcp", opt.Addr) if err != nil { - log.Fatal().Str("addr", opt.Addr).Err(err).Msg("internal/grpcutil: unexpected ") + return nil, err } - grpcAuth := NewSharedSecretCred(opt.SharedKey) - grpcOpts := []grpc.ServerOption{ - grpc.UnaryInterceptor(grpcAuth.ValidateRequest), - grpc.StatsHandler(metrics.NewGRPCServerStatsHandler(opt.Addr))} + grpcOpts := []grpc.ServerOption{grpc.StatsHandler(metrics.NewGRPCServerStatsHandler(opt.Addr))} if opt.TLSCertificate != nil { - log.Debug().Str("addr", opt.Addr).Msg("internal/grpcutil: with TLS") + log.Debug().Str("addr", opt.Addr).Msg("internal/grpc: serving over TLS") cert := credentials.NewServerTLSFromCert(opt.TLSCertificate) grpcOpts = append(grpcOpts, grpc.Creds(cert)) } else { - log.Warn().Str("addr", opt.Addr).Msg("internal/grpcutil: insecure server") + log.Warn().Str("addr", opt.Addr).Msg("internal/grpc: serving without TLS") } srv := grpc.NewServer(grpcOpts...) registrationFn(srv) + log.Info().Interface("grpc-service-info", srv.GetServiceInfo()).Msg("internal/grpc: registered") wg.Add(1) go func() { defer wg.Done() if err := srv.Serve(ln); err != grpc.ErrServerStopped { - log.Error().Str("addr", opt.Addr).Err(err).Msg("internal/grpcutil: unexpected shutdown") + log.Error().Str("addr", opt.Addr).Err(err).Msg("internal/grpc: unexpected shutdown") } }() - return srv + return srv, nil } // ServerOptions contains the configurations settings for a gRPC server. @@ -59,10 +58,6 @@ type ServerOptions struct { // gRPC requests. If empty, ":443" is used. Addr string - // SharedKey is the shared secret authorization key used to mutually authenticate - // requests between services. - SharedKey string - // TLS certificates to use, if any. TLSCertificate *tls.Certificate @@ -93,8 +88,7 @@ func Shutdown(srv *grpc.Server) { signal.Notify(sigint, os.Interrupt) signal.Notify(sigint, syscall.SIGTERM) rec := <-sigint - log.Info().Str("signal", rec.String()).Msg("internal/grpcutil: shutting down servers") + log.Info().Str("signal", rec.String()).Msg("internal/grpc: shutting down servers") srv.GracefulStop() - log.Info().Str("signal", rec.String()).Msg("internal/grpcutil: shut down servers") - + log.Info().Str("signal", rec.String()).Msg("internal/grpc: shut down servers") } diff --git a/internal/grpc/server_test.go b/internal/grpc/server_test.go new file mode 100644 index 000000000..13ef07c0f --- /dev/null +++ b/internal/grpc/server_test.go @@ -0,0 +1,84 @@ +package grpc + +import ( + "encoding/base64" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/pomerium/pomerium/internal/cryptutil" + "google.golang.org/grpc" +) + +const privKey = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMQiDy26/R4ca/OdnjIf8OEDeHcw8yB5SDV9FD500CW5oAoGCCqGSM49 +AwEHoUQDQgAEFumdSrEe9dnPEUU3LuyC8l6MM6PefNgpSsRL4GrD22XITMjqDKFr +jqJTf0Fo1ZWm4v+Eds6s88rsLzEC+cKLRQ== +-----END EC PRIVATE KEY-----` +const pubKey = `-----BEGIN CERTIFICATE----- +MIIBeDCCAR+gAwIBAgIUUGE8w2S7XzpkVLbNq5QUxyVOwqEwCgYIKoZIzj0EAwIw +ETEPMA0GA1UEAwwGdW51c2VkMCAXDTE5MDcxNTIzNDQyOVoYDzQ3NTcwNjExMjM0 +NDI5WjARMQ8wDQYDVQQDDAZ1bnVzZWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC +AAQW6Z1KsR712c8RRTcu7ILyXowzo9582ClKxEvgasPbZchMyOoMoWuOolN/QWjV +labi/4R2zqzzyuwvMQL5wotFo1MwUTAdBgNVHQ4EFgQURYdcaniRqBHXeaM79LtV +pyJ4EwAwHwYDVR0jBBgwFoAURYdcaniRqBHXeaM79LtVpyJ4EwAwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAgNHADBEAiBHbhVnGbwXqaMZ1dB8eBAK56jyeWDZ +2PWXmFMTu7+RywIgaZ7UwVNB2k7KjEEBiLm0PIRcpJmczI2cP9+ZMIkPHHw= +-----END CERTIFICATE-----` + +func TestNewServer(t *testing.T) { + certb64, err := cryptutil.CertifcateFromBase64( + base64.StdEncoding.EncodeToString([]byte(pubKey)), + base64.StdEncoding.EncodeToString([]byte(privKey))) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + opt *ServerOptions + registrationFn func(s *grpc.Server) + wg *sync.WaitGroup + wantNil bool + wantErr bool + }{ + {"simple", &ServerOptions{Addr: ":0"}, func(s *grpc.Server) {}, &sync.WaitGroup{}, false, false}, + {"bad tcp port", &ServerOptions{Addr: ":9999999"}, func(s *grpc.Server) {}, &sync.WaitGroup{}, true, true}, + {"with certs", &ServerOptions{Addr: ":0", TLSCertificate: certb64}, func(s *grpc.Server) {}, &sync.WaitGroup{}, false, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewServer(tt.opt, tt.registrationFn, tt.wg) + if (err != nil) != tt.wantErr { + t.Errorf("NewServer() error = %v, wantErr %v", err, tt.wantErr) + return + } + if (got == nil) != tt.wantNil { + t.Errorf("NewServer() = %v, want %v", got, tt.wantNil) + } + if got != nil { + // simulate a sigterm and cleanup the server + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT) + defer signal.Stop(c) + go Shutdown(got) + syscall.Kill(syscall.Getpid(), syscall.SIGINT) + waitSig(t, c, syscall.SIGINT) + } + }) + } +} + +func waitSig(t *testing.T, c <-chan os.Signal, sig os.Signal) { + select { + case s := <-c: + if s != sig { + t.Fatalf("signal was %v, want %v", s, sig) + } + case <-time.After(1 * time.Second): + t.Fatalf("timeout waiting for %v", sig) + } +} diff --git a/proxy/clients/testdata/example.crt b/internal/grpc/testdata/example.crt similarity index 100% rename from proxy/clients/testdata/example.crt rename to internal/grpc/testdata/example.crt diff --git a/internal/grpcutil/middleware.go b/internal/grpcutil/middleware.go deleted file mode 100644 index 2f7498145..000000000 --- a/internal/grpcutil/middleware.go +++ /dev/null @@ -1,54 +0,0 @@ -package grpcutil // import "github.com/pomerium/pomerium/internal/grpcutil" - -import ( - "context" - - "github.com/pomerium/pomerium/internal/telemetry/trace" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -// SharedSecretCred is a simple token-based method of mutual authentication. -type SharedSecretCred struct{ sharedSecret string } - -// NewSharedSecretCred returns a new instance of shared secret credential middleware for gRPC clients -func NewSharedSecretCred(secret string) *SharedSecretCred { - return &SharedSecretCred{sharedSecret: secret} -} - -// GetRequestMetadata sets the value for "authorization" key -func (s SharedSecretCred) GetRequestMetadata(context.Context, ...string) (map[string]string, error) { - return map[string]string{"authorization": s.sharedSecret}, nil -} - -// RequireTransportSecurity indicates whether the credentials requires -// transport security. -func (s SharedSecretCred) RequireTransportSecurity() bool { return false } - -// ValidateRequest ensures a valid token exists within a request's metadata. If -// the token is missing or invalid, the interceptor blocks execution of the -// handler and returns an error. Otherwise, the interceptor invokes the unary -// handler. -func (s SharedSecretCred) ValidateRequest(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - ctx, span := trace.StartSpan(ctx, "middleware.grpc.ValidateRequest") - defer span.End() - - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return nil, status.Errorf(codes.InvalidArgument, "missing metadata") - } - // The keys within metadata.MD are normalized to lowercase. - // See: https://godoc.org/google.golang.org/grpc/metadata#New - elem, ok := md["authorization"] - if !ok { - return nil, status.Errorf(codes.InvalidArgument, "no auth details supplied") - } - if elem[0] != s.sharedSecret { - return nil, status.Errorf(codes.Unauthenticated, "invalid shared secrets") - } - // Continue execution of handler after ensuring a valid token. - return handler(ctx, req) -} diff --git a/internal/kv/autocache/autocache.go b/internal/kv/autocache/autocache.go new file mode 100644 index 000000000..919a5a9f2 --- /dev/null +++ b/internal/kv/autocache/autocache.go @@ -0,0 +1,235 @@ +package autocache + +import ( + "context" + "errors" + "fmt" + stdlog "log" + "net/http" + "sync" + + "github.com/golang/groupcache" + "github.com/pomerium/autocache" + "github.com/pomerium/pomerium/internal/httputil" + "github.com/pomerium/pomerium/internal/kv" + "github.com/pomerium/pomerium/internal/urlutil" +) + +// Name represents autocache's shorthand named. +const Name = "autocache" + +const defaultQueryParamKey = "ati" + +var _ kv.Store = &Store{} + +// Store implements a the store interface for autocache, a distributed cache +// with gossip based peer membership enrollment. +// https://github.com/pomerium/autocache +type Store struct { + db *groupcache.Group + cluster *autocache.Autocache + sharedKey string + srv *http.Server +} + +// Options represent autocache options. +type Options struct { + Addr string + CacheSize int64 + ClusterDomain string + GetterFn groupcache.GetterFunc + Group string + Log *stdlog.Logger + Port int + Scheme string + SharedKey string +} + +// DefaultOptions are the default options used by the autocache service. +var DefaultOptions = &Options{ + Addr: ":8333", + Port: 8333, + Scheme: "http", + CacheSize: 10 << 20, + Group: "default", + GetterFn: func(ctx context.Context, id string, dest groupcache.Sink) error { + b := fromContext(ctx) + if len(b) == 0 { + return fmt.Errorf("autocache: empty ctx for id: %s", id) + } + if err := dest.SetBytes(b); err != nil { + return fmt.Errorf("autocache: sink error %w", err) + } + return nil + }, +} + +// New creates a new autocache key value store. Autocache will start several +// services to support distributed cluster management and membership. +// A HTTP server will be used by groupcache to perform cross node-RPC. By +// default that server will start on port ``:8333`. +// Memberlist will likewise start and listen for group membership on port +// +// +// NOTE: RPC communication between nodes is _authenticated_ but not encrypted. +// NOTE: Groupchache starts a HTTP listener (Default: :8333) +// NOTE: Memberlist starts a GOSSIP listener on TCP/UDP. (Default: :7946) +func New(o *Options) (*Store, error) { + var s Store + var err error + if o.SharedKey == "" { + return nil, errors.New("autocache: shared secret must be set") + } + if o.Addr == "" { + o.Addr = DefaultOptions.Addr + } + if o.Scheme == "" { + o.Scheme = DefaultOptions.Scheme + } + if o.Port == 0 { + o.Port = DefaultOptions.Port + } + if o.Group == "" { + o.Group = DefaultOptions.Group + } + if o.GetterFn == nil { + o.GetterFn = DefaultOptions.GetterFn + } + if o.CacheSize == 0 { + o.CacheSize = DefaultOptions.CacheSize + } + if o.ClusterDomain == "" { + o.Log.Println("") + } + s.db = groupcache.NewGroup(o.Group, o.CacheSize, o.GetterFn) + s.cluster, err = autocache.New(&autocache.Options{ + PoolTransportFn: s.addSessionToCtx, + PoolScheme: o.Scheme, + PoolPort: o.Port, + Logger: o.Log, + }) + if err != nil { + return nil, err + } + serverOpts := &httputil.ServerOptions{Addr: o.Addr} + var wg sync.WaitGroup + s.srv, err = httputil.NewServer(serverOpts, QueryParamToCtx(s.cluster), &wg) + if err != nil { + return nil, err + } + if _, err := s.cluster.Join([]string{o.ClusterDomain}); err != nil { + return nil, err + } + return &s, nil +} + +// Set stores a key value pair. Since group cache actually only implements +// Get, we have to be a little creative in how we smuggle in value using +// context. +func (s Store) Set(ctx context.Context, k string, v []byte) error { + // smuggle the the value pair as a context value + ctx = newContext(ctx, v) + if err := s.db.Get(ctx, k, groupcache.AllocatingByteSliceSink(&v)); err != nil { + return fmt.Errorf("autocache: set %s failed: %w", k, err) + } + return nil +} + +// Get retrieves the value for a key in the bucket. +func (s *Store) Get(ctx context.Context, k string) (bool, []byte, error) { + var value []byte + if err := s.db.Get(ctx, k, groupcache.AllocatingByteSliceSink(&value)); err != nil { + return false, nil, fmt.Errorf("autocache: get %s failed: %w", k, err) + } + return true, value, nil +} + +// Close shuts down any HTTP server used for groupcache pool, and +// also stop any background maintenance of memberlist. +func (s Store) Close(ctx context.Context) error { + var retErr error + if s.srv != nil { + if err := s.srv.Shutdown(ctx); err != nil { + retErr = fmt.Errorf("autocache: http shutdown error: %w", err) + } + } + if s.cluster.Memberlist != nil { + if err := s.cluster.Memberlist.Shutdown(); err != nil { + retErr = fmt.Errorf("autocache: memberlist shutdown error: %w", err) + } + } + return retErr +} + +// addSessionToCtx is a wrapper function that allows us to add a session +// into http client's round trip and sign the outgoing request. +func (s *Store) addSessionToCtx(ctx context.Context) http.RoundTripper { + var sh signedSession + sh.session = string(fromContext(ctx)) + sh.sharedKey = s.sharedKey + return sh +} + +type signedSession struct { + session string + sharedKey string +} + +// RoundTrip copies the request's session context and adds it to the +// outgoing client request as a query param. The whole URL is then signed for +// authenticity. +func (s signedSession) RoundTrip(req *http.Request) (*http.Response, error) { + // clone request before mutating + // https://golang.org/src/net/http/client.go?s=4306:5535#L105 + newReq := cloneRequest(req) + session := s.session + newReqURL := *newReq.URL + q := newReqURL.Query() + q.Set(defaultQueryParamKey, session) + newReqURL.RawQuery = q.Encode() + newReq.URL = urlutil.NewSignedURL(s.sharedKey, &newReqURL).Sign() + return http.DefaultTransport.RoundTrip(newReq) +} + +// QueryParamToCtx takes a value from a query param and adds it to the +// current request request context. +func QueryParamToCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.FormValue(defaultQueryParamKey) + ctx := newContext(r.Context(), []byte(session)) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +var sessionCtxKey = &contextKey{"PomeriumCachedSessionBytes"} + +type contextKey struct { + name string +} + +func newContext(ctx context.Context, b []byte) context.Context { + ctx = context.WithValue(ctx, sessionCtxKey, b) + return ctx +} + +func fromContext(ctx context.Context) []byte { + b, _ := ctx.Value(sessionCtxKey).([]byte) + return b +} + +func cloneRequest(req *http.Request) *http.Request { + r := new(http.Request) + *r = *req + r.Header = cloneHeaders(req.Header) + return r +} + +func cloneHeaders(in http.Header) http.Header { + out := make(http.Header, len(in)) + for key, values := range in { + newValues := make([]string, len(values)) + copy(newValues, values) + out[key] = newValues + } + return out +} diff --git a/internal/kv/bolt/bolt.go b/internal/kv/bolt/bolt.go new file mode 100644 index 000000000..d680af811 --- /dev/null +++ b/internal/kv/bolt/bolt.go @@ -0,0 +1,109 @@ +package bolt + +import ( + "context" + + "github.com/pomerium/pomerium/internal/kv" + + bolt "go.etcd.io/bbolt" +) + +var _ kv.Store = &Store{} + +// Name represents bbolt's shorthand named. +const Name = "bolt" + +// Store implements a the Store interface for bolt. +// https://godoc.org/github.com/etcd-io/bbolt +type Store struct { + db *bolt.DB + bucket string +} + +// Options represents options for configuring the boltdb cache store. +type Options struct { + // Buckets are collections of key/value pairs within the database. + // All keys in a bucket must be unique. + Bucket string + // Path is where the database file will be stored. + Path string +} + +// DefaultOptions contain's bolts default options. +var DefaultOptions = &Options{ + Bucket: "default", + Path: Name + ".db", +} + +// New creates a new bolt cache store. +// It is up to the operator to make sure that the store's path +// is writeable. +func New(o *Options) (*Store, error) { + if o.Path == "" { + o.Path = DefaultOptions.Path + } + if o.Bucket == "" { + o.Bucket = DefaultOptions.Bucket + } + + db, err := bolt.Open(o.Path, 0600, nil) + if err != nil { + return nil, err + } + + err = db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(o.Bucket)) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + + return &Store{db: db, bucket: o.Bucket}, nil +} + +// Set sets the value for a key in the bucket. +// If the key exist then its previous value will be overwritten. +// Supplied value must remain valid for the life of the transaction. +// Returns an error if the bucket was created from a read-only transaction, +// if the key is blank, if the key is too large, or if the value is too large. +func (s Store) Set(ctx context.Context, k string, v []byte) error { + err := s.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(s.bucket)) + return b.Put([]byte(k), v) + }) + if err != nil { + return err + } + return nil +} + +// Get retrieves the value for a key in the bucket. +// Returns a nil value if the key does not exist or if the key is a nested bucket. +// The returned value is only valid for the life of the transaction. +func (s *Store) Get(ctx context.Context, k string) (bool, []byte, error) { + var value []byte + err := s.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(s.bucket)) + txData := b.Get([]byte(k)) // only valid in transaction + value = append(txData[:0:0], txData...) + return nil + }) + if err != nil { + return false, nil, err + } + if value == nil { + return false, nil, nil + } + return true, value, nil +} + +// Close releases all database resources. +// It will block waiting for any open transactions to finish +// before closing the database and returning. +func (s Store) Close(ctx context.Context) error { + return s.db.Close() +} diff --git a/internal/kv/redis/redis.go b/internal/kv/redis/redis.go new file mode 100644 index 000000000..45aa11c42 --- /dev/null +++ b/internal/kv/redis/redis.go @@ -0,0 +1,89 @@ +package redis + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + + "github.com/go-redis/redis" + "github.com/pomerium/pomerium/internal/kv" +) + +var _ kv.Store = &Store{} + +// Name represents redis's shorthand name. +const Name = "redis" + +// Store implements a the Store interface for redis. +// https://godoc.org/github.com/go-redis/redis +type Store struct { + db *redis.Client +} + +// Options represents options for configuring the redis store. +type Options struct { + // host:port Addr. + Addr string + // Optional password. Must match the password specified in the + // requirepass server configuration option. + Password string + // Database to be selected after connecting to the server. + DB int + // TLS Config to use. When set TLS will be negotiated. + TLSConfig *tls.Config +} + +// New creates a new redis cache store. +// It is up to the operator to make sure that the store's path +// is writeable. +func New(o *Options) (*Store, error) { + if o.Addr == "" { + return nil, fmt.Errorf("kv/redis: connection address is required") + } + + db := redis.NewClient( + &redis.Options{ + Addr: o.Addr, + Password: o.Password, + DB: o.DB, + TLSConfig: o.TLSConfig, + }) + + if _, err := db.Ping().Result(); err != nil { + return nil, fmt.Errorf("kv/redis: error connecting to redis: %w", err) + } + + return &Store{db: db}, nil +} + +// Set is equivalent to redis `SET key value [expiration]` command. +// +// Use expiration for `SETEX`-like behavior. +// Zero expiration means the key has no expiration time. +func (s Store) Set(ctx context.Context, k string, v []byte) error { + if err := s.db.Set(k, string(v), 0).Err(); err != nil { + return err + } + return nil +} + +// Get is equivalent to Redis `GET key` command. +// It returns redis.Nil error when key does not exist. +func (s *Store) Get(ctx context.Context, k string) (bool, []byte, error) { + v, err := s.db.Get(k).Result() + if errors.Is(err, redis.Nil) { + return false, nil, nil + } else if err != nil { + return false, nil, err + } + return true, []byte(v), nil +} + +// Close closes the client, releasing any open resources. +// +// It is rare to Close a Client, as the Client is meant to be +// long-lived and shared between many goroutines. +func (s Store) Close(ctx context.Context) error { + return s.db.Close() +} diff --git a/internal/kv/store.go b/internal/kv/store.go new file mode 100644 index 000000000..4f22942ee --- /dev/null +++ b/internal/kv/store.go @@ -0,0 +1,10 @@ +package kv + +import "context" + +// Store specifies a key value storage interface. +type Store interface { + Set(ctx context.Context, key string, value []byte) error + Get(ctx context.Context, key string) (keyExists bool, value []byte, err error) + Close(ctx context.Context) error +} diff --git a/internal/sessions/cache/cache_store.go b/internal/sessions/cache/cache_store.go index 2c9748be4..31ab91e79 100644 --- a/internal/sessions/cache/cache_store.go +++ b/internal/sessions/cache/cache_store.go @@ -1,14 +1,12 @@ package cache // import "github.com/pomerium/pomerium/internal/sessions/cache" import ( - "context" "errors" "fmt" "net/http" - "github.com/golang/groupcache" - "github.com/pomerium/pomerium/internal/encoding" + "github.com/pomerium/pomerium/internal/grpc/cache/client" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/sessions" ) @@ -16,83 +14,69 @@ import ( var _ sessions.SessionStore = &Store{} var _ sessions.SessionLoader = &Store{} -const ( - defaultQueryParamKey = "ati" -) - -// Store implements the session store interface using a distributed cache. +// Store implements the session store interface using a cache service. type Store struct { - name string - encoder encoding.Marshaler - decoder encoding.Unmarshaler - - cache *groupcache.Group + cache client.Cacher + encoder encoding.MarshalUnmarshaler + queryParam string wrappedStore sessions.SessionStore } -// defaultCacheSize is ~10MB -var defaultCacheSize int64 = 10 << 20 - -// NewStore creates a new session store built on the distributed caching library -// groupcache. On a cache miss, the cache store attempts to fallback to another -// SessionStore implementation. -func NewStore(enc encoding.MarshalUnmarshaler, wrappedStore sessions.SessionStore, name string) *Store { - store := &Store{ - name: name, - encoder: enc, - decoder: enc, - wrappedStore: wrappedStore, - } - - store.cache = groupcache.NewGroup(name, defaultCacheSize, groupcache.GetterFunc( - func(ctx context.Context, id string, dest groupcache.Sink) error { - // fill the cache with session set as part of the request - // context set previously as part of SaveSession. - b := fromContext(ctx) - if len(b) == 0 { - return fmt.Errorf("sessions/cache: cannot fill key %s from ctx", id) - } - if err := dest.SetBytes(b); err != nil { - return fmt.Errorf("sessions/cache: sink error %w", err) - } - return nil - }, - )) - - return store +// Options represent cache store's available configurations. +type Options struct { + Cache client.Cacher + Encoder encoding.MarshalUnmarshaler + QueryParam string + WrappedStore sessions.SessionStore } -// LoadSession implements SessionLoaders's LoadSession method for cache store. +var defaultOptions = &Options{ + QueryParam: "cache_store_key", +} + +// NewStore creates a new cache +func NewStore(o *Options) *Store { + if o.QueryParam == "" { + o.QueryParam = defaultOptions.QueryParam + } + return &Store{ + cache: o.Cache, + encoder: o.Encoder, + queryParam: o.QueryParam, + wrappedStore: o.WrappedStore, + } +} + +// LoadSession looks for a preset query parameter in the request body +// representing the key to lookup from the cache. func (s *Store) LoadSession(r *http.Request) (*sessions.State, error) { // look for our cache's key in the default query param - sessionID := r.URL.Query().Get(defaultQueryParamKey) + sessionID := r.URL.Query().Get(s.queryParam) if sessionID == "" { - // if unset, fallback to default cache store - log.FromRequest(r).Debug().Msg("sessions/cache: no query param, trying wrapped loader") - return s.wrappedStore.LoadSession(r) + return nil, sessions.ErrNoSessionFound } - - var b []byte - if err := s.cache.Get(r.Context(), sessionID, groupcache.AllocatingByteSliceSink(&b)); err != nil { - log.FromRequest(r).Debug().Err(err).Msg("sessions/cache: miss, trying wrapped loader") - return s.wrappedStore.LoadSession(r) + exists, val, err := s.cache.Get(r.Context(), sessionID) + if err != nil { + log.FromRequest(r).Debug().Msg("sessions/cache: miss, trying wrapped loader") + return nil, err + } + if !exists { + return nil, sessions.ErrNoSessionFound } var session sessions.State - if err := s.decoder.Unmarshal(b, &session); err != nil { + if err := s.encoder.Unmarshal(val, &session); err != nil { log.FromRequest(r).Error().Err(err).Msg("sessions/cache: unmarshal") return nil, sessions.ErrMalformed } return &session, nil } -// ClearSession implements SessionStore's ClearSession for the cache store. -// Since group cache has no explicit eviction, we just call the wrapped -// store's ClearSession method here. +// ClearSession clears the session from the wrapped store. func (s *Store) ClearSession(w http.ResponseWriter, r *http.Request) { s.wrappedStore.ClearSession(w, r) } -// SaveSession implements SessionStore's SaveSession method for cache store. +// SaveSession saves the session to the cache, and wrapped store. func (s *Store) SaveSession(w http.ResponseWriter, r *http.Request, x interface{}) error { err := s.wrappedStore.SaveSession(w, r, x) if err != nil { @@ -101,7 +85,7 @@ func (s *Store) SaveSession(w http.ResponseWriter, r *http.Request, x interface{ state, ok := x.(*sessions.State) if !ok { - return errors.New("internal/sessions: cannot cache non state type") + return errors.New("sessions/cache: cannot cache non state type") } data, err := s.encoder.Marshal(&state) @@ -109,23 +93,5 @@ func (s *Store) SaveSession(w http.ResponseWriter, r *http.Request, x interface{ return fmt.Errorf("sessions/cache: marshal %w", err) } - ctx := newContext(r.Context(), data) - var b []byte - return s.cache.Get(ctx, state.AccessTokenID, groupcache.AllocatingByteSliceSink(&b)) -} - -var sessionCtxKey = &contextKey{"PomeriumCachedSessionBytes"} - -type contextKey struct { - name string -} - -func newContext(ctx context.Context, b []byte) context.Context { - ctx = context.WithValue(ctx, sessionCtxKey, b) - return ctx -} - -func fromContext(ctx context.Context) []byte { - b, _ := ctx.Value(sessionCtxKey).([]byte) - return b + return s.cache.Set(r.Context(), state.AccessTokenID, data) } diff --git a/internal/sessions/cache/cache_store_test.go b/internal/sessions/cache/cache_store_test.go index 6949338a0..436b0784b 100644 --- a/internal/sessions/cache/cache_store_test.go +++ b/internal/sessions/cache/cache_store_test.go @@ -1,7 +1,8 @@ package cache import ( - "fmt" + "context" + "errors" "net/http" "net/http/httptest" "testing" @@ -9,125 +10,188 @@ import ( "github.com/google/go-cmp/cmp" "github.com/pomerium/pomerium/internal/cryptutil" + "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/ecjson" + mock_encoder "github.com/pomerium/pomerium/internal/encoding/mock" + "github.com/pomerium/pomerium/internal/grpc/cache/client" "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions/cookie" + "github.com/pomerium/pomerium/internal/sessions/mock" "gopkg.in/square/go-jose.v2/jwt" ) -func testAuthorizer(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := sessions.FromContext(r.Context()) - if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - next.ServeHTTP(w, r) - }) +type mockCache struct { + Key string + KeyExists bool + Value []byte + Err error } -func TestVerifier(t *testing.T) { - fnh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - fmt.Fprint(w, http.StatusText(http.StatusOK)) - w.WriteHeader(http.StatusOK) - }) +func (mc *mockCache) Get(ctx context.Context, key string) (keyExists bool, value []byte, err error) { + return mc.KeyExists, mc.Value, mc.Err +} +func (mc *mockCache) Set(ctx context.Context, key string, value []byte) error { + return mc.Err +} +func (mc *mockCache) Close() error { + return mc.Err +} + +func TestNewStore(t *testing.T) { tests := []struct { - name string - skipSave bool - cacheSize int64 - state sessions.State + name string + Options *Options + State *sessions.State - wantBody string - wantStatus int + wantErr bool + wantLoadErr bool + wantStatus int }{ - {"good", false, 1 << 10, sessions.State{AccessTokenID: cryptutil.NewBase64Key(), Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, http.StatusText(http.StatusOK), http.StatusOK}, - {"expired", false, 1 << 10, sessions.State{AccessTokenID: cryptutil.NewBase64Key(), Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: validation failed, token is expired (exp)\n", http.StatusUnauthorized}, - {"empty", false, 1 << 10, sessions.State{AccessTokenID: "", Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized}, - {"miss", true, 1 << 10, sessions.State{AccessTokenID: cryptutil.NewBase64Key(), Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized}, - {"cache eviction", false, 1, sessions.State{AccessTokenID: cryptutil.NewBase64Key(), Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, "internal/sessions: session is not found\n", http.StatusUnauthorized}, + {"simple good", + &Options{ + Cache: &mockCache{}, + WrappedStore: &mock.Store{}, + Encoder: mock_encoder.Encoder{MarshalResponse: []byte("ok")}, + }, + &sessions.State{Email: "user@domain.com", User: "user"}, + false, false, + http.StatusOK}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - defaultCacheSize = tt.cacheSize - cipher, err := cryptutil.NewAEADCipherFromBase64(cryptutil.NewBase64Key()) - encoder := ecjson.New(cipher) - if err != nil { - t.Fatal(err) - } - cs, err := cookie.NewStore(&cookie.Options{Name: t.Name()}, encoder) - if err != nil { - t.Fatal(err) - } - cacheStore := NewStore(encoder, cs, t.Name()) + got := NewStore(tt.Options) - r := httptest.NewRequest(http.MethodGet, "/", nil) - q := r.URL.Query() - - q.Set(defaultQueryParamKey, tt.state.AccessTokenID) - r.URL.RawQuery = q.Encode() - r.Header.Set("Accept", "application/json") + r := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() - got := sessions.RetrieveSession(cacheStore)(testAuthorizer((fnh))) - - if !tt.skipSave { - cacheStore.SaveSession(w, r, &tt.state) + if err := got.SaveSession(w, r, tt.State); (err != nil) != tt.wantErr { + t.Errorf("NewStore.SaveSession() error = %v, wantErr %v", err, tt.wantErr) } - for i := 1; i <= 10; i++ { - s := tt.state - s.AccessTokenID = cryptutil.NewBase64Key() - cacheStore.SaveSession(w, r, s) - } + r = httptest.NewRequest("GET", "/", nil) + w = httptest.NewRecorder() - got.ServeHTTP(w, r) - - gotBody := w.Body.String() - gotStatus := w.Result().StatusCode - - if diff := cmp.Diff(gotBody, tt.wantBody); diff != "" { - t.Errorf("RetrieveSession() = %v", diff) - } - if diff := cmp.Diff(gotStatus, tt.wantStatus); diff != "" { - t.Errorf("RetrieveSession() = %v", diff) + got.ClearSession(w, r) + status := w.Result().StatusCode + if diff := cmp.Diff(status, tt.wantStatus); diff != "" { + t.Errorf("ClearSession() = %v", diff) } }) } } func TestStore_SaveSession(t *testing.T) { - + cipher, err := cryptutil.NewAEADCipherFromBase64(cryptutil.NewBase64Key()) + encoder := ecjson.New(cipher) + if err != nil { + t.Fatal(err) + } + cs, err := cookie.NewStore(&cookie.Options{ + Name: "_pomerium", + }, encoder) + if err != nil { + t.Fatal(err) + } tests := []struct { name string + Options *Options + x interface{} wantErr bool }{ - {"good", &sessions.State{AccessTokenID: cryptutil.NewBase64Key(), Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, false}, - {"bad type", "bad type!", true}, + {"good", &Options{Cache: &mockCache{}, WrappedStore: cs, Encoder: mock_encoder.Encoder{MarshalResponse: []byte("ok")}}, &sessions.State{AccessTokenID: cryptutil.NewBase64Key(), Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, false}, + {"encoder error", &Options{Cache: &mockCache{}, WrappedStore: cs, Encoder: mock_encoder.Encoder{MarshalError: errors.New("err")}}, &sessions.State{AccessTokenID: cryptutil.NewBase64Key(), Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, true}, + {"good", &Options{Cache: &mockCache{}, WrappedStore: &mock.Store{SaveError: errors.New("err")}}, &sessions.State{AccessTokenID: cryptutil.NewBase64Key(), Email: "user@pomerium.io", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}, true}, + {"bad type", &Options{Cache: &mockCache{}, WrappedStore: cs, Encoder: mock_encoder.Encoder{MarshalError: errors.New("err")}}, "bad type!", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cipher, err := cryptutil.NewAEADCipherFromBase64(cryptutil.NewBase64Key()) - encoder := ecjson.New(cipher) - if err != nil { - t.Fatal(err) + o := tt.Options + if o.WrappedStore == nil { + o.WrappedStore = cs + } - cs, err := cookie.NewStore(&cookie.Options{ - Name: "_pomerium", - }, encoder) - if err != nil { - t.Fatal(err) - } - cacheStore := NewStore(encoder, cs, t.Name()) + cacheStore := NewStore(tt.Options) r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Set("Accept", "application/json") w := httptest.NewRecorder() - if err := cacheStore.SaveSession(w, r, tt.x); (err != nil) != tt.wantErr { t.Errorf("Store.SaveSession() error = %v, wantErr %v", err, tt.wantErr) } + + }) + } +} + +func TestStore_LoadSession(t *testing.T) { + key := cryptutil.NewBase64Key() + tests := []struct { + name string + state *sessions.State + cache client.Cacher + encoder encoding.MarshalUnmarshaler + queryParam string + wrappedStore sessions.SessionStore + wantErr bool + }{ + {"good", + &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}, + &mockCache{KeyExists: true}, + mock_encoder.Encoder{MarshalResponse: []byte("ok")}, + defaultOptions.QueryParam, + &mock.Store{Session: &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}}, + false}, + {"missing param with key", + &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}, + &mockCache{KeyExists: true}, + mock_encoder.Encoder{MarshalResponse: []byte("ok")}, + "bad_query", + &mock.Store{Session: &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}}, + true}, + {"doesn't exist", + &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}, + &mockCache{KeyExists: false}, + mock_encoder.Encoder{MarshalResponse: []byte("ok")}, + defaultOptions.QueryParam, + &mock.Store{Session: &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}}, + true}, + {"retrieval error", + &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}, + &mockCache{Err: errors.New("err")}, + mock_encoder.Encoder{MarshalResponse: []byte("ok")}, + defaultOptions.QueryParam, + &mock.Store{Session: &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}}, + true}, + {"unmarshal failure", + &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}, + &mockCache{KeyExists: true}, + mock_encoder.Encoder{UnmarshalError: errors.New("err")}, + defaultOptions.QueryParam, + &mock.Store{Session: &sessions.State{AccessTokenID: key, Email: "user@pomerium.io"}}, + true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Store{ + cache: tt.cache, + encoder: tt.encoder, + queryParam: tt.queryParam, + wrappedStore: tt.wrappedStore, + } + + r := httptest.NewRequest(http.MethodGet, "/", nil) + q := r.URL.Query() + + q.Set(defaultOptions.QueryParam, tt.state.AccessTokenID) + r.URL.RawQuery = q.Encode() + r.Header.Set("Accept", "application/json") + + _, err := s.LoadSession(r) + if (err != nil) != tt.wantErr { + t.Errorf("Store.LoadSession() error = %v, wantErr %v", err, tt.wantErr) + return + } }) } } diff --git a/internal/urlutil/query_params.go b/internal/urlutil/query_params.go index 723753a2b..9847a510e 100644 --- a/internal/urlutil/query_params.go +++ b/internal/urlutil/query_params.go @@ -14,6 +14,8 @@ const ( QuerySessionEncrypted = "pomerium_session_encrypted" QueryRedirectURI = "pomerium_redirect_uri" QueryRefreshToken = "pomerium_refresh_token" + QueryAccessTokenID = "pomerium_session_access_token_id" + QueryAudience = "pomerium_session_audience" ) // URL signature based query params used for verifying the authenticity of a URL. diff --git a/proxy/clients/authorize_client_test.go b/proxy/clients/authorize_client_test.go deleted file mode 100644 index 0db1bdc1c..000000000 --- a/proxy/clients/authorize_client_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package clients - -import ( - "context" - "net/url" - "strings" - "testing" - - "github.com/golang/mock/gomock" - - "github.com/pomerium/pomerium/internal/sessions" - "github.com/pomerium/pomerium/proto/authorize" - mock "github.com/pomerium/pomerium/proto/authorize/mock_authorize" -) - -func TestAuthorizeGRPC_Authorize(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - client := mock.NewMockAuthorizerClient(ctrl) - client.EXPECT().Authorize( - gomock.Any(), - gomock.Any(), - ).Return(&authorize.AuthorizeReply{IsValid: true}, nil).AnyTimes() - - tests := []struct { - name string - route string - s *sessions.State - want bool - wantErr bool - }{ - {"good", "hello.pomerium.io", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false}, - {"impersonate request", "hello.pomerium.io", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io", ImpersonateEmail: "other@other.example"}, true, false}, - {"session cannot be nil", "hello.pomerium.io", nil, false, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - a := &AuthorizeGRPC{client: client} - got, err := a.Authorize(context.Background(), tt.route, tt.s) - if (err != nil) != tt.wantErr { - t.Errorf("AuthorizeGRPC.Authorize() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("AuthorizeGRPC.Authorize() = %v, want %v", got, tt.want) - } - }) - } -} -func TestAuthorizeGRPC_IsAdmin(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - client := mock.NewMockAuthorizerClient(ctrl) - client.EXPECT().IsAdmin( - gomock.Any(), - gomock.Any(), - ).Return(&authorize.IsAdminReply{IsAdmin: true}, nil).AnyTimes() - - tests := []struct { - name string - s *sessions.State - want bool - wantErr bool - }{ - {"good", &sessions.State{User: "admin@pomerium.io", Email: "admin@pomerium.io"}, true, false}, - {"session cannot be nil", nil, false, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - a := &AuthorizeGRPC{client: client} - got, err := a.IsAdmin(context.Background(), tt.s) - if (err != nil) != tt.wantErr { - t.Errorf("AuthorizeGRPC.IsAdmin() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("AuthorizeGRPC.IsAdmin() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestNewGRPC(t *testing.T) { - t.Parallel() - tests := []struct { - name string - opts *Options - wantErr bool - wantErrStr string - wantTarget string - }{ - {"no shared secret", &Options{}, true, "proxy/authenticator: grpc client requires shared secret", ""}, - {"empty connection", &Options{Addr: nil, SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""}, - {"both internal and addr empty", &Options{Addr: nil, InternalAddr: nil, SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""}, - {"addr with port", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, SharedSecret: "shh"}, false, "", "localhost.example:8443"}, - {"addr without port", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: "shh"}, false, "", "localhost.example:443"}, - {"internal addr with port", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, SharedSecret: "shh"}, false, "", "localhost.example:8443"}, - {"internal addr without port", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: "shh"}, false, "", "localhost.example:443"}, - {"cert override", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh"}, false, "", "localhost.example:443"}, - {"custom ca", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZrQ0ZBWHhneFg5K0hjWlBVVVBEK0laV0NGNUEvVTdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1FVXgKQ3pBSkJnTlZCQVlUQWtGVk1STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbApjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTk1Ua3dNakk0TVRnMU1EQTNXaGNOTWprd01qSTFNVGcxCk1EQTNXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNBd0tVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUUKQ2d3WVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBOVRFMEFiaTdnMHhYeURkVUtEbDViNTBCT05ZVVVSc3F2THQrSWkwdlpjMzRRTHhOClJrT0hrOFZEVUgzcUt1N2UrNGVubUdLVVNUdzRPNFlkQktiSWRJTFpnb3o0YitNL3FVOG5adVpiN2pBVTdOYWkKajMzVDVrbXB3L2d4WHNNUzNzdUpXUE1EUDB3Z1BUZUVRK2J1bUxVWmpLdUVIaWNTL0l5dmtaVlBzRlE4NWlaUwpkNXE2a0ZGUUdjWnFXeFg0dlhDV25Sd3E3cHY3TThJd1RYc1pYSVRuNXB5Z3VTczNKb29GQkg5U3ZNTjRKU25GCmJMK0t6ekduMy9ScXFrTXpMN3FUdkMrNWxVT3UxUmNES21mZXBuVGVaN1IyVnJUQm42NndWMjVHRnBkSDIzN00KOXhJVkJrWEd1U2NvWHVPN1lDcWFrZkt6aXdoRTV4UmRaa3gweXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFCaHRWUEI0OCs4eFZyVmRxM1BIY3k5QkxtVEtrRFl6N2Q0ODJzTG1HczBuVUdGSTFZUDdmaFJPV3ZxCktCTlpkNEI5MUpwU1NoRGUrMHpoNno4WG5Ha01mYnRSYWx0NHEwZ3lKdk9hUWhqQ3ZCcSswTFk5d2NLbXpFdnMKcTRiNUZ5NXNpRUZSekJLTmZtTGwxTTF2cW1hNmFCVnNYUUhPREdzYS83dE5MalZ2ay9PYm52cFg3UFhLa0E3cQpLMTQvV0tBRFBJWm9mb00xMzB4Q1RTYXVpeXROajlnWkx1WU9leEZhblVwNCt2MHBYWS81OFFSNTk2U0ROVTlKClJaeDhwTzBTaUYvZXkxVUZXbmpzdHBjbTQzTFVQKzFwU1hFeVhZOFJrRTI2QzNvdjNaTFNKc2pMbC90aXVqUlgKZUJPOWorWDdzS0R4amdtajBPbWdpVkpIM0YrUAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}, false, "", "localhost.example:443"}, - {"bad ca encoding", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "^"}, true, "", "localhost.example:443"}, - {"custom ca file", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt"}, false, "", "localhost.example:443"}, - {"bad custom ca file", &Options{Addr: nil, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt2"}, true, "", "localhost.example:443"}, - {"valid with insecure", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, SharedSecret: "shh", WithInsecure: true}, false, "", "localhost.example:8443"}, - {"valid client round robin", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example:8443"}, SharedSecret: "shh", ClientDNSRoundRobin: true}, false, "", "dns:///localhost.example:8443"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := NewGRPCAuthorizeClient(tt.opts) - if (err != nil) != tt.wantErr { - t.Errorf("NewGRPCAuthorizeClient() error = %v, wantErr %v", err, tt.wantErr) - if !strings.EqualFold(err.Error(), tt.wantErrStr) { - t.Errorf("NewGRPCAuthorizeClient() error = %v did not contain wantErr %v", err, tt.wantErrStr) - } - } - if got != nil && got.Conn.Target() != tt.wantTarget { - t.Errorf("NewGRPCAuthorizeClient() target = %v expected %v", got.Conn.Target(), tt.wantTarget) - - } - }) - } -} diff --git a/proxy/clients/clients_test.go b/proxy/clients/clients_test.go deleted file mode 100644 index 0157acd50..000000000 --- a/proxy/clients/clients_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package clients // import "github.com/pomerium/pomerium/proxy/clients" - -import ( - "context" - "testing" - "time" - - "google.golang.org/grpc" -) - -func Test_grpcTimeoutInterceptor(t *testing.T) { - - mockInvoker := func(sleepTime time.Duration, wantFail bool) grpc.UnaryInvoker { - return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { - time.Sleep(sleepTime) - deadline, ok := ctx.Deadline() - if !ok { - t.Fatal("No deadline set") - } - - now := time.Now() - - if ok && now.After(deadline) && !wantFail { - t.Errorf("Deadline exceeded, but should not have. now=%v, deadline=%v", now, deadline) - } else if now.Before(deadline) && wantFail { - t.Errorf("Deadline not exceeded, but should have. now=%v, deadline=%v", now, deadline) - } - return nil - } - } - - timeOut := 300 * time.Millisecond - to := grpcTimeoutInterceptor(timeOut) - - to(context.Background(), "test", nil, nil, nil, mockInvoker(timeOut*2, true)) - to(context.Background(), "test", nil, nil, nil, mockInvoker(timeOut/2, false)) - -} diff --git a/proxy/forward_auth_test.go b/proxy/forward_auth_test.go index a09bed4ac..cb9aa1f85 100644 --- a/proxy/forward_auth_test.go +++ b/proxy/forward_auth_test.go @@ -14,11 +14,11 @@ import ( "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/mock" + "github.com/pomerium/pomerium/internal/grpc/authorize/client" "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/sessions" mstore "github.com/pomerium/pomerium/internal/sessions/mock" "github.com/pomerium/pomerium/internal/urlutil" - "github.com/pomerium/pomerium/proxy/clients" ) func TestProxy_ForwardAuth(t *testing.T) { @@ -38,33 +38,33 @@ func TestProxy_ForwardAuth(t *testing.T) { cipher encoding.MarshalUnmarshaler sessionStore sessions.SessionStore - authorizer clients.Authorizer + authorizer client.Authorizer wantStatus int wantBody string }{ - {"good redirect not required", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK, "Access to some.domain.example is allowed."}, - {"good verify only, no redirect", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK, ""}, - {"bad claim", opts, nil, http.MethodGet, nil, nil, "/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{LoadError: sessions.ErrInvalidAudience}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, invalid audience claim (aud)\"}\n"}, - {"bad naked domain uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "a.naked.domain", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: a.naked.domain url does contain a valid scheme\"}\n"}, - {"bad naked domain uri verify only", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "a.naked.domain", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: a.naked.domain url does contain a valid scheme\"}\n"}, - {"bad empty verification uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", " ", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: %20 url does contain a valid scheme\"}\n"}, - {"bad empty verification uri verify only", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", " ", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: %20 url does contain a valid scheme\"}\n"}, - {"not authorized", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden, "{\"Status\":403,\"Error\":\"Forbidden: user@test.example is not authorized for some.domain.example\"}\n"}, - {"not authorized verify endpoint", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden, "{\"Status\":403,\"Error\":\"Forbidden: user@test.example is not authorized for some.domain.example\"}\n"}, - {"not authorized expired, redirect to auth", opts, sessions.ErrExpired, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusFound, ""}, - {"not authorized expired, don't redirect!", opts, sessions.ErrExpired, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, token is expired (exp)\"}\n"}, - {"not authorized because of error", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeError: errors.New("authz error")}, http.StatusInternalServerError, "{\"Status\":500,\"Error\":\"Internal Server Error: authz error\"}\n"}, - {"not authorized expired, do not redirect to auth", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, token is expired (exp)\"}\n"}, - {"not authorized, bad audience request uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Audience: []string{"not.domain.example"}, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, invalid audience claim (aud)\"}\n"}, - {"not authorized, bad audience verify uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://fwdauth.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Audience: []string{"some.domain.example"}, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, invalid audience claim (aud)\"}\n"}, + {"good redirect not required", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusOK, "Access to some.domain.example is allowed."}, + {"good verify only, no redirect", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusOK, ""}, + {"bad claim", opts, nil, http.MethodGet, nil, nil, "/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{LoadError: sessions.ErrInvalidAudience}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, invalid audience claim (aud)\"}\n"}, + {"bad naked domain uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "a.naked.domain", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: a.naked.domain url does contain a valid scheme\"}\n"}, + {"bad naked domain uri verify only", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "a.naked.domain", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: a.naked.domain url does contain a valid scheme\"}\n"}, + {"bad empty verification uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", " ", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: %20 url does contain a valid scheme\"}\n"}, + {"bad empty verification uri verify only", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", " ", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: %20 url does contain a valid scheme\"}\n"}, + {"not authorized", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden, "{\"Status\":403,\"Error\":\"Forbidden: user@test.example is not authorized for some.domain.example\"}\n"}, + {"not authorized verify endpoint", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden, "{\"Status\":403,\"Error\":\"Forbidden: user@test.example is not authorized for some.domain.example\"}\n"}, + {"not authorized expired, redirect to auth", opts, sessions.ErrExpired, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusFound, ""}, + {"not authorized expired, don't redirect!", opts, sessions.ErrExpired, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, token is expired (exp)\"}\n"}, + {"not authorized because of error", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeError: errors.New("authz error")}, http.StatusInternalServerError, "{\"Status\":500,\"Error\":\"Internal Server Error: authz error\"}\n"}, + {"not authorized expired, do not redirect to auth", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, token is expired (exp)\"}\n"}, + {"not authorized, bad audience request uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Audience: []string{"not.domain.example"}, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, invalid audience claim (aud)\"}\n"}, + {"not authorized, bad audience verify uri", opts, nil, http.MethodGet, nil, nil, "https://some.domain.example/", "https://fwdauth.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Audience: []string{"some.domain.example"}, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, "{\"Status\":401,\"Error\":\"Unauthorized: internal/sessions: validation failed, invalid audience claim (aud)\"}\n"}, // traefik - {"good traefik callback", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: "https://some.domain.example?" + urlutil.QuerySessionEncrypted + "=" + goodEncryptionString}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, - {"bad traefik callback bad session", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: "https://some.domain.example?" + urlutil.QuerySessionEncrypted + "=" + goodEncryptionString + "garbage"}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, - {"bad traefik callback bad url", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: urlutil.QuerySessionEncrypted + ""}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"good traefik callback", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: "https://some.domain.example?" + urlutil.QuerySessionEncrypted + "=" + goodEncryptionString}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, + {"bad traefik callback bad session", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: "https://some.domain.example?" + urlutil.QuerySessionEncrypted + "=" + goodEncryptionString + "garbage"}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"bad traefik callback bad url", opts, nil, http.MethodGet, map[string]string{httputil.HeaderForwardedURI: urlutil.QuerySessionEncrypted + ""}, nil, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, // nginx - {"good nginx callback redirect", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString}, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, - {"good nginx callback set session okay but return unauthorized", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString}, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, ""}, - {"bad nginx callback failed to set session", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString + "nope"}, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"good nginx callback redirect", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString}, "https://some.domain.example/", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, + {"good nginx callback set session okay but return unauthorized", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString}, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusUnauthorized, ""}, + {"bad nginx callback failed to set session", opts, nil, http.MethodGet, nil, map[string]string{urlutil.QueryRedirectURI: "https://some.domain.example/", urlutil.QuerySessionEncrypted: goodEncryptionString + "nope"}, "https://some.domain.example/verify", "https://some.domain.example", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/proxy/handlers_test.go b/proxy/handlers_test.go index f05c69b15..c8f301758 100644 --- a/proxy/handlers_test.go +++ b/proxy/handlers_test.go @@ -17,10 +17,10 @@ import ( "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/mock" + "github.com/pomerium/pomerium/internal/grpc/authorize/client" "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/urlutil" - "github.com/pomerium/pomerium/proxy/clients" "github.com/google/go-cmp/cmp" "gopkg.in/square/go-jose.v2/jwt" @@ -74,15 +74,15 @@ func TestProxy_UserDashboard(t *testing.T) { method string cipher encoding.MarshalUnmarshaler session sessions.SessionStore - authorizer clients.Authorizer + authorizer client.Authorizer wantAdminForm bool wantStatus int }{ - {"good", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{}, false, http.StatusOK}, - {"session context error", errors.New("error"), opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{}, false, http.StatusInternalServerError}, - {"want admin form good admin authorization", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{IsAdminResponse: true}, true, http.StatusOK}, - {"is admin but authorization fails", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{IsAdminError: errors.New("err")}, false, http.StatusInternalServerError}, + {"good", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{}, false, http.StatusOK}, + {"session context error", errors.New("error"), opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{}, false, http.StatusInternalServerError}, + {"want admin form good admin authorization", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{IsAdminResponse: true}, true, http.StatusOK}, + {"is admin but authorization fails", nil, opts, http.MethodGet, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{IsAdminError: errors.New("err")}, false, http.StatusInternalServerError}, } for _, tt := range tests { @@ -133,15 +133,15 @@ func TestProxy_Impersonate(t *testing.T) { csrf string cipher encoding.MarshalUnmarshaler sessionStore sessions.SessionStore - authorizer clients.Authorizer + authorizer client.Authorizer wantStatus int }{ - {"good", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound}, - {"good", false, opts, errors.New("error"), http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError}, - {"session load error", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{LoadError: errors.New("err"), Session: &sessions.State{Email: "user@test.example"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound}, - {"non admin users rejected", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), Email: "user@test.example"}}, clients.MockAuthorize{IsAdminResponse: false}, http.StatusForbidden}, - {"non admin users rejected on error", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), Email: "user@test.example"}}, clients.MockAuthorize{IsAdminResponse: true, IsAdminError: errors.New("err")}, http.StatusInternalServerError}, - {"groups", false, opts, nil, http.MethodPost, "user@blah.com", "group1,group2", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), Email: "user@test.example"}}, clients.MockAuthorize{IsAdminResponse: true}, http.StatusFound}, + {"good", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true}, http.StatusFound}, + {"good", false, opts, errors.New("error"), http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true}, http.StatusInternalServerError}, + {"session load error", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{LoadError: errors.New("err"), Session: &sessions.State{Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true}, http.StatusFound}, + {"non admin users rejected", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: false}, http.StatusForbidden}, + {"non admin users rejected on error", false, opts, nil, http.MethodPost, "user@blah.com", "", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true, IsAdminError: errors.New("err")}, http.StatusInternalServerError}, + {"groups", false, opts, nil, http.MethodPost, "user@blah.com", "group1,group2", "", &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), Email: "user@test.example"}}, client.MockAuthorize{IsAdminResponse: true}, http.StatusFound}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -242,16 +242,16 @@ func TestProxy_Callback(t *testing.T) { cipher encoding.MarshalUnmarshaler sessionStore sessions.SessionStore - authorizer clients.Authorizer + authorizer client.Authorizer wantStatus int wantBody string }{ - {"good", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, - {"good programmatic", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QueryIsProgrammatic: "true", urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, - {"bad decrypt", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: "KBEjQ9rnCxaAX-GOqexGw9ivEQURqts3zZ2mNGy0wnVa3SbtM399KlBq2nZ-9wM21FfsZX52er4jlmC7kPEKM3P7uZ41zR0zeys1-_74a5tQp-vsf1WXZfRsgVOuBcWPkMiWEoc379JFHxGDudp5VhU8B-dcQt4f3_PtLTHARkuH54io1Va2gNMq4Hiy8sQ1MPGCQeltH_JMzzdDpXdmdusWrXUvCGkba24muvAV06D8XRVJj6Iu9eK94qFnqcHc7wzziEbb8ADBues9dwbtb6jl8vMWz5rN6XvXqA5YpZv_MQZlsrO4oXFFQDevdgB84cX1tVbVu6qZvK_yQBZqzpOjWA9uIaoSENMytoXuWAlFO_sXjswfX8JTNdGwzB7qQRNPqxVG_sM_tzY3QhPm8zqwEzsXG5DokxZfVt2I5WJRUEovFDb4BnK9KFnnkEzLEdMudixVnXeGmTtycgJvoTeTCQRPfDYkcgJ7oKf4tGea-W7z5UAVa2RduJM9ZoM6YtJX7jgDm__PvvqcE0knJUF87XHBzdcOjoDF-CUze9xDJgNBlvPbJqVshKrwoqSYpePSDH9GUCNKxGequW3Ma8GvlFfhwd0rK6IZG-XWkyk0XSWQIGkDSjAvhB1wsOusCCguDjbpVZpaW5MMyTkmx68pl6qlIKT5UCcrVPl4ix5ZEj91mUDF0O1t04haD7VZuLVFXVGmqtFrBKI76sdYN-zkokaa1_chPRTyqMQFlqu_8LD6-RiK3UccGM-dEmnX72i91NP9F9OK0WJr9Cheup1C_P0mjqAO4Cb8oIHm0Oxz_mRqv5QbTGJtb3xwPLPuVjVCiE4gGBcuU2ixpSVf5HUF7y1KicVMCKiX9ATCBtg8sTdQZQnPEtHcHHAvdsnDVwev1LGfqA-Gdvg="}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, - {"bad save session", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{SaveError: errors.New("hi")}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, - {"bad base64", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: "^"}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, - {"malformed redirect", opts, http.MethodGet, "http", "example.com", "/", nil, nil, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"good", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, + {"good programmatic", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QueryIsProgrammatic: "true", urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, + {"bad decrypt", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: "KBEjQ9rnCxaAX-GOqexGw9ivEQURqts3zZ2mNGy0wnVa3SbtM399KlBq2nZ-9wM21FfsZX52er4jlmC7kPEKM3P7uZ41zR0zeys1-_74a5tQp-vsf1WXZfRsgVOuBcWPkMiWEoc379JFHxGDudp5VhU8B-dcQt4f3_PtLTHARkuH54io1Va2gNMq4Hiy8sQ1MPGCQeltH_JMzzdDpXdmdusWrXUvCGkba24muvAV06D8XRVJj6Iu9eK94qFnqcHc7wzziEbb8ADBues9dwbtb6jl8vMWz5rN6XvXqA5YpZv_MQZlsrO4oXFFQDevdgB84cX1tVbVu6qZvK_yQBZqzpOjWA9uIaoSENMytoXuWAlFO_sXjswfX8JTNdGwzB7qQRNPqxVG_sM_tzY3QhPm8zqwEzsXG5DokxZfVt2I5WJRUEovFDb4BnK9KFnnkEzLEdMudixVnXeGmTtycgJvoTeTCQRPfDYkcgJ7oKf4tGea-W7z5UAVa2RduJM9ZoM6YtJX7jgDm__PvvqcE0knJUF87XHBzdcOjoDF-CUze9xDJgNBlvPbJqVshKrwoqSYpePSDH9GUCNKxGequW3Ma8GvlFfhwd0rK6IZG-XWkyk0XSWQIGkDSjAvhB1wsOusCCguDjbpVZpaW5MMyTkmx68pl6qlIKT5UCcrVPl4ix5ZEj91mUDF0O1t04haD7VZuLVFXVGmqtFrBKI76sdYN-zkokaa1_chPRTyqMQFlqu_8LD6-RiK3UccGM-dEmnX72i91NP9F9OK0WJr9Cheup1C_P0mjqAO4Cb8oIHm0Oxz_mRqv5QbTGJtb3xwPLPuVjVCiE4gGBcuU2ixpSVf5HUF7y1KicVMCKiX9ATCBtg8sTdQZQnPEtHcHHAvdsnDVwev1LGfqA-Gdvg="}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"bad save session", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{SaveError: errors.New("hi")}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"bad base64", opts, http.MethodGet, "http", "example.com", "/", nil, map[string]string{urlutil.QuerySessionEncrypted: "^"}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"malformed redirect", opts, http.MethodGet, "http", "example.com", "/", nil, nil, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -383,16 +383,16 @@ func TestProxy_ProgrammaticCallback(t *testing.T) { cipher encoding.MarshalUnmarshaler sessionStore sessions.SessionStore - authorizer clients.Authorizer + authorizer client.Authorizer wantStatus int wantBody string }{ - {"good", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, - {"good programmatic", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QueryIsProgrammatic: "true", urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, - {"bad decrypt", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString + cryptutil.NewBase64Key()}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, - {"bad save session", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{SaveError: errors.New("hi")}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, - {"bad base64", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: "^"}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, - {"malformed redirect", opts, http.MethodGet, "http://pomerium.io/", nil, nil, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"good", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, + {"good programmatic", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QueryIsProgrammatic: "true", urlutil.QueryCallbackURI: "ok", urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusFound, ""}, + {"bad decrypt", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString + cryptutil.NewBase64Key()}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"bad save session", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: goodEncryptionString}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{SaveError: errors.New("hi")}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"bad base64", opts, http.MethodGet, "http://pomerium.io/", nil, map[string]string{urlutil.QuerySessionEncrypted: "^"}, &mock.Encoder{MarshalResponse: []byte("x")}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, + {"malformed redirect", opts, http.MethodGet, "http://pomerium.io/", nil, nil, &mock.Encoder{}, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Minute))}}, client.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/proxy/middleware.go b/proxy/middleware.go index b4c7cbd41..9e995d859 100644 --- a/proxy/middleware.go +++ b/proxy/middleware.go @@ -62,25 +62,32 @@ func (p *Proxy) refresh(ctx context.Context, w http.ResponseWriter, r *http.Requ // 1 - build a signed url to call refresh on authenticate service refreshURI := *p.authenticateRefreshURL q := refreshURI.Query() - q.Set("ati", s.AccessTokenID) // hash value points to parent token - q.Set("aud", urlutil.StripPort(r.Host)) // request's audience, this route + q.Set(urlutil.QueryAccessTokenID, s.AccessTokenID) // hash value points to parent token + q.Set(urlutil.QueryAudience, urlutil.StripPort(r.Host)) // request's audience, this route refreshURI.RawQuery = q.Encode() signedRefreshURL := urlutil.NewSignedURL(p.SharedKey, &refreshURI).String() // 2 - http call to authenticate service req, err := http.NewRequestWithContext(ctx, http.MethodGet, signedRefreshURL, nil) if err != nil { - return nil, fmt.Errorf("proxy: backend refresh: new request: %v", err) + return nil, fmt.Errorf("proxy: refresh request: %v", err) } + + req.Header.Set("X-Requested-With", "XmlHttpRequest") + req.Header.Set("Accept", "application/json") res, err := httputil.DefaultClient.Do(req) if err != nil { - return nil, fmt.Errorf("proxy: fetch %v: %w", signedRefreshURL, err) + return nil, fmt.Errorf("proxy: client err %s: %w", signedRefreshURL, err) } defer res.Body.Close() jwtBytes, err := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10)) if err != nil { return nil, err } + // auth couldn't refersh the session, delete the session and reload via 302 + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("proxy: backend refresh failed: %s", jwtBytes) + } // 3 - save refreshed session to the client's session store if err = p.sessionStore.SaveSession(w, r, jwtBytes); err != nil { @@ -99,10 +106,10 @@ func (p *Proxy) refresh(ctx context.Context, w http.ResponseWriter, r *http.Requ func (p *Proxy) redirectToSignin(w http.ResponseWriter, r *http.Request) error { s, err := sessions.FromContext(r.Context()) + p.sessionStore.ClearSession(w, r) if s != nil && err != nil && s.Programmatic { return httputil.NewError(http.StatusUnauthorized, err) } - p.sessionStore.ClearSession(w, r) signinURL := *p.authenticateSigninURL q := signinURL.Query() q.Set(urlutil.QueryRedirectURI, urlutil.GetAbsoluteURL(r).String()) diff --git a/proxy/middleware_test.go b/proxy/middleware_test.go index 166ad7d48..74f05aab9 100644 --- a/proxy/middleware_test.go +++ b/proxy/middleware_test.go @@ -10,11 +10,11 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/pomerium/pomerium/proxy/clients" "gopkg.in/square/go-jose.v2/jwt" "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/mock" + "github.com/pomerium/pomerium/internal/grpc/authorize/client" "github.com/pomerium/pomerium/internal/identity" "github.com/pomerium/pomerium/internal/sessions" mstore "github.com/pomerium/pomerium/internal/sessions/mock" @@ -29,28 +29,32 @@ func TestProxy_AuthenticateSession(t *testing.T) { }) tests := []struct { - name string - errOnFailure bool - session sessions.SessionStore - ctxError error - provider identity.Authenticator - encoder encoding.MarshalUnmarshaler - refreshURL string + name string + refreshRespStatus int + errOnFailure bool + session sessions.SessionStore + ctxError error + provider identity.Authenticator + encoder encoding.MarshalUnmarshaler + refreshURL string wantStatus int }{ - {"good", false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, nil, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK}, - {"invalid session", false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, errors.New("hi"), identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound}, - {"expired", false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK}, - {"expired and programmatic", false, &mstore.Store{Session: &sessions.State{Programmatic: true, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK}, - {"invalid session and programmatic", false, &mstore.Store{Session: &sessions.State{Programmatic: true, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, errors.New("hi"), identity.MockProvider{}, &mock.Encoder{}, "", http.StatusUnauthorized}, - {"expired and refreshed ok", false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK}, - {"expired and save failed", false, &mstore.Store{SaveError: errors.New("err"), Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound}, - {"expired and unmarshal failed", false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{UnmarshalError: errors.New("err")}, "", http.StatusFound}, + {"good", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, nil, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK}, + {"invalid session", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, errors.New("hi"), identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound}, + {"expired", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK}, + {"expired and programmatic", 200, false, &mstore.Store{Session: &sessions.State{Programmatic: true, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK}, + {"invalid session and programmatic", 200, false, &mstore.Store{Session: &sessions.State{Programmatic: true, Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, errors.New("hi"), identity.MockProvider{}, &mock.Encoder{}, "", http.StatusUnauthorized}, + {"expired and refreshed ok", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusOK}, + {"expired and save failed", 200, false, &mstore.Store{SaveError: errors.New("err"), Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound}, + {"expired and unmarshal failed", 200, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{UnmarshalError: errors.New("err")}, "", http.StatusFound}, + {"expired and malformed session", 200, false, &mstore.Store{Session: nil}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound}, + {"expired and refresh failed", 500, false, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute))}}, sessions.ErrExpired, identity.MockProvider{}, &mock.Encoder{}, "", http.StatusFound}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.refreshRespStatus) fmt.Fprintln(w, "REFRESH GOOD") })) defer ts.Close() @@ -94,17 +98,17 @@ func TestProxy_AuthorizeSession(t *testing.T) { tests := []struct { name string session sessions.SessionStore - authzClient clients.Authorizer + authzClient client.Authorizer ctxError error provider identity.Authenticator wantStatus int }{ - {"user is authorized", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, clients.MockAuthorize{AuthorizeResponse: true}, nil, identity.MockProvider{}, http.StatusOK}, - {"user is not authorized", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, clients.MockAuthorize{AuthorizeResponse: false}, nil, identity.MockProvider{}, http.StatusForbidden}, - {"ctx error", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, clients.MockAuthorize{AuthorizeResponse: true}, errors.New("hi"), identity.MockProvider{}, http.StatusInternalServerError}, - {"authz client error", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, clients.MockAuthorize{AuthorizeError: errors.New("err")}, nil, identity.MockProvider{}, http.StatusInternalServerError}, + {"user is authorized", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: true}, nil, identity.MockProvider{}, http.StatusOK}, + {"user is not authorized", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: false}, nil, identity.MockProvider{}, http.StatusForbidden}, + {"ctx error", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: true}, errors.New("hi"), identity.MockProvider{}, http.StatusInternalServerError}, + {"authz client error", &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeError: errors.New("err")}, nil, identity.MockProvider{}, http.StatusInternalServerError}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/proxy/proxy.go b/proxy/proxy.go index 3a491c6f1..e55864488 100755 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -17,6 +17,8 @@ import ( "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/jws" "github.com/pomerium/pomerium/internal/frontend" + "github.com/pomerium/pomerium/internal/grpc" + "github.com/pomerium/pomerium/internal/grpc/authorize/client" "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/middleware" @@ -27,7 +29,6 @@ import ( "github.com/pomerium/pomerium/internal/telemetry/metrics" "github.com/pomerium/pomerium/internal/tripper" "github.com/pomerium/pomerium/internal/urlutil" - "github.com/pomerium/pomerium/proxy/clients" ) const ( @@ -78,7 +79,7 @@ type Proxy struct { authorizeURL *url.URL - AuthorizeClient clients.Authorizer + AuthorizeClient client.Authorizer encoder encoding.Unmarshaler cookieOptions *cookie.Options @@ -151,17 +152,21 @@ func New(opts config.Options) (*Proxy, error) { metrics.AddPolicyCountCallback("proxy", func() int64 { return int64(len(opts.Policies)) }) - p.AuthorizeClient, err = clients.NewAuthorizeClient("grpc", - &clients.Options{ - Addr: p.authorizeURL, - OverrideCertificateName: opts.OverrideCertificateName, - SharedSecret: opts.SharedKey, - CA: opts.CA, - CAFile: opts.CAFile, - RequestTimeout: opts.GRPCClientTimeout, - ClientDNSRoundRobin: opts.GRPCClientDNSRoundRobin, - WithInsecure: opts.GRPCInsecure, - }) + + authzConn, err := grpc.NewGRPCClientConn(&grpc.Options{ + Addr: p.authorizeURL, + OverrideCertificateName: opts.OverrideCertificateName, + CA: opts.CA, + CAFile: opts.CAFile, + RequestTimeout: opts.GRPCClientTimeout, + ClientDNSRoundRobin: opts.GRPCClientDNSRoundRobin, + WithInsecure: opts.GRPCInsecure, + }) + if err != nil { + return nil, err + } + + p.AuthorizeClient, err = client.New(authzConn) return p, err }