internal/config: refactor option parsing

- authorize: build whitelist from policy's URLs instead of strings.
- internal/httputil: merged httputil and https package.
- internal/config: merged config and policy packages.
- internal/metrics: removed unused measure struct.
- proxy/clients: refactor Addr fields to be urls.
- proxy: remove unused extend deadline function.
- proxy: use handler middleware for reverse proxy leg.
- proxy: change the way websocket requests are made (route based).

General improvements
- omitted value from range in several cases where for loop could be simplified.
- added error checking to many tests.
- standardize url parsing.
- remove unnecessary return statements.

- proxy: add self-signed certificate support. #179
- proxy: add skip tls certificate verification. #179
- proxy: Refactor websocket support to be route based. #204
This commit is contained in:
Bobby DeSimone 2019-07-04 10:12:25 -07:00
parent 28efa3359b
commit 7558d5b0de
No known key found for this signature in database
GPG key ID: AEE4CF12FE86D07E
38 changed files with 1354 additions and 1079 deletions

View file

@ -3,6 +3,7 @@ package clients // import "github.com/pomerium/pomerium/proxy/clients"
import (
"context"
"fmt"
"net/url"
"reflect"
"strings"
"testing"
@ -23,8 +24,8 @@ func TestNew(t *testing.T) {
opts *Options
wantErr bool
}{
{"grpc good", "grpc", &Options{Addr: "test", InternalAddr: "intranet.local", SharedSecret: "secret"}, false},
{"grpc missing shared secret", "grpc", &Options{Addr: "test", InternalAddr: "intranet.local", SharedSecret: ""}, true},
{"grpc good", "grpc", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example"}, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: "secret"}, false},
{"grpc missing shared secret", "grpc", &Options{Addr: &url.URL{Scheme: "https", Host: "localhost.example"}, InternalAddr: &url.URL{Scheme: "https", Host: "localhost.example"}, SharedSecret: ""}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -211,15 +212,17 @@ func TestNewGRPC(t *testing.T) {
wantTarget string
}{
{"no shared secret", &Options{}, true, "proxy/authenticator: grpc client requires shared secret", ""},
{"empty connection", &Options{Addr: "", SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
{"both internal and addr empty", &Options{Addr: "", InternalAddr: "", SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
{"internal addr with port", &Options{Addr: "", InternalAddr: "intranet.local:8443", SharedSecret: "shh"}, false, "", "intranet.local:8443"},
{"internal addr without port", &Options{Addr: "", InternalAddr: "intranet.local", SharedSecret: "shh"}, false, "", "intranet.local:443"},
{"cert override", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh"}, false, "", "intranet.local:443"},
{"custom ca", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZrQ0ZBWHhneFg5K0hjWlBVVVBEK0laV0NGNUEvVTdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1FVXgKQ3pBSkJnTlZCQVlUQWtGVk1STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbApjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTk1Ua3dNakk0TVRnMU1EQTNXaGNOTWprd01qSTFNVGcxCk1EQTNXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNBd0tVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUUKQ2d3WVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBOVRFMEFiaTdnMHhYeURkVUtEbDViNTBCT05ZVVVSc3F2THQrSWkwdlpjMzRRTHhOClJrT0hrOFZEVUgzcUt1N2UrNGVubUdLVVNUdzRPNFlkQktiSWRJTFpnb3o0YitNL3FVOG5adVpiN2pBVTdOYWkKajMzVDVrbXB3L2d4WHNNUzNzdUpXUE1EUDB3Z1BUZUVRK2J1bUxVWmpLdUVIaWNTL0l5dmtaVlBzRlE4NWlaUwpkNXE2a0ZGUUdjWnFXeFg0dlhDV25Sd3E3cHY3TThJd1RYc1pYSVRuNXB5Z3VTczNKb29GQkg5U3ZNTjRKU25GCmJMK0t6ekduMy9ScXFrTXpMN3FUdkMrNWxVT3UxUmNES21mZXBuVGVaN1IyVnJUQm42NndWMjVHRnBkSDIzN00KOXhJVkJrWEd1U2NvWHVPN1lDcWFrZkt6aXdoRTV4UmRaa3gweXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFCaHRWUEI0OCs4eFZyVmRxM1BIY3k5QkxtVEtrRFl6N2Q0ODJzTG1HczBuVUdGSTFZUDdmaFJPV3ZxCktCTlpkNEI5MUpwU1NoRGUrMHpoNno4WG5Ha01mYnRSYWx0NHEwZ3lKdk9hUWhqQ3ZCcSswTFk5d2NLbXpFdnMKcTRiNUZ5NXNpRUZSekJLTmZtTGwxTTF2cW1hNmFCVnNYUUhPREdzYS83dE5MalZ2ay9PYm52cFg3UFhLa0E3cQpLMTQvV0tBRFBJWm9mb00xMzB4Q1RTYXVpeXROajlnWkx1WU9leEZhblVwNCt2MHBYWS81OFFSNTk2U0ROVTlKClJaeDhwTzBTaUYvZXkxVUZXbmpzdHBjbTQzTFVQKzFwU1hFeVhZOFJrRTI2QzNvdjNaTFNKc2pMbC90aXVqUlgKZUJPOWorWDdzS0R4amdtajBPbWdpVkpIM0YrUAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}, false, "", "intranet.local:443"},
{"bad ca encoding", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "^"}, true, "", "intranet.local:443"},
{"custom ca file", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt"}, false, "", "intranet.local:443"},
{"bad custom ca file", &Options{Addr: "", InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt2"}, true, "", "intranet.local:443"},
{"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"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -5,6 +5,7 @@ import (
"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"

View file

@ -7,15 +7,15 @@ import (
"errors"
"fmt"
"io/ioutil"
"net/url"
"strings"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/metrics"
"github.com/pomerium/pomerium/internal/middleware"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
)
const defaultGRPCPort = 443
@ -23,10 +23,10 @@ const defaultGRPCPort = 443
// Options contains options for connecting to a pomerium rpc service.
type Options struct {
// Addr is the location of the authenticate service. e.g. "service.corp.example:8443"
Addr string
Addr *url.URL
// InternalAddr is the internal (behind the ingress) address to use when
// making a connection. If empty, Addr is used.
InternalAddr string
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
// hosting name if it is set.
@ -45,16 +45,17 @@ func NewGRPCClientConn(opts *Options) (*grpc.ClientConn, error) {
if opts.SharedSecret == "" {
return nil, errors.New("proxy/clients: grpc client requires shared secret")
}
if opts.InternalAddr == nil && opts.Addr == nil {
return nil, errors.New("proxy/clients: connection address required")
}
grpcAuth := middleware.NewSharedSecretCred(opts.SharedSecret)
var connAddr string
if opts.InternalAddr != "" {
connAddr = opts.InternalAddr
if opts.InternalAddr != nil {
connAddr = opts.InternalAddr.Host
} else {
connAddr = opts.Addr
}
if connAddr == "" {
return nil, errors.New("proxy/clients: connection address required")
connAddr = opts.Addr.Host
}
// no colon exists in the connection string, assume one must be added manually
if !strings.Contains(connAddr, ":") {

View file

@ -9,12 +9,10 @@ import (
"time"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/policy"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates"
)
@ -345,7 +343,6 @@ func (p *Proxy) UserDashboard(w http.ResponseWriter, r *http.Request) {
CSRF: csrf.SessionID,
}
templates.New().ExecuteTemplate(w, "dashboard.html", t)
return
}
// Refresh redeems and extends an existing authenticated oidc session with
@ -366,8 +363,7 @@ func (p *Proxy) Refresh(w http.ResponseWriter, r *http.Request) {
return
}
// reject a refresh if it's been less than 5 minutes to prevent a bad actor
// trying to DOS the identity provider.
// reject a refresh if it's been less than the refresh cooldown to prevent abuse
if time.Since(iss) < p.refreshCooldown {
log.FromRequest(r).Error().Dur("cooldown", p.refreshCooldown).Err(err).Msg("proxy: refresh cooldown")
httpErr := &httputil.Error{
@ -467,22 +463,21 @@ func (p *Proxy) authenticate(w http.ResponseWriter, r *http.Request, s *sessions
if err != nil {
return fmt.Errorf("proxy: session refresh failed : %v", err)
}
err = p.sessionStore.SaveSession(w, r, s)
if err != nil {
if err := p.sessionStore.SaveSession(w, r, s); err != nil {
return fmt.Errorf("proxy: refresh failed : %v", err)
}
} else {
valid, err := p.AuthenticateClient.Validate(r.Context(), s.IDToken)
if err != nil || !valid {
return fmt.Errorf("proxy: session valid: %v : %v", valid, err)
return fmt.Errorf("proxy: session validate failed: %v : %v", valid, err)
}
}
return nil
}
// router attempts to find a route for a request. If a route is successfully matched,
// it returns the route information and a bool value of `true`. If a route can not be matched,
// a nil value for the route and false bool value is returned.
// it returns the route information and a bool value of `true`. If a route can
// not be matched, a nil value for the route and false bool value is returned.
func (p *Proxy) router(r *http.Request) (http.Handler, bool) {
config, ok := p.routeConfigs[r.Host]
if ok {
@ -494,7 +489,7 @@ func (p *Proxy) router(r *http.Request) (http.Handler, bool) {
// policy attempts to find a policy for a request. If a policy is successfully matched,
// it returns the policy information and a bool value of `true`. If a policy can not be matched,
// a nil value for the policy and false bool value is returned.
func (p *Proxy) policy(r *http.Request) (*policy.Policy, bool) {
func (p *Proxy) policy(r *http.Request) (*config.Policy, bool) {
config, ok := p.routeConfigs[r.Host]
if ok {
return &config.policy, true
@ -546,32 +541,3 @@ func (p *Proxy) GetSignOutURL(authenticateURL, redirectURL *url.URL) *url.URL {
a.RawQuery = params.Encode()
return a
}
func extendDeadline(ttl time.Duration) time.Time {
return time.Now().Add(ttl).Truncate(time.Second)
}
// websocketHandlerFunc splits request serving with timeouts depending on the protocol
func websocketHandlerFunc(baseHandler http.Handler, timeoutHandler http.Handler, o config.Options) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do not use timeouts for websockets because they are long-lived connections.
if r.ProtoMajor == 1 &&
strings.EqualFold(r.Header.Get("Connection"), "upgrade") &&
strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
if o.AllowWebsockets {
baseHandler.ServeHTTP(w, r)
return
}
log.FromRequest(r).Warn().Msg("proxy: attempt to proxy a websocket connection, but websocket support is disabled in the configuration")
httpErr := &httputil.Error{Message: "websockets not supported by proxy", Code: http.StatusBadRequest}
httputil.ErrorResponse(w, r, httpErr)
return
}
// All other non-websocket requests are served with timeouts to prevent abuse
timeoutHandler.ServeHTTP(w, r)
})
}

View file

@ -14,7 +14,6 @@ import (
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/policy"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/proxy/clients"
)
@ -108,13 +107,12 @@ func TestProxy_GetSignOutURL(t *testing.T) {
redirect string
wantPrefix string
}{
{"without scheme", "auth.corp.pomerium.io", "hello.corp.pomerium.io", "https://auth.corp.pomerium.io/sign_out?redirect_uri=https%3A%2F%2Fhello.corp.pomerium.io"},
{"with scheme", "https://auth.corp.pomerium.io", "https://hello.corp.pomerium.io", "https://auth.corp.pomerium.io/sign_out?redirect_uri=https%3A%2F%2Fhello.corp.pomerium.io"},
{"good", "https://auth.corp.pomerium.io", "https://hello.corp.pomerium.io", "https://auth.corp.pomerium.io/sign_out?redirect_uri=https%3A%2F%2Fhello.corp.pomerium.io"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authenticateURL, _ := urlParse(tt.authenticate)
redirectURL, _ := urlParse(tt.redirect)
authenticateURL, _ := url.Parse(tt.authenticate)
redirectURL, _ := url.Parse(tt.redirect)
p := &Proxy{}
// signature is ignored as it is tested above. Avoids testing time.Now
@ -135,14 +133,13 @@ func TestProxy_GetSignInURL(t *testing.T) {
wantPrefix string
}{
{"without scheme", "auth.corp.pomerium.io", "hello.corp.pomerium.io", "example_state", "https://auth.corp.pomerium.io/sign_in?redirect_uri=https%3A%2F%2Fhello.corp.pomerium.io&response_type=code&shared_secret=shared-secret"},
{"with scheme", "https://auth.corp.pomerium.io", "https://hello.corp.pomerium.io", "example_state", "https://auth.corp.pomerium.io/sign_in?redirect_uri=https%3A%2F%2Fhello.corp.pomerium.io&response_type=code&shared_secret=shared-secret"},
{"good", "https://auth.corp.pomerium.io", "https://hello.corp.pomerium.io", "example_state", "https://auth.corp.pomerium.io/sign_in?redirect_uri=https%3A%2F%2Fhello.corp.pomerium.io&response_type=code&shared_secret=shared-secret"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Proxy{SharedKey: "shared-secret"}
authenticateURL, _ := urlParse(tt.authenticate)
redirectURL, _ := urlParse(tt.redirect)
authenticateURL, _ := url.Parse(tt.authenticate)
redirectURL, _ := url.Parse(tt.redirect)
if got := p.GetSignInURL(authenticateURL, redirectURL, tt.state); !strings.HasPrefix(got.String(), tt.wantPrefix) {
t.Errorf("Proxy.GetSignOutURL() = %v, wantPrefix %v", got.String(), tt.wantPrefix)
@ -153,7 +150,12 @@ func TestProxy_GetSignInURL(t *testing.T) {
}
func TestProxy_Signout(t *testing.T) {
proxy, err := New(testOptions())
opts := testOptions(t)
err := ValidateOptions(opts)
if err != nil {
t.Fatal(err)
}
proxy, err := New(opts)
if err != nil {
t.Fatal(err)
}
@ -171,7 +173,7 @@ func TestProxy_Signout(t *testing.T) {
}
func TestProxy_OAuthStart(t *testing.T) {
proxy, err := New(testOptions())
proxy, err := New(testOptions(t))
if err != nil {
t.Fatal(err)
}
@ -184,14 +186,14 @@ func TestProxy_OAuthStart(t *testing.T) {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusFound)
}
// expected url
expected := `<a href="https://authenticate.corp.beyondperimeter.com/sign_in`
expected := `<a href="https://authenticate.example/sign_in`
body := rr.Body.String()
if !strings.HasPrefix(body, expected) {
t.Errorf("handler returned unexpected body: got %v want %v", body, expected)
}
}
func TestProxy_Handler(t *testing.T) {
proxy, err := New(testOptions())
proxy, err := New(testOptions(t))
if err != nil {
t.Fatal(err)
}
@ -209,32 +211,16 @@ func TestProxy_Handler(t *testing.T) {
}
}
func Test_extendDeadline(t *testing.T) {
tests := []struct {
name string
ttl time.Duration
want time.Time
}{
{"good", time.Second, time.Now().Add(time.Second).Truncate(time.Second)},
{"test nanoseconds truncated", 500 * time.Nanosecond, time.Now().Truncate(time.Second)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := extendDeadline(tt.ttl); !reflect.DeepEqual(got, tt.want) {
t.Errorf("extendDeadline() = %v, want %v", got, tt.want)
}
})
}
}
func TestProxy_router(t *testing.T) {
testPolicy := policy.Policy{From: "corp.example.com", To: "example.com"}
testPolicy.Validate()
policies := []policy.Policy{testPolicy}
testPolicy := config.Policy{From: "https://corp.example.com", To: "https://example.com"}
if err := testPolicy.Validate(); err != nil {
t.Fatal(err)
}
policies := []config.Policy{testPolicy}
tests := []struct {
name string
host string
mux []policy.Policy
mux []config.Policy
route http.Handler
wantOk bool
}{
@ -242,13 +228,13 @@ func TestProxy_router(t *testing.T) {
{"good with slash", "https://corp.example.com/", policies, nil, true},
{"good with path", "https://corp.example.com/123", policies, nil, true},
// {"multiple", "https://corp.example.com/", map[string]string{"corp.unrelated.com": "unrelated.com", "corp.example.com": "example.com"}, nil, true},
{"no policies", "https://notcorp.example.com/123", []policy.Policy{}, nil, false},
{"no policies", "https://notcorp.example.com/123", []config.Policy{}, nil, false},
{"bad corp", "https://notcorp.example.com/123", policies, nil, false},
{"bad sub-sub", "https://notcorp.corp.example.com/123", policies, nil, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := testOptions()
opts := testOptions(t)
opts.Policies = tt.mux
p, err := New(opts)
if err != nil {
@ -278,11 +264,10 @@ func TestProxy_Proxy(t *testing.T) {
}))
defer ts.Close()
opts, optsWs := testOptionsTestServer(ts.URL), testOptionsTestServer(ts.URL)
optsCORS := testOptionsWithCORS(ts.URL)
optsPublic := testOptionsWithPublicAccess(ts.URL)
optsNoPolicies := testOptionsWithEmptyPolicies(ts.URL)
optsWs.AllowWebsockets = true
opts := testOptionsTestServer(t, ts.URL)
optsCORS := testOptionsWithCORS(t, ts.URL)
optsPublic := testOptionsWithPublicAccess(t, ts.URL)
optsNoPolicies := testOptionsWithEmptyPolicies(t, ts.URL)
defaultHeaders, goodCORSHeaders, badCORSHeaders, headersWs := http.Header{}, http.Header{}, http.Header{}, http.Header{}
goodCORSHeaders.Set("origin", "anything")
@ -325,15 +310,15 @@ func TestProxy_Proxy(t *testing.T) {
{"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized},
// no session, redirect to login
{"no http found (no session)", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest},
// Should be expecting a 101 Switching Protocols, but expect a 200 OK because we don't have a websocket backend to respond
{"ws supported, ws connection", optsWs, http.MethodGet, headersWs, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
{"ws supported, http connection", optsWs, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
{"ws unsupported, ws connection", opts, http.MethodGet, headersWs, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest},
{"No policies", optsNoPolicies, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateOptions(tt.options)
if err != nil {
t.Fatal(err)
}
p, err := New(tt.options)
if err != nil {
t.Fatal(err)
@ -361,7 +346,7 @@ func TestProxy_Proxy(t *testing.T) {
}
func TestProxy_UserDashboard(t *testing.T) {
opts := testOptions()
opts := testOptions(t)
tests := []struct {
name string
options config.Options
@ -409,9 +394,9 @@ func TestProxy_UserDashboard(t *testing.T) {
}
func TestProxy_Refresh(t *testing.T) {
opts := testOptions()
opts := testOptions(t)
opts.RefreshCooldown = 0
timeSinceError := testOptions()
timeSinceError := testOptions(t)
timeSinceError.RefreshCooldown = time.Duration(int(^uint(0) >> 1))
tests := []struct {
@ -455,7 +440,7 @@ func TestProxy_Refresh(t *testing.T) {
}
func TestProxy_Impersonate(t *testing.T) {
opts := testOptions()
opts := testOptions(t)
tests := []struct {
name string
@ -535,7 +520,7 @@ func TestProxy_OAuthCallback(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proxy, err := New(testOptions())
proxy, err := New(testOptions(t))
if err != nil {
t.Fatal(err)
}
@ -576,7 +561,7 @@ func TestProxy_SignOut(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := testOptions()
opts := testOptions(t)
p, err := New(opts)
if err != nil {
t.Fatal(err)

View file

@ -1,6 +1,8 @@
package proxy // import "github.com/pomerium/pomerium/proxy"
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
@ -9,14 +11,13 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/metrics"
"github.com/pomerium/pomerium/internal/policy"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates"
"github.com/pomerium/pomerium/internal/tripper"
@ -44,13 +45,13 @@ func ValidateOptions(o config.Options) error {
if len(decoded) != 32 {
return fmt.Errorf("`SHARED_SECRET` want 32 but got %d bytes", len(decoded))
}
if o.AuthenticateURL.String() == "" {
if o.AuthenticateURL == nil || o.AuthenticateURL.String() == "" {
return errors.New("missing setting: authenticate-service-url")
}
if o.AuthenticateURL.Scheme != "https" {
return errors.New("authenticate-service-url must be a valid https url")
}
if o.AuthorizeURL.String() == "" {
if o.AuthorizeURL == nil || o.AuthorizeURL.String() == "" {
return errors.New("missing setting: authorize-service-url")
}
if o.AuthorizeURL.Scheme != "https" {
@ -67,40 +68,42 @@ func ValidateOptions(o config.Options) error {
return fmt.Errorf("cookie secret expects 32 bytes but got %d", len(decodedCookieSecret))
}
if len(o.SigningKey) != 0 {
_, err := base64.StdEncoding.DecodeString(o.SigningKey)
decodedSigningKey, err := base64.StdEncoding.DecodeString(o.SigningKey)
if err != nil {
return fmt.Errorf("signing key is invalid base64: %v", err)
}
_, err = cryptutil.NewES256Signer(decodedSigningKey, "localhost")
if err != nil {
return fmt.Errorf("invalid signing key is : %v", err)
}
}
return nil
}
// Proxy stores all the information associated with proxying a request.
type Proxy struct {
SharedKey string
// authenticate service
// SharedKey used to mutually authenticate service communication
SharedKey string
AuthenticateURL *url.URL
AuthenticateClient clients.Authenticator
AuthorizeClient clients.Authorizer
// authorize service
AuthorizeClient clients.Authorizer
// session
cipher cryptutil.Cipher
csrfStore sessions.CSRFStore
sessionStore sessions.SessionStore
restStore sessions.SessionStore
redirectURL *url.URL
templates *template.Template
routeConfigs map[string]*routeConfig
refreshCooldown time.Duration
cipher cryptutil.Cipher
cookieName string
csrfStore sessions.CSRFStore
defaultUpstreamTimeout time.Duration
redirectURL *url.URL
refreshCooldown time.Duration
restStore sessions.SessionStore
routeConfigs map[string]*routeConfig
sessionStore sessions.SessionStore
signingKey string
templates *template.Template
}
type routeConfig struct {
mux http.Handler
policy policy.Policy
policy config.Policy
}
// New takes a Proxy service from options and a validation function.
@ -134,29 +137,32 @@ func New(opts config.Options) (*Proxy, error) {
return nil, err
}
p := &Proxy{
SharedKey: opts.SharedKey,
routeConfigs: make(map[string]*routeConfig),
// services
AuthenticateURL: &opts.AuthenticateURL,
// session state
cipher: cipher,
csrfStore: cookieStore,
sessionStore: cookieStore,
restStore: restStore,
SharedKey: opts.SharedKey,
redirectURL: &url.URL{Path: "/.pomerium/callback"},
templates: templates.New(),
refreshCooldown: opts.RefreshCooldown,
AuthenticateURL: opts.AuthenticateURL,
cipher: cipher,
cookieName: opts.CookieName,
csrfStore: cookieStore,
defaultUpstreamTimeout: opts.DefaultUpstreamTimeout,
redirectURL: &url.URL{Path: "/.pomerium/callback"},
refreshCooldown: opts.RefreshCooldown,
restStore: restStore,
sessionStore: cookieStore,
signingKey: opts.SigningKey,
templates: templates.New(),
}
err = p.UpdatePolicies(opts)
if err != nil {
if err := p.UpdatePolicies(&opts); err != nil {
return nil, err
}
p.AuthenticateClient, err = clients.NewAuthenticateClient("grpc",
&clients.Options{
Addr: opts.AuthenticateURL.Host,
InternalAddr: opts.AuthenticateInternalAddr.Host,
Addr: opts.AuthenticateURL,
InternalAddr: opts.AuthenticateInternalAddr,
OverrideCertificateName: opts.OverrideCertificateName,
SharedSecret: opts.SharedKey,
CA: opts.CA,
@ -167,7 +173,7 @@ func New(opts config.Options) (*Proxy, error) {
}
p.AuthorizeClient, err = clients.NewAuthorizeClient("grpc",
&clients.Options{
Addr: opts.AuthorizeURL.Host,
Addr: opts.AuthorizeURL,
OverrideCertificateName: opts.OverrideCertificateName,
SharedSecret: opts.SharedKey,
CA: opts.CA,
@ -177,26 +183,44 @@ func New(opts config.Options) (*Proxy, error) {
}
// UpdatePolicies updates the handlers based on the configured policies
func (p *Proxy) UpdatePolicies(opts config.Options) error {
routeConfigs := make(map[string]*routeConfig)
policyCount := len(opts.Policies)
if policyCount == 0 {
log.Warn().Msg("proxy: loaded configuration with no policies specified")
func (p *Proxy) UpdatePolicies(opts *config.Options) error {
routeConfigs := make(map[string]*routeConfig, len(opts.Policies))
if len(opts.Policies) == 0 {
log.Warn().Msg("proxy: configuration has no policies")
}
log.Info().Int("policy-count", policyCount).Msg("proxy: updated policies")
for _, policy := range opts.Policies {
if err := policy.Validate(); err != nil {
return fmt.Errorf("proxy: couldn't update policies %s", err)
}
proxy := NewReverseProxy(policy.Destination)
// build http transport (roundtripper) middleware chain
// todo(bdd): this will make vet complain, it is safe
// and can be replaced with transport.Clone() in go 1.13
// https://go-review.googlesource.com/c/go/+/174597/
// https://github.com/golang/go/issues/26013#issuecomment-399481302
transport := *(http.DefaultTransport.(*http.Transport))
c := tripper.NewChain()
c = c.Append(metrics.HTTPMetricsRoundTripper("proxy"))
if policy.TLSSkipVerify {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if policy.TLSCustomCA != "" {
rootCA, err := p.customCAPool(policy.TLSCustomCA)
if err != nil {
return fmt.Errorf("proxy: couldn't add custom ca to policy %s", policy.From)
}
transport.TLSClientConfig = &tls.Config{RootCAs: rootCA}
}
proxy.Transport = c.Then(&transport)
for _, route := range opts.Policies {
proxy := NewReverseProxy(route.Destination)
handler, err := NewReverseProxyHandler(opts, proxy, &route)
handler, err := p.newReverseProxyHandler(proxy, &policy)
if err != nil {
return err
}
routeConfigs[route.Source.Host] = &routeConfig{
routeConfigs[policy.Source.Host] = &routeConfig{
mux: handler,
policy: route,
policy: policy,
}
log.Info().Str("src", route.Source.Host).Str("dst", route.Destination.Host).Msg("proxy: new route")
}
p.routeConfigs = routeConfigs
return nil
@ -204,40 +228,12 @@ func (p *Proxy) UpdatePolicies(opts config.Options) error {
// UpstreamProxy stores information for proxying the request to the upstream.
type UpstreamProxy struct {
name string
cookieName string
handler http.Handler
signer cryptutil.JWTSigner
name string
handler http.Handler
}
// deleteUpstreamCookies deletes the session cookie from the request header string.
func deleteUpstreamCookies(req *http.Request, cookieName string) {
headers := []string{}
for _, cookie := range req.Cookies() {
if cookie.Name != cookieName {
headers = append(headers, cookie.String())
}
}
req.Header.Set("Cookie", strings.Join(headers, ";"))
}
func (u *UpstreamProxy) signRequest(r *http.Request) {
if u.signer != nil {
jwt, err := u.signer.SignJWT(
r.Header.Get(HeaderUserID),
r.Header.Get(HeaderEmail),
r.Header.Get(HeaderGroups))
if err == nil {
r.Header.Set(HeaderJWT, jwt)
}
}
}
// ServeHTTP signs the http request and deletes cookie headers
// before calling the upstream's ServeHTTP function.
// ServeHTTP handles the second (reverse-proxying) leg of pomerium's request flow
func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
deleteUpstreamCookies(r, u.cookieName)
u.signRequest(r)
u.handler.ServeHTTP(w, r)
}
@ -247,8 +243,6 @@ func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(to)
sublogger := log.With().Str("proxy", to.Host).Logger()
proxy.ErrorLog = stdlog.New(&log.StdLogWrapper{Logger: &sublogger}, "", 0)
// todo(bdd): default is already http.DefaultTransport)
// proxy.Transport = defaultUpstreamTransport
director := proxy.Director
proxy.Director = func(req *http.Request) {
// Identifies the originating IP addresses of a client connecting to
@ -257,51 +251,62 @@ func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
director(req)
req.Host = to.Host
}
chain := tripper.NewChain().Append(metrics.HTTPMetricsRoundTripper("proxy"))
proxy.Transport = chain.Then(nil)
return proxy
}
// NewReverseProxyHandler applies handler specific options to a given route.
func NewReverseProxyHandler(o config.Options, proxy *httputil.ReverseProxy, route *policy.Policy) (http.Handler, error) {
up := &UpstreamProxy{
name: route.Destination.Host,
handler: proxy,
cookieName: o.CookieName,
// newRouteSigner creates a route specific signer.
func (p *Proxy) newRouteSigner(audience string) (cryptutil.JWTSigner, error) {
decodedSigningKey, err := base64.StdEncoding.DecodeString(p.signingKey)
if err != nil {
return nil, err
}
if len(o.SigningKey) != 0 {
decodedSigningKey, _ := base64.StdEncoding.DecodeString(o.SigningKey)
signer, err := cryptutil.NewES256Signer(decodedSigningKey, route.Source.Host)
return cryptutil.NewES256Signer(decodedSigningKey, audience)
}
func (p *Proxy) customCAPool(cert string) (*x509.CertPool, error) {
certPool := x509.NewCertPool()
decodedCert, err := base64.StdEncoding.DecodeString(cert)
if err != nil {
return nil, fmt.Errorf("failed to decode cert: %s", err)
}
if ok := certPool.AppendCertsFromPEM(decodedCert); !ok {
return nil, fmt.Errorf("could not append cert: %s", decodedCert)
}
return certPool, nil
}
// newReverseProxyHandler applies handler specific options to a given route.
func (p *Proxy) newReverseProxyHandler(rp *httputil.ReverseProxy, route *config.Policy) (http.Handler, error) {
var handler http.Handler
handler = &UpstreamProxy{
name: route.Destination.Host,
handler: rp,
}
c := middleware.NewChain()
c = c.Append(middleware.StripPomeriumCookie(p.cookieName))
// if signing key is set, add signer to middleware
if len(p.signingKey) != 0 {
signer, err := p.newRouteSigner(route.Source.Host)
if err != nil {
return nil, err
}
up.signer = signer
c = c.Append(middleware.SignRequest(signer, HeaderUserID, HeaderEmail, HeaderGroups, HeaderJWT))
}
timeout := o.DefaultUpstreamTimeout
if route.UpstreamTimeout != 0 {
timeout = route.UpstreamTimeout
// websockets cannot use the non-hijackable timeout-handler
if !route.AllowWebsockets {
timeout := p.defaultUpstreamTimeout
if route.UpstreamTimeout != 0 {
timeout = route.UpstreamTimeout
}
timeoutMsg := fmt.Sprintf("%s failed to respond within the %s timeout period", route.Destination.Host, timeout)
handler = http.TimeoutHandler(handler, timeout, timeoutMsg)
}
timeoutMsg := fmt.Sprintf("%s failed to respond within the %s timeout period", route.Destination.Host, timeout)
timeoutHandler := http.TimeoutHandler(up, timeout, timeoutMsg)
return websocketHandlerFunc(up, timeoutHandler, o), nil
}
// urlParse wraps url.Parse to add a scheme if none-exists.
// https://github.com/golang/go/issues/12585
func urlParse(uri string) (*url.URL, error) {
if !strings.Contains(uri, "://") {
uri = fmt.Sprintf("https://%s", uri)
}
return url.ParseRequestURI(uri)
return c.Then(handler), nil
}
// UpdateOptions updates internal structures based on config.Options
func (p *Proxy) UpdateOptions(o config.Options) error {
log.Info().Msg("proxy: updating options")
err := p.UpdatePolicies(o)
if err != nil {
return fmt.Errorf("Could not update policies: %s", err)
}
return nil
return p.UpdatePolicies(&o)
}

View file

@ -10,12 +10,19 @@ import (
"time"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/policy"
)
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
func newTestOptions(t *testing.T) *config.Options {
opts, err := config.NewOptions("https://authenticate.example", "https://authorize.example")
if err != nil {
t.Fatal(err)
}
opts.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
return opts
}
func TestNewReverseProxy(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@ -54,14 +61,19 @@ func TestNewReverseProxyHandler(t *testing.T) {
backendHost := net.JoinHostPort(backendHostname, backendPort)
proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
proxyHandler := NewReverseProxy(proxyURL)
opts := config.NewOptions()
opts := newTestOptions(t)
opts.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
testPolicy := policy.Policy{From: "corp.example.com", To: "example.com", UpstreamTimeout: 1 * time.Second}
testPolicy.Validate()
handle, err := NewReverseProxyHandler(opts, proxyHandler, &testPolicy)
testPolicy := config.Policy{From: "https://corp.example.com", To: "https://example.com", UpstreamTimeout: 1 * time.Second}
if err := testPolicy.Validate(); err != nil {
t.Fatal(err)
}
p, err := New(*opts)
if err != nil {
t.Errorf("got %q", err)
t.Fatal(err)
}
handle, err := p.newReverseProxyHandler(proxyHandler, &testPolicy)
if err != nil {
t.Fatal(err)
}
frontend := httptest.NewServer(handle)
@ -77,109 +89,104 @@ func TestNewReverseProxyHandler(t *testing.T) {
}
}
func testOptions() config.Options {
func testOptions(t *testing.T) config.Options {
authenticateService, _ := url.Parse("https://authenticate.corp.beyondperimeter.com")
authorizeService, _ := url.Parse("https://authorize.corp.beyondperimeter.com")
opts := config.NewOptions()
testPolicy := policy.Policy{From: "corp.example.notatld", To: "example.notatld"}
testPolicy.Validate()
opts.Policies = []policy.Policy{testPolicy}
opts.AuthenticateURL = *authenticateService
opts.AuthorizeURL = *authorizeService
opts := newTestOptions(t)
testPolicy := config.Policy{From: "https://corp.example.example", To: "https://example.example"}
opts.Policies = []config.Policy{testPolicy}
opts.AuthenticateURL = authenticateService
opts.AuthorizeURL = authorizeService
opts.SharedKey = "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="
opts.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
opts.CookieName = "pomerium"
return opts
err := opts.Validate()
if err != nil {
t.Fatal(err)
}
return *opts
}
func testOptionsTestServer(uri string) config.Options {
func testOptionsTestServer(t *testing.T, uri string) config.Options {
authenticateService, _ := url.Parse("https://authenticate.corp.beyondperimeter.com")
authorizeService, _ := url.Parse("https://authorize.corp.beyondperimeter.com")
// RFC 2606
testPolicy := policy.Policy{
From: "httpbin.corp.example",
testPolicy := config.Policy{
From: "https://httpbin.corp.example",
To: uri,
}
testPolicy.Validate()
opts := config.NewOptions()
opts.Policies = []policy.Policy{testPolicy}
opts.AuthenticateURL = *authenticateService
opts.AuthorizeURL = *authorizeService
if err := testPolicy.Validate(); err != nil {
t.Fatal(err)
}
opts := newTestOptions(t)
opts.Policies = []config.Policy{testPolicy}
opts.AuthenticateURL = authenticateService
opts.AuthorizeURL = authorizeService
opts.SharedKey = "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="
opts.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
opts.CookieName = "pomerium"
return opts
return *opts
}
func testOptionsWithCORS(uri string) config.Options {
testPolicy := policy.Policy{
From: "httpbin.corp.example",
func testOptionsWithCORS(t *testing.T, uri string) config.Options {
testPolicy := config.Policy{
From: "https://httpbin.corp.example",
To: uri,
CORSAllowPreflight: true,
}
testPolicy.Validate()
opts := testOptionsTestServer(uri)
opts.Policies = []policy.Policy{testPolicy}
if err := testPolicy.Validate(); err != nil {
t.Fatal(err)
}
opts := testOptionsTestServer(t, uri)
opts.Policies = []config.Policy{testPolicy}
return opts
}
func testOptionsWithPublicAccess(uri string) config.Options {
testPolicy := policy.Policy{
From: "httpbin.corp.example",
func testOptionsWithPublicAccess(t *testing.T, uri string) config.Options {
testPolicy := config.Policy{
From: "https://httpbin.corp.example",
To: uri,
AllowPublicUnauthenticatedAccess: true,
}
testPolicy.Validate()
opts := testOptions()
opts.Policies = []policy.Policy{testPolicy}
return opts
}
func testOptionsWithPublicAccessAndWhitelist(uri string) config.Options {
testPolicy := policy.Policy{
From: "httpbin.corp.example",
To: uri,
AllowPublicUnauthenticatedAccess: true,
AllowedEmails: []string{"test@gmail.com"},
if err := testPolicy.Validate(); err != nil {
t.Fatal(err)
}
testPolicy.Validate()
opts := testOptions()
opts.Policies = []policy.Policy{testPolicy}
opts := testOptions(t)
opts.Policies = []config.Policy{testPolicy}
return opts
}
func testOptionsWithEmptyPolicies(uri string) config.Options {
opts := testOptionsTestServer(uri)
opts.Policies = []policy.Policy{}
func testOptionsWithEmptyPolicies(t *testing.T, uri string) config.Options {
opts := testOptionsTestServer(t, uri)
opts.Policies = []config.Policy{}
return opts
}
func TestOptions_Validate(t *testing.T) {
good := testOptions()
badAuthURL := testOptions()
badAuthURL.AuthenticateURL = url.URL{}
good := testOptions(t)
badAuthURL := testOptions(t)
badAuthURL.AuthenticateURL = nil
authurl, _ := url.Parse("http://authenticate.corp.beyondperimeter.com")
authenticateBadScheme := testOptions()
authenticateBadScheme.AuthenticateURL = *authurl
authorizeBadSCheme := testOptions()
authorizeBadSCheme.AuthorizeURL = *authurl
authorizeNil := testOptions()
authorizeNil.AuthorizeURL = url.URL{}
emptyCookieSecret := testOptions()
authenticateBadScheme := testOptions(t)
authenticateBadScheme.AuthenticateURL = authurl
authorizeBadSCheme := testOptions(t)
authorizeBadSCheme.AuthorizeURL = authurl
authorizeNil := testOptions(t)
authorizeNil.AuthorizeURL = nil
emptyCookieSecret := testOptions(t)
emptyCookieSecret.CookieSecret = ""
invalidCookieSecret := testOptions()
invalidCookieSecret := testOptions(t)
invalidCookieSecret.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^"
shortCookieLength := testOptions()
shortCookieLength := testOptions(t)
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
invalidSignKey := testOptions()
invalidSignKey := testOptions(t)
invalidSignKey.SigningKey = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^"
badSharedKey := testOptions()
badSharedKey := testOptions(t)
badSharedKey.SharedKey = ""
sharedKeyBadBas64 := testOptions()
sharedKeyBadBas64 := testOptions(t)
sharedKeyBadBas64.SharedKey = "%(*@389"
missingPolicy := testOptions()
missingPolicy.Policies = []policy.Policy{}
missingPolicy := testOptions(t)
missingPolicy.Policies = []config.Policy{}
tests := []struct {
name string
@ -197,7 +204,6 @@ func TestOptions_Validate(t *testing.T) {
{"short cookie secret", shortCookieLength, true},
{"no shared secret", badSharedKey, true},
{"invalid signing key", invalidSignKey, true},
{"missing policy", missingPolicy, false},
{"shared secret bad base64", sharedKeyBadBas64, true},
}
for _, tt := range tests {
@ -212,10 +218,10 @@ func TestOptions_Validate(t *testing.T) {
func TestNew(t *testing.T) {
good := testOptions()
shortCookieLength := testOptions()
good := testOptions(t)
shortCookieLength := testOptions(t)
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
badRoutedProxy := testOptions()
badRoutedProxy := testOptions(t)
badRoutedProxy.SigningKey = "YmFkIGtleQo="
tests := []struct {
name string
@ -240,7 +246,7 @@ func TestNew(t *testing.T) {
t.Errorf("New() expected valid proxy struct")
}
if got != nil && len(got.routeConfigs) != tt.numRoutes {
t.Errorf("New() = num routeConfigs \n%+v, want \n%+v", got, tt.numRoutes)
t.Errorf("New() = num routeConfigs \n%+v, want \n%+v \nfrom %+v", got, tt.numRoutes, tt.opts)
}
})
}
@ -248,34 +254,65 @@ func TestNew(t *testing.T) {
func Test_UpdateOptions(t *testing.T) {
good := testOptions()
bad := testOptions()
bad.SigningKey = "f"
newPolicy := policy.Policy{To: "foo.notatld", From: "bar.notatld"}
newPolicy.Validate()
newPolicies := []policy.Policy{
good := testOptions(t)
newPolicy := config.Policy{To: "http://foo.example", From: "http://bar.example"}
newPolicies := testOptions(t)
newPolicies.Policies = []config.Policy{
newPolicy,
}
err := newPolicy.Validate()
if err != nil {
t.Fatal(err)
}
badPolicyURL := config.Policy{To: "http://", From: "http://bar.example"}
badNewPolicy := testOptions(t)
badNewPolicy.Policies = []config.Policy{
badPolicyURL,
}
disableTLSPolicy := config.Policy{To: "http://foo.example", From: "http://bar.example", TLSSkipVerify: true}
disableTLSPolicies := testOptions(t)
disableTLSPolicies.Policies = []config.Policy{
disableTLSPolicy,
}
customCAPolicy := config.Policy{To: "http://foo.example", From: "http://bar.example", TLSCustomCA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURlVENDQW1HZ0F3SUJBZ0lKQUszMmhoR0JIcmFtTUEwR0NTcUdTSWIzRFFFQkN3VUFNR0l4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlEQXBEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIREExVFlXNGdSbkpoYm1OcApjMk52TVE4d0RRWURWUVFLREFaQ1lXUlRVMHd4RlRBVEJnTlZCQU1NRENvdVltRmtjM05zTG1OdmJUQWVGdzB4Ck9UQTJNVEl4TlRNeE5UbGFGdzB5TVRBMk1URXhOVE14TlRsYU1HSXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWUQKVlFRSURBcERZV3hwWm05eWJtbGhNUll3RkFZRFZRUUhEQTFUWVc0Z1JuSmhibU5wYzJOdk1ROHdEUVlEVlFRSwpEQVpDWVdSVFUwd3hGVEFUQmdOVkJBTU1EQ291WW1Ga2MzTnNMbU52YlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCCkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU1JRTdQaU03Z1RDczloUTFYQll6Sk1ZNjF5b2FFbXdJclg1bFo2eEt5eDIKUG16QVMyQk1UT3F5dE1BUGdMYXcrWExKaGdMNVhFRmRFeXQvY2NSTHZPbVVMbEEzcG1jY1lZejJRVUxGUnRNVwpoeWVmZE9zS25SRlNKaUZ6YklSTWVWWGswV3ZvQmoxSUZWS3RzeWpicXY5dS8yQ1ZTbmRyT2ZFazBURzIzVTNBCnhQeFR1VzFDcmJWOC9xNzFGZEl6U09jaWNjZkNGSHBzS09vM1N0L3FiTFZ5dEg1YW9oYmNhYkZYUk5zS0VxdmUKd3c5SGRGeEJJdUdhK1J1VDVxMGlCaWt1c2JwSkhBd25ucVA3aS9kQWNnQ3NrZ2paakZlRVU0RUZ5K2IrYTFTWQpRQ2VGeHhDN2MzRHZhUmhCQjBWVmZQbGtQejBzdzZsODY1TWFUSWJSeW9VQ0F3RUFBYU15TURBd0NRWURWUjBUCkJBSXdBREFqQmdOVkhSRUVIREFhZ2d3cUxtSmhaSE56YkM1amIyMkNDbUpoWkhOemJDNWpiMjB3RFFZSktvWkkKaHZjTkFRRUxCUUFEZ2dFQkFJaTV1OXc4bWdUNnBwQ2M3eHNHK0E5ZkkzVzR6K3FTS2FwaHI1bHM3MEdCS2JpWQpZTEpVWVpoUGZXcGgxcXRra1UwTEhGUG04M1ZhNTJlSUhyalhUMFZlNEt0TzFuMElBZkl0RmFXNjJDSmdoR1luCmp6dzByeXpnQzRQeUZwTk1uTnRCcm9QdS9iUGdXaU1nTE9OcEVaaGlneDRROHdmMVkvVTlzK3pDQ3hvSmxhS1IKTVhidVE4N1g3bS85VlJueHhvNk56NVpmN09USFRwTk9JNlZqYTBCeGJtSUFVNnlyaXc5VXJnaWJYZk9qM2o2bgpNVExCdWdVVklCMGJCYWFzSnNBTUsrdzRMQU52YXBlWjBET1NuT1I0S0syNEowT3lvRjVmSG1wNTllTTE3SW9GClFxQmh6cG1RVWd1bmVjRVc4QlRxck5wRzc5UjF1K1YrNHd3Y2tQYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="}
customCAPolicies := testOptions(t)
customCAPolicies.Policies = []config.Policy{
customCAPolicy,
}
badCustomCAPolicy := config.Policy{To: "http://foo.example", From: "http://bar.example", TLSCustomCA: "=@@"}
badCustomCAPolicies := testOptions(t)
badCustomCAPolicies.Policies = []config.Policy{
badCustomCAPolicy,
}
tests := []struct {
name string
opts config.Options
newPolicy []policy.Policy
host string
wantErr bool
wantRoute bool
name string
originalOptions config.Options
updatedOptions config.Options
signingKey string
host string
wantErr bool
wantRoute bool
}{
{"good", good, good.Policies, "https://corp.example.notatld", false, true},
{"changed", good, newPolicies, "https://bar.notatld", false, true},
{"changed and missing", good, newPolicies, "https://corp.example.notatld", false, false},
{"bad options", bad, good.Policies, "https://corp.example.notatld", true, false},
{"good no change", good, good, "", "https://corp.example.example", false, true},
{"changed", good, newPolicies, "", "https://bar.example", false, true},
{"changed and missing", good, newPolicies, "", "https://corp.example.example", false, false},
// todo(bdd): not sure what intent of this test is?
{"bad signing key", good, newPolicies, "^bad base 64", "https://corp.example.example", true, false},
{"bad change bad policy url", good, badNewPolicy, "", "https://bar.example", true, false},
// todo: stand up a test server using self signed certificates
{"disable tls verification", good, disableTLSPolicies, "", "https://bar.example", false, true},
{"custom root ca", good, customCAPolicies, "", "https://bar.example", false, true},
{"bad custom root ca base64", good, badCustomCAPolicies, "", "https://bar.example", true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := tt.opts
p, _ := New(o)
p, err := New(tt.originalOptions)
if err != nil {
t.Fatal(err)
}
o.Policies = tt.newPolicy
err := p.UpdateOptions(o)
p.signingKey = tt.signingKey
err = p.UpdateOptions(tt.updatedOptions)
if (err != nil) != tt.wantErr {
t.Errorf("UpdateOptions: err = %v, wantErr = %v", err, tt.wantErr)
return