Refactor to central options struct and parsing

This commit is contained in:
Travis Groth 2019-05-14 22:46:10 -04:00
parent 5970d6c766
commit ebb6df6c3f
12 changed files with 415 additions and 511 deletions

View file

@ -6,9 +6,8 @@ import (
"fmt"
"html/template"
"net/url"
"time"
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/identity"
@ -16,53 +15,10 @@ import (
"github.com/pomerium/pomerium/internal/templates"
)
var defaultOptions = &Options{
CookieName: "_pomerium_authenticate",
CookieHTTPOnly: true,
CookieSecure: true,
CookieExpire: time.Duration(14) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
}
// Options details the available configuration settings for the authenticate service
type Options struct {
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
// SharedKey is used to authenticate requests between services
SharedKey string `envconfig:"SHARED_SECRET"`
// Session/Cookie management
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
CookieName string
CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
CookieSecure bool `envconfig:"COOKIE_SECURE"`
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
// Identity provider configuration variables as specified by RFC6749
// https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
ClientID string `envconfig:"IDP_CLIENT_ID"`
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"`
Provider string `envconfig:"IDP_PROVIDER"`
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
Scopes []string `envconfig:"IDP_SCOPES"`
ServiceAccount string `envconfig:"IDP_SERVICE_ACCOUNT"`
}
// OptionsFromEnvConfig builds the authenticate service's configuration environmental variables
func OptionsFromEnvConfig() (*Options, error) {
o := defaultOptions
if err := envconfig.Process("", o); err != nil {
return nil, err
}
return o, nil
}
// Validate checks to see if configuration values are valid for the authenticate service.
// ValidateOptions checks to see if configuration values are valid for the authenticate service.
// The checks do not modify the internal state of the Option structure. Returns
// on first error found.
func (o *Options) Validate() error {
func ValidateOptions(o *config.Options) error {
if o.AuthenticateURL == nil {
return errors.New("authenticate: 'AUTHENTICATE_SERVICE_URL' missing")
}
@ -98,11 +54,11 @@ type Authenticate struct {
}
// New validates and creates a new authenticate service from a set of Options
func New(opts *Options) (*Authenticate, error) {
func New(opts *config.Options) (*Authenticate, error) {
if opts == nil {
return nil, errors.New("authenticate: options cannot be nil")
}
if err := opts.Validate(); err != nil {
if err := ValidateOptions(opts); err != nil {
return nil, err
}
decodedCookieSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
@ -112,7 +68,7 @@ func New(opts *Options) (*Authenticate, error) {
}
cookieStore, err := sessions.NewCookieStore(
&sessions.CookieStoreOptions{
Name: opts.CookieName,
Name: opts.AuthenticateCookieName,
CookieSecure: opts.CookieSecure,
CookieHTTPOnly: opts.CookieHTTPOnly,
CookieExpire: opts.CookieExpire,

View file

@ -2,23 +2,23 @@ package authenticate
import (
"net/url"
"os"
"reflect"
"testing"
"time"
"github.com/pomerium/pomerium/internal/config"
)
func testOptions() *Options {
func testOptions() *config.Options {
redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
return &Options{
AuthenticateURL: redirectURL,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
ClientID: "test-client-id",
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieRefresh: time.Duration(1) * time.Hour,
CookieExpire: time.Duration(168) * time.Hour,
CookieName: "pomerium",
return &config.Options{
AuthenticateURL: redirectURL,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
ClientID: "test-client-id",
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieRefresh: time.Duration(1) * time.Hour,
CookieExpire: time.Duration(168) * time.Hour,
AuthenticateCookieName: "pomerium",
}
}
@ -41,11 +41,11 @@ func TestOptions_Validate(t *testing.T) {
tests := []struct {
name string
o *Options
o *config.Options
wantErr bool
}{
{"minimum options", good, false},
{"nil options", &Options{}, true},
{"nil options", &config.Options{}, true},
{"bad redirect url", badRedirectURL, true},
{"no cookie secret", emptyCookieSecret, true},
{"invalid cookie secret", invalidCookieSecret, true},
@ -57,46 +57,13 @@ func TestOptions_Validate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := tt.o
if err := o.Validate(); (err != nil) != tt.wantErr {
if err := ValidateOptions(o); (err != nil) != tt.wantErr {
t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestOptionsFromEnvConfig(t *testing.T) {
os.Clearenv()
tests := []struct {
name string
want *Options
envKey string
envValue string
wantErr bool
}{
{"good default, no env settings", defaultOptions, "", "", false},
{"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.rjlw", true},
{"good duration", defaultOptions, "COOKIE_EXPIRE", "1m", false},
{"bad duration", nil, "COOKIE_REFRESH", "1sm", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envKey != "" {
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
}
got, err := OptionsFromEnvConfig()
if (err != nil) != tt.wantErr {
t.Errorf("OptionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("OptionsFromEnvConfig() = %v, want %v", got, tt.want)
}
})
}
}
func TestNew(t *testing.T) {
good := testOptions()
good.Provider = "google"
@ -106,7 +73,7 @@ func TestNew(t *testing.T) {
tests := []struct {
name string
opts *Options
opts *config.Options
// want *Authenticate
wantErr bool
}{

View file

@ -5,34 +5,14 @@ import (
"errors"
"fmt"
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/policy"
)
// Options contains configuration settings for the authorize service.
type Options struct {
// SharedKey is used to validate requests between services
SharedKey string `envconfig:"SHARED_SECRET" required:"true"`
// Policy is a base64 encoded yaml blob which enumerates
// per-route access control policies.
Policy string `envconfig:"POLICY"`
PolicyFile string `envconfig:"POLICY_FILE"`
}
// OptionsFromEnvConfig creates an authorize service options from environmental
// variables.
func OptionsFromEnvConfig() (*Options, error) {
o := new(Options)
if err := envconfig.Process("", o); err != nil {
return nil, err
}
return o, nil
}
// Validate checks to see if configuration values are valid for the
// ValidateOptions checks to see if configuration values are valid for the
// authorize service. Returns first error, if found.
func (o *Options) Validate() error {
func ValidateOptions(o *config.Options) error {
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
if err != nil {
return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err)
@ -72,11 +52,11 @@ type Authorize struct {
}
// New validates and creates a new Authorize service from a set of Options
func New(opts *Options) (*Authorize, error) {
func New(opts *config.Options) (*Authorize, error) {
if opts == nil {
return nil, errors.New("authorize: options cannot be nil")
}
if err := opts.Validate(); err != nil {
if err := ValidateOptions(opts); err != nil {
return nil, err
}
// errors handled by validate

View file

@ -4,40 +4,10 @@ import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
)
func TestOptionsFromEnvConfig(t *testing.T) {
t.Parallel()
os.Clearenv()
tests := []struct {
name string
want *Options
envKey string
envValue string
wantErr bool
}{
{"shared secret missing", nil, "", "", true},
{"with secret", &Options{SharedKey: "aGkK"}, "SHARED_SECRET", "aGkK", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envKey != "" {
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
}
got, err := OptionsFromEnvConfig()
if (err != nil) != tt.wantErr {
t.Errorf("OptionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("OptionsFromEnvConfig() = %v, want %v", got, tt.want)
}
})
}
}
"github.com/pomerium/pomerium/internal/config"
)
func TestNew(t *testing.T) {
t.Parallel()
@ -76,7 +46,7 @@ func TestNew(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &Options{SharedKey: tt.SharedKey, Policy: tt.Policy, PolicyFile: tt.PolicyFile}
o := &config.Options{SharedKey: tt.SharedKey, Policy: tt.Policy, PolicyFile: tt.PolicyFile}
if tt.name == "nil options" {
o = nil
}

View file

@ -12,6 +12,7 @@ import (
"github.com/pomerium/pomerium/authenticate"
"github.com/pomerium/pomerium/authorize"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/https"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
@ -30,7 +31,7 @@ func main() {
fmt.Println(version.FullVersion())
os.Exit(0)
}
opt, err := optionsFromEnvConfig()
opt, err := config.OptionsFromEnvConfig()
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: options")
}
@ -41,17 +42,17 @@ func main() {
mux := http.NewServeMux()
_, err = newAuthenticateService(opt.Services, mux, grpcServer)
_, err = newAuthenticateService(opt, mux, grpcServer)
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: authenticate")
}
_, err = newAuthorizeService(opt.Services, grpcServer)
_, err = newAuthorizeService(opt, grpcServer)
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: authorize")
}
_, err = newProxyService(opt.Services, mux)
_, err = newProxyService(opt, mux)
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: proxy")
}
@ -102,32 +103,24 @@ func startRedirectServer(addr string) (*http.Server, error) {
return srv, nil
}
func newAuthenticateService(s string, mux *http.ServeMux, rpc *grpc.Server) (*authenticate.Authenticate, error) {
if !isAuthenticate(s) {
func newAuthenticateService(opt *config.Options, mux *http.ServeMux, rpc *grpc.Server) (*authenticate.Authenticate, error) {
if opt == nil || !config.IsAuthenticate(opt.Services) {
return nil, nil
}
opts, err := authenticate.OptionsFromEnvConfig()
if err != nil {
return nil, err
}
service, err := authenticate.New(opts)
service, err := authenticate.New(opt)
if err != nil {
return nil, err
}
pbAuthenticate.RegisterAuthenticatorServer(rpc, service)
mux.Handle(urlutil.StripPort(opts.AuthenticateURL.Host)+"/", service.Handler())
mux.Handle(urlutil.StripPort(opt.AuthenticateURL.Host)+"/", service.Handler())
return service, nil
}
func newAuthorizeService(s string, rpc *grpc.Server) (*authorize.Authorize, error) {
if !isAuthorize(s) {
func newAuthorizeService(opt *config.Options, rpc *grpc.Server) (*authorize.Authorize, error) {
if opt == nil || !config.IsAuthorize(opt.Services) {
return nil, nil
}
opts, err := authorize.OptionsFromEnvConfig()
if err != nil {
return nil, err
}
service, err := authorize.New(opts)
service, err := authorize.New(opt)
if err != nil {
return nil, err
}
@ -135,15 +128,11 @@ func newAuthorizeService(s string, rpc *grpc.Server) (*authorize.Authorize, erro
return service, nil
}
func newProxyService(s string, mux *http.ServeMux) (*proxy.Proxy, error) {
if !isProxy(s) {
func newProxyService(opt *config.Options, mux *http.ServeMux) (*proxy.Proxy, error) {
if opt == nil || !config.IsProxy(opt.Services) {
return nil, nil
}
opts, err := proxy.OptionsFromEnvConfig()
if err != nil {
return nil, err
}
service, err := proxy.New(opts)
service, err := proxy.New(opt)
if err != nil {
return nil, err
}
@ -151,7 +140,7 @@ func newProxyService(s string, mux *http.ServeMux) (*proxy.Proxy, error) {
return service, nil
}
func wrapMiddleware(o *Options, mux *http.ServeMux) http.Handler {
func wrapMiddleware(o *config.Options, mux *http.ServeMux) http.Handler {
c := middleware.NewChain()
c = c.Append(middleware.NewHandler(log.Logger))
c = c.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
@ -179,3 +168,17 @@ func wrapMiddleware(o *Options, mux *http.ServeMux) http.Handler {
c = c.Append(middleware.Healthcheck("/ping", version.UserAgent()))
return c.Then(mux)
}
func parseOptions() (*config.Options, error) {
o, err := config.OptionsFromEnvConfig()
if err != nil {
return nil, err
}
if o.Debug {
log.SetDebugMode()
}
if o.LogLevel != "" {
log.SetLevel(o.LogLevel)
}
return o, nil
}

View file

@ -5,10 +5,13 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"strings"
"testing"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/middleware"
"google.golang.org/grpc"
)
@ -47,42 +50,41 @@ func Test_startRedirectServer(t *testing.T) {
}
func Test_newAuthenticateService(t *testing.T) {
os.Clearenv()
grpcAuth := middleware.NewSharedSecretCred("test")
grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)}
grpcServer := grpc.NewServer(grpcOpts...)
mux := http.NewServeMux()
tests := []struct {
name string
s string
envKey string
envValue string
name string
s string
Field string
Value string
wantHostname string
wantErr bool
}{
{"wrong service", "proxy", "", "", "", false},
{"bad", "authenticate", "SHARED_SECRET", "error!", "", true},
{"bad emv", "authenticate", "COOKIE_REFRESH", "error!", "", true},
{"good", "authenticate", "IDP_CLIENT_ID", "test", "auth.server.com", false},
{"bad", "authenticate", "SharedKey", "error!", "", true},
{"good", "authenticate", "ClientID", "test", "auth.server.com", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Setenv("IDP_PROVIDER", "google")
os.Setenv("IDP_CLIENT_SECRET", "TEST")
os.Setenv("SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=")
os.Setenv("COOKIE_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=")
os.Setenv("AUTHENTICATE_SERVICE_URL", "http://auth.server.com")
defer os.Unsetenv("IDP_CLIENT_ID")
defer os.Unsetenv("IDP_CLIENT_SECRET")
defer os.Unsetenv("SHARED_SECRET")
defer os.Unsetenv("COOKIE_SECRET")
authURL, _ := url.Parse("http://auth.server.com")
testOpts := config.NewOptions()
testOpts.Provider = "google"
testOpts.ClientSecret = "TEST"
testOpts.SharedKey = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
testOpts.CookieSecret = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
testOpts.AuthenticateURL = authURL
testOpts.Services = tt.s
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
if tt.Field != "" {
testOptsField := reflect.ValueOf(testOpts).Elem().FieldByName(tt.Field)
testOptsField.Set(reflect.ValueOf(tt).FieldByName("Value"))
}
_, err := newAuthenticateService(tt.s, mux, grpcServer)
_, err := newAuthenticateService(testOpts, mux, grpcServer)
if (err != nil) != tt.wantErr {
t.Errorf("newAuthenticateService() error = %v, wantErr %v", err, tt.wantErr)
return
@ -99,27 +101,31 @@ func Test_newAuthorizeService(t *testing.T) {
grpcServer := grpc.NewServer(grpcOpts...)
tests := []struct {
name string
s string
envKey string
envValue string
name string
s string
Field string
Value string
wantErr bool
}{
{"wrong service", "proxy", "", "", false},
{"bad option parsing", "authorize", "SHARED_SECRET", "false", true},
{"bad env", "authorize", "POLICY", "error!", true},
{"good", "authorize", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
{"bad option parsing", "authorize", "SharedKey", "false", true},
{"bad env", "authorize", "Policy", "error!", true},
{"good", "authorize", "SharedKey", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Setenv("POLICY", "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw==")
os.Setenv("COOKIE_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=")
defer os.Unsetenv("SHARED_SECRET")
defer os.Unsetenv("COOKIE_SECRET")
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
_, err := newAuthorizeService(tt.s, grpcServer)
testOpts := config.NewOptions()
testOpts.Services = tt.s
testOpts.CookieSecret = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
testOpts.Policy = "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw=="
if tt.Field != "" {
testOptsField := reflect.ValueOf(testOpts).Elem().FieldByName(tt.Field)
testOptsField.Set(reflect.ValueOf(tt).FieldByName("Value"))
}
_, err := newAuthorizeService(testOpts, grpcServer)
if (err != nil) != tt.wantErr {
t.Errorf("newAuthorizeService() error = %v, wantErr %v", err, tt.wantErr)
return
@ -131,34 +137,33 @@ func Test_newAuthorizeService(t *testing.T) {
func Test_newProxyeService(t *testing.T) {
os.Clearenv()
tests := []struct {
name string
s string
envKey string
envValue string
name string
s string
Field string
Value string
wantErr bool
}{
{"wrong service", "authenticate", "", "", false},
{"bad option parsing", "proxy", "SHARED_SECRET", "false", true},
{"bad env", "proxy", "POLICY", "error!", true},
{"bad encoding for envar", "proxy", "COOKIE_REFRESH", "error!", true},
{"good", "proxy", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
{"bad option parsing", "proxy", "SharedKey", "false", true},
{"bad env", "proxy", "Policy", "error!", true},
{"good", "proxy", "SharedKey", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mux := http.NewServeMux()
testOpts := config.NewOptions()
testOpts.AuthenticateURL, _ = url.Parse("https://authenticate.example.com")
testOpts.AuthorizeURL, _ = url.Parse("https://authorize.example.com")
testOpts.Policy = "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw=="
testOpts.CookieSecret = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
testOpts.Services = tt.s
os.Setenv("AUTHENTICATE_SERVICE_URL", "https://authenticate.example.com")
os.Setenv("AUTHORIZE_SERVICE_URL", "https://authorize.example.com")
os.Setenv("POLICY", "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw==")
os.Setenv("COOKIE_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=")
defer os.Unsetenv("AUTHENTICATE_SERVICE_URL")
defer os.Unsetenv("AUTHORIZE_SERVICE_URL")
defer os.Unsetenv("SHARED_SECRET")
defer os.Unsetenv("COOKIE_SECRET")
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
_, err := newProxyService(tt.s, mux)
if tt.Field != "" {
testOptsField := reflect.ValueOf(testOpts).Elem().FieldByName(tt.Field)
testOptsField.Set(reflect.ValueOf(tt).FieldByName("Value"))
}
_, err := newProxyService(testOpts, mux)
if (err != nil) != tt.wantErr {
t.Errorf("newProxyService() error = %v, wantErr %v", err, tt.wantErr)
return
@ -168,7 +173,7 @@ func Test_newProxyeService(t *testing.T) {
}
func Test_wrapMiddleware(t *testing.T) {
o := &Options{
o := &config.Options{
Services: "all",
Headers: map[string]string{
"X-Content-Type-Options": "nosniff",
@ -197,3 +202,34 @@ func Test_wrapMiddleware(t *testing.T) {
t.Errorf("handler returned unexpected body: got %v want %v", body, expected)
}
}
func Test_parseOptions(t *testing.T) {
tests := []struct {
name string
envKey string
envValue string
wantSharedKey string
wantErr bool
}{
{"no shared secret", "", "", "", true},
{"good", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
got, err := parseOptions()
if (err != nil) != tt.wantErr {
t.Errorf("parseOptions() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil && got.SharedKey != tt.wantSharedKey {
t.Errorf("parseOptions()\n")
t.Errorf("got: %+v\n", got.SharedKey)
t.Errorf("want: %+v\n", tt.wantSharedKey)
}
})
}
}

View file

@ -1,140 +0,0 @@
package main // import "github.com/pomerium/pomerium/cmd/pomerium"
import (
"errors"
"fmt"
"time"
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/internal/log"
)
// DisableHeaderKey is the key used to check whether to disable setting header
const DisableHeaderKey = "disable"
// Options are the global environmental flags used to set up pomerium's services.
// If a base64 encoded certificate and key are not provided as environmental variables,
// or if a file location is not provided, the server will attempt to find a matching keypair
// in the local directory as `./cert.pem` and `./privkey.pem` respectively.
type Options struct {
// Debug outputs human-readable logs to Stdout.
Debug bool `envconfig:"POMERIUM_DEBUG"`
// LogLevel sets the global override for log level. All Loggers will use at least this value.
// Possible options are "info","warn", and "error". Defaults to "debug".
LogLevel string `envconfig:"LOG_LEVEL"`
// SharedKey is the shared secret authorization key used to mutually authenticate
// requests between services.
SharedKey string `envconfig:"SHARED_SECRET"`
// Services is a list enabled service mode. If none are selected, "all" is used.
// Available options are : "all", "authenticate", "proxy".
Services string `envconfig:"SERVICES"`
// Addr specifies the host and port on which the server should serve
// HTTPS requests. If empty, ":https" is used.
Addr string `envconfig:"ADDRESS"`
// Cert and Key specifies the base64 encoded TLS certificates to use.
Cert string `envconfig:"CERTIFICATE"`
Key string `envconfig:"CERTIFICATE_KEY"`
// CertFile and KeyFile specifies the TLS certificates to use.
CertFile string `envconfig:"CERTIFICATE_FILE"`
KeyFile string `envconfig:"CERTIFICATE_KEY_FILE"`
// HttpRedirectAddr, if set, specifies the host and port to run the HTTP
// to HTTPS redirect server on. For example, ":http" would start a server
// on port 80. If empty, no redirect server is started.
HTTPRedirectAddr string `envconfig:"HTTP_REDIRECT_ADDR"`
// Headers to set on all proxied requests. Add a 'disable' key map to turn off.
Headers map[string]string `envconfig:"HEADERS"`
// Timeout settings : https://github.com/pomerium/pomerium/issues/40
ReadTimeout time.Duration `envconfig:"TIMEOUT_READ"`
WriteTimeout time.Duration `envconfig:"TIMEOUT_WRITE"`
ReadHeaderTimeout time.Duration `envconfig:"TIMEOUT_READ_HEADER"`
IdleTimeout time.Duration `envconfig:"TIMEOUT_IDLE"`
}
var defaultOptions = &Options{
Debug: false,
LogLevel: "debug",
Services: "all",
Headers: map[string]string{
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
},
}
// optionsFromEnvConfig builds the main binary's configuration
// options from provided environmental variables
func optionsFromEnvConfig() (*Options, error) {
o := defaultOptions
if err := envconfig.Process("", o); err != nil {
return nil, err
}
if !isValidService(o.Services) {
return nil, fmt.Errorf("%s is an invalid service type", o.Services)
}
if o.SharedKey == "" {
return nil, errors.New("shared-key cannot be empty")
}
if o.Debug {
log.SetDebugMode()
}
if o.LogLevel != "" {
log.SetLevel(o.LogLevel)
}
if _, disable := o.Headers[DisableHeaderKey]; disable {
o.Headers = make(map[string]string)
}
return o, nil
}
// isValidService checks to see if a service is a valid service mode
func isValidService(s string) bool {
switch s {
case
"all",
"proxy",
"authorize",
"authenticate":
return true
}
return false
}
func isAuthenticate(s string) bool {
switch s {
case
"all",
"authenticate":
return true
}
return false
}
func isAuthorize(s string) bool {
switch s {
case
"all",
"authorize":
return true
}
return false
}
func isProxy(s string) bool {
switch s {
case
"all",
"proxy":
return true
}
return false
}

226
internal/config/options.go Normal file
View file

@ -0,0 +1,226 @@
package config
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"time"
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/policy"
)
// DisableHeaderKey is the key used to check whether to disable setting header
const DisableHeaderKey = "disable"
// Options are the global environmental flags used to set up pomerium's services.
// If a base64 encoded certificate and key are not provided as environmental variables,
// or if a file location is not provided, the server will attempt to find a matching keypair
// in the local directory as `./cert.pem` and `./privkey.pem` respectively.
type Options struct {
// Debug outputs human-readable logs to Stdout.
Debug bool `envconfig:"POMERIUM_DEBUG"`
// LogLevel sets the global override for log level. All Loggers will use at least this value.
// Possible options are "info","warn", and "error". Defaults to "debug".
LogLevel string `envconfig:"LOG_LEVEL"`
// SharedKey is the shared secret authorization key used to mutually authenticate
// requests between services.
SharedKey string `envconfig:"SHARED_SECRET"`
// Services is a list enabled service mode. If none are selected, "all" is used.
// Available options are : "all", "authenticate", "proxy".
Services string `envconfig:"SERVICES"`
// Addr specifies the host and port on which the server should serve
// HTTPS requests. If empty, ":https" is used.
Addr string `envconfig:"ADDRESS"`
// Cert and Key specifies the base64 encoded TLS certificates to use.
Cert string `envconfig:"CERTIFICATE"`
Key string `envconfig:"CERTIFICATE_KEY"`
// CertFile and KeyFile specifies the TLS certificates to use.
CertFile string `envconfig:"CERTIFICATE_FILE"`
KeyFile string `envconfig:"CERTIFICATE_KEY_FILE"`
// HttpRedirectAddr, if set, specifies the host and port to run the HTTP
// to HTTPS redirect server on. For example, ":http" would start a server
// on port 80. If empty, no redirect server is started.
HTTPRedirectAddr string `envconfig:"HTTP_REDIRECT_ADDR"`
// Timeout settings : https://github.com/pomerium/pomerium/issues/40
ReadTimeout time.Duration `envconfig:"TIMEOUT_READ"`
WriteTimeout time.Duration `envconfig:"TIMEOUT_WRITE"`
ReadHeaderTimeout time.Duration `envconfig:"TIMEOUT_READ_HEADER"`
IdleTimeout time.Duration `envconfig:"TIMEOUT_IDLE"`
// Policy is a base64 encoded yaml blob which enumerates
// per-route access control policies.
Policy string `envconfig:"POLICY"`
PolicyFile string `envconfig:"POLICY_FILE"`
// AuthenticateURL represents the externally accessible http endpoints
// used for authentication requests and callbacks
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
// Session/Cookie management
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
AuthenticateCookieName string
ProxyCookieName string
CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
CookieSecure bool `envconfig:"COOKIE_SECURE"`
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
// Identity provider configuration variables as specified by RFC6749
// https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
ClientID string `envconfig:"IDP_CLIENT_ID"`
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"`
Provider string `envconfig:"IDP_PROVIDER"`
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
Scopes []string `envconfig:"IDP_SCOPES"`
ServiceAccount string `envconfig:"IDP_SERVICE_ACCOUNT"`
Policies []policy.Policy `envconfig:"POLICY"`
// AuthenticateInternalAddr is used as an override when using a load balancer
// or ingress that does not natively support routing gRPC.
AuthenticateInternalAddr string `envconfig:"AUTHENTICATE_INTERNAL_URL"`
// AuthorizeURL is the routable destination of the authorize service's
// gRPC endpoint. NOTE: As above, many load balancers do not support
// externally routed gRPC so this may be an internal location.
AuthorizeURL *url.URL `envconfig:"AUTHORIZE_SERVICE_URL"`
// Settings to enable custom behind-the-ingress service communication
OverrideCertificateName string `envconfig:"OVERRIDE_CERTIFICATE_NAME"`
CA string `envconfig:"CERTIFICATE_AUTHORITY"`
CAFile string `envconfig:"CERTIFICATE_AUTHORITY_FILE"`
// SigningKey is a base64 encoded private key used to add a JWT-signature.
// https://www.pomerium.io/docs/signed-headers.html
SigningKey string `envconfig:"SIGNING_KEY"`
// Headers to set on all proxied requests. Add a 'disable' key map to turn off.
Headers map[string]string `envconfig:"HEADERS"`
// Sub-routes
Routes map[string]string `envconfig:"ROUTES"`
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
}
// NewOptions returns a new options struct with default vaules
func NewOptions() *Options {
o := &Options{
Debug: false,
LogLevel: "debug",
Services: "all",
AuthenticateCookieName: "_pomerium_authenticate",
CookieHTTPOnly: true,
CookieSecure: true,
CookieExpire: time.Duration(14) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
ProxyCookieName: "_pomerium_proxy",
DefaultUpstreamTimeout: time.Duration(30) * time.Second,
Headers: map[string]string{
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
},
Addr: ":https",
CertFile: filepath.Join(findPwd(), "cert.pem"),
KeyFile: filepath.Join(findPwd(), "privkey.pem"),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 0, // support streaming by default
IdleTimeout: 5 * time.Minute,
AuthenticateURL: new(url.URL),
AuthorizeURL: new(url.URL),
}
return o
}
// OptionsFromEnvConfig builds the main binary's configuration
// options from provided environmental variables
func OptionsFromEnvConfig() (*Options, error) {
o := NewOptions()
if err := envconfig.Process("", o); err != nil {
return nil, err
}
if !IsValidService(o.Services) {
return nil, fmt.Errorf("%s is an invalid service type", o.Services)
}
if o.SharedKey == "" {
return nil, errors.New("shared-key cannot be empty")
}
if o.Debug {
log.SetDebugMode()
}
if o.LogLevel != "" {
log.SetLevel(o.LogLevel)
}
if _, disable := o.Headers[DisableHeaderKey]; disable {
o.Headers = make(map[string]string)
}
return o, nil
}
// findPwd returns best guess at current working directory
func findPwd() string {
p, err := os.Getwd()
if err != nil {
return "."
}
return p
}
// isValidService checks to see if a service is a valid service mode
func IsValidService(s string) bool {
switch s {
case
"all",
"proxy",
"authorize",
"authenticate":
return true
}
return false
}
func IsAuthenticate(s string) bool {
switch s {
case
"all",
"authenticate":
return true
}
return false
}
func IsAuthorize(s string) bool {
switch s {
case
"all",
"authorize":
return true
}
return false
}
func IsProxy(s string) bool {
switch s {
case
"all",
"proxy":
return true
}
return false
}

View file

@ -1,4 +1,4 @@
package main // import "github.com/pomerium/pomerium/cmd/pomerium"
package config
import (
"os"
@ -7,7 +7,7 @@ import (
)
func Test_optionsFromEnvConfig(t *testing.T) {
good := defaultOptions
good := NewOptions()
good.SharedKey = "test"
tests := []struct {
name string
@ -31,7 +31,7 @@ func Test_optionsFromEnvConfig(t *testing.T) {
if tt.envKey != "SHARED_SECRET" {
os.Setenv("SHARED_SECRET", "test")
}
got, err := optionsFromEnvConfig()
got, err := OptionsFromEnvConfig()
os.Unsetenv(tt.envKey)
if (err != nil) != tt.wantErr {
@ -39,7 +39,7 @@ func Test_optionsFromEnvConfig(t *testing.T) {
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("optionsFromEnvConfig() = got %v, want %v", got, tt.want)
t.Errorf("optionsFromEnvConfig() = got %#v,\n want %#v", got, tt.want)
}
})
}
@ -60,7 +60,7 @@ func Test_isValidService(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isValidService(tt.service); got != tt.want {
if got := IsValidService(tt.service); got != tt.want {
t.Errorf("isValidService() = %v, want %v", got, tt.want)
}
})
@ -83,7 +83,7 @@ func Test_isAuthenticate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isAuthenticate(tt.service); got != tt.want {
if got := IsAuthenticate(tt.service); got != tt.want {
t.Errorf("isAuthenticate() = %v, want %v", got, tt.want)
}
})

View file

@ -12,6 +12,7 @@ import (
"testing"
"time"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/proxy/clients"
)
@ -361,7 +362,7 @@ func TestProxy_Proxy(t *testing.T) {
tests := []struct {
name string
options *Options
options *config.Options
method string
header http.Header
host string
@ -371,8 +372,8 @@ func TestProxy_Proxy(t *testing.T) {
wantStatus int
}{
// weirdly, we want 503 here because that means proxy is trying to route a domain (example.com) that we dont control. Weird. I know.
{"good", opts, http.MethodGet, defaultHeaders, "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusServiceUnavailable},
{"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusServiceUnavailable},
{"good", opts, http.MethodGet, defaultHeaders, "https://corp.example.notatld/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadGateway},
{"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://corp.example.notatld/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
// same request as above, but with cors_allow_preflight=false in the policy
{"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
// cors allowed, but the request is missing proper headers

View file

@ -10,9 +10,8 @@ import (
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/internal/config"
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/log"
@ -33,71 +32,9 @@ const (
HeaderGroups = "x-pomerium-authenticated-user-groups"
)
// Options represents the configurations available for the proxy service.
type Options struct {
Policy string `envconfig:"POLICY"`
PolicyFile string `envconfig:"POLICY_FILE"`
// AuthenticateURL represents the externally accessible http endpoints
// used for authentication requests and callbacks
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
// AuthenticateInternalAddr is used as an override when using a load balancer
// or ingress that does not natively support routing gRPC.
AuthenticateInternalAddr string `envconfig:"AUTHENTICATE_INTERNAL_URL"`
// AuthorizeURL is the routable destination of the authorize service's
// gRPC endpoint. NOTE: As above, many load balancers do not support
// externally routed gRPC so this may be an internal location.
AuthorizeURL *url.URL `envconfig:"AUTHORIZE_SERVICE_URL"`
// Settings to enable custom behind-the-ingress service communication
OverrideCertificateName string `envconfig:"OVERRIDE_CERTIFICATE_NAME"`
CA string `envconfig:"CERTIFICATE_AUTHORITY"`
CAFile string `envconfig:"CERTIFICATE_AUTHORITY_FILE"`
// SigningKey is a base64 encoded private key used to add a JWT-signature.
// https://www.pomerium.io/docs/signed-headers.html
SigningKey string `envconfig:"SIGNING_KEY"`
// SharedKey is a 32 byte random key used to authenticate access between services.
SharedKey string `envconfig:"SHARED_SECRET"`
// Session/Cookie management
CookieName string
CookieSecret string `envconfig:"COOKIE_SECRET"`
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
CookieSecure bool `envconfig:"COOKIE_SECURE"`
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
// Sub-routes
Routes map[string]string `envconfig:"ROUTES"`
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
}
// NewOptions returns a new options struct
var defaultOptions = &Options{
CookieName: "_pomerium_proxy",
CookieHTTPOnly: true,
CookieSecure: true,
CookieExpire: time.Duration(14) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
DefaultUpstreamTimeout: time.Duration(30) * time.Second,
}
// OptionsFromEnvConfig builds the identity provider service's configuration
// options from provided environmental variables
func OptionsFromEnvConfig() (*Options, error) {
o := defaultOptions
if err := envconfig.Process("", o); err != nil {
return nil, err
}
return o, nil
}
// Validate checks that proper configuration settings are set to create
// ValidateOptions checks that proper configuration settings are set to create
// a proper Proxy instance
func (o *Options) Validate() error {
func ValidateOptions(o *config.Options) error {
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
if err != nil {
return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err)
@ -187,11 +124,11 @@ type routeConfig struct {
// New takes a Proxy service from options and a validation function.
// Function returns an error if options fail to validate.
func New(opts *Options) (*Proxy, error) {
func New(opts *config.Options) (*Proxy, error) {
if opts == nil {
return nil, errors.New("options cannot be nil")
}
if err := opts.Validate(); err != nil {
if err := ValidateOptions(opts); err != nil {
return nil, err
}
// error explicitly handled by validate
@ -203,7 +140,7 @@ func New(opts *Options) (*Proxy, error) {
cookieStore, err := sessions.NewCookieStore(
&sessions.CookieStoreOptions{
Name: opts.CookieName,
Name: opts.ProxyCookieName,
CookieDomain: opts.CookieDomain,
CookieSecure: opts.CookieSecure,
CookieHTTPOnly: opts.CookieHTTPOnly,
@ -326,11 +263,11 @@ func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
}
// NewReverseProxyHandler applies handler specific options to a given route.
func NewReverseProxyHandler(o *Options, proxy *httputil.ReverseProxy, route *policy.Policy) (http.Handler, error) {
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,
cookieName: o.ProxyCookieName,
}
if len(o.SigningKey) != 0 {
decodedSigningKey, _ := base64.StdEncoding.DecodeString(o.SigningKey)

View file

@ -7,49 +7,16 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"testing"
"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 TestOptionsFromEnvConfig(t *testing.T) {
os.Clearenv()
tests := []struct {
name string
want *Options
envKey string
envValue string
wantErr bool
}{
{"good default, no env settings", defaultOptions, "", "", false},
{"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.ugly", true},
{"good duration", defaultOptions, "COOKIE_REFRESH", "1m", false},
{"bad duration", nil, "COOKIE_REFRESH", "1sm", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envKey != "" {
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
}
got, err := OptionsFromEnvConfig()
if (err != nil) != tt.wantErr {
t.Errorf("OptionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("OptionsFromEnvConfig() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewReverseProxy(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@ -88,7 +55,7 @@ func TestNewReverseProxyHandler(t *testing.T) {
backendHost := net.JoinHostPort(backendHostname, backendPort)
proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
proxyHandler := NewReverseProxy(proxyURL)
opts := defaultOptions
opts := config.NewOptions()
opts.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
route, err := policy.FromConfig([]byte(`[{"from":"corp.example.com","to":"example.com","timeout":"1s"}]`))
if err != nil {
@ -112,22 +79,23 @@ func TestNewReverseProxyHandler(t *testing.T) {
}
}
func testOptions() *Options {
func testOptions() *config.Options {
authenticateService, _ := url.Parse("https://authenticate.corp.beyondperimeter.com")
authorizeService, _ := url.Parse("https://authorize.corp.beyondperimeter.com")
configBlob := `[{"from":"corp.example.com","to":"example.com"}]` //valid yaml
configBlob := `[{"from":"corp.example.notatld","to":"example.notatld"}]` //valid yaml
policy := base64.URLEncoding.EncodeToString([]byte(configBlob))
return &Options{
Policy: policy,
AuthenticateURL: authenticateService,
AuthorizeURL: authorizeService,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieName: "pomerium",
}
opts := config.NewOptions()
opts.Policy = policy
opts.AuthenticateURL = authenticateService
opts.AuthorizeURL = authorizeService
opts.SharedKey = "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="
opts.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
opts.ProxyCookieName = "pomerium"
return opts
}
func testOptionsWithCORS() *Options {
func testOptionsWithCORS() *config.Options {
configBlob := `[{"from":"corp.example.com","to":"example.com","cors_allow_preflight":true}]` //valid yaml
opts := testOptions()
opts.Policy = base64.URLEncoding.EncodeToString([]byte(configBlob))
@ -168,11 +136,11 @@ func TestOptions_Validate(t *testing.T) {
tests := []struct {
name string
o *Options
o *config.Options
wantErr bool
}{
{"good - minimum options", good, false},
{"nil options", &Options{}, true},
{"nil options", &config.Options{}, true},
{"from route", badFromRoute, true},
{"to route", badToRoute, true},
{"authenticate service url", badAuthURL, true},
@ -191,7 +159,7 @@ func TestOptions_Validate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := tt.o
if err := o.Validate(); (err != nil) != tt.wantErr {
if err := ValidateOptions(o); (err != nil) != tt.wantErr {
t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
@ -207,13 +175,13 @@ func TestNew(t *testing.T) {
badRoutedProxy.SigningKey = "YmFkIGtleQo="
tests := []struct {
name string
opts *Options
opts *config.Options
wantProxy bool
numRoutes int
wantErr bool
}{
{"good", good, true, 1, false},
{"empty options", &Options{}, false, 0, true},
{"empty options", &config.Options{}, false, 0, true},
{"nil options", nil, false, 0, true},
{"short secret/validate sanity check", shortCookieLength, false, 0, true},
{"invalid ec key, valid base64 though", badRoutedProxy, false, 0, true},