pomerium/internal/config/options_test.go
2019-07-13 18:28:51 -07:00

410 lines
13 KiB
Go

package config
import (
"encoding/base64"
"fmt"
"io/ioutil"
"net/url"
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/viper"
)
func Test_validate(t *testing.T) {
testOptions := func() Options {
o := defaultOptions
o.SharedKey = "test"
o.Services = "all"
return o
}
good := testOptions()
badServices := testOptions()
badServices.Services = "blue"
badSecret := testOptions()
badSecret.SharedKey = ""
badSecret.Services = "authenticate"
badSecretAllServices := testOptions()
badSecretAllServices.SharedKey = ""
badPolicyFile := testOptions()
badPolicyFile.PolicyFile = "file"
tests := []struct {
name string
testOpts Options
wantErr bool
}{
{"good default with no env settings", good, false},
{"invalid service type", badServices, true},
{"missing shared secret", badSecret, true},
{"missing shared secret but all service", badSecretAllServices, false},
{"policy file specified", badPolicyFile, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.testOpts.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("optionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func Test_bindEnvs(t *testing.T) {
o := &Options{}
os.Clearenv()
defer os.Unsetenv("POMERIUM_DEBUG")
defer os.Unsetenv("POLICY")
defer os.Unsetenv("HEADERS")
os.Setenv("POMERIUM_DEBUG", "true")
os.Setenv("POLICY", "mypolicy")
os.Setenv("HEADERS", `{"X-Custom-1":"foo", "X-Custom-2":"bar"}`)
o.bindEnvs()
err := viper.Unmarshal(o)
if err != nil {
t.Errorf("Could not unmarshal %#v: %s", o, err)
}
if !o.Debug {
t.Errorf("Failed to load POMERIUM_DEBUG from environment")
}
if o.Services != "" {
t.Errorf("Somehow got SERVICES from environment without configuring it")
}
if o.PolicyEnv != "mypolicy" {
t.Errorf("Failed to bind policy env var to PolicyEnv")
}
if o.HeadersEnv != `{"X-Custom-1":"foo", "X-Custom-2":"bar"}` {
t.Errorf("Failed to bind headers env var to HeadersEnv")
}
}
func Test_parseHeaders(t *testing.T) {
tests := []struct {
name string
want map[string]string
envHeaders string
viperHeaders interface{}
wantErr bool
}{
{"good env", map[string]string{"X-Custom-1": "foo", "X-Custom-2": "bar"}, `{"X-Custom-1":"foo", "X-Custom-2":"bar"}`, map[string]string{"X": "foo"}, false},
{"good env not_json", map[string]string{"X-Custom-1": "foo", "X-Custom-2": "bar"}, `X-Custom-1:foo,X-Custom-2:bar`, map[string]string{"X": "foo"}, false},
{"bad env", map[string]string{}, "xyyyy", map[string]string{"X": "foo"}, true},
{"bad env not_json", map[string]string{"X-Custom-1": "foo", "X-Custom-2": "bar"}, `X-Custom-1:foo,X-Custom-2bar`, map[string]string{"X": "foo"}, true},
{"bad viper", map[string]string{}, "", "notaheaderstruct", true},
{"good viper", map[string]string{"X-Custom-1": "foo", "X-Custom-2": "bar"}, "", map[string]string{"X-Custom-1": "foo", "X-Custom-2": "bar"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := defaultOptions
viper.Set("headers", tt.viperHeaders)
viper.Set("HeadersEnv", tt.envHeaders)
o.HeadersEnv = tt.envHeaders
err := o.parseHeaders()
if (err != nil) != tt.wantErr {
t.Errorf("Error condition unexpected: err=%s", err)
}
if !tt.wantErr && !cmp.Equal(tt.want, o.Headers) {
t.Errorf("Did get expected headers: %s", cmp.Diff(tt.want, o.Headers))
}
viper.Reset()
})
}
}
func Test_OptionsFromViper(t *testing.T) {
viper.Reset()
testPolicy := Policy{
To: "https://httpbin.org",
From: "https://pomerium.io",
}
if err := testPolicy.Validate(); err != nil {
t.Fatal(err)
}
testPolicies := []Policy{
testPolicy,
}
goodConfigBytes := []byte(`{"authorize_service_url":"https://authorize.corp.example","authenticate_service_url":"https://authenticate.corp.example","shared_secret":"Setec Astronomy","service":"all","policy":[{"from":"https://pomerium.io","to":"https://httpbin.org"}]}`)
goodOptions := defaultOptions
goodOptions.SharedKey = "Setec Astronomy"
goodOptions.Services = "all"
goodOptions.Policies = testPolicies
goodOptions.CookieName = "oatmeal"
goodOptions.AuthorizeURLString = "https://authorize.corp.example"
goodOptions.AuthenticateURLString = "https://authenticate.corp.example"
authorize, err := url.Parse(goodOptions.AuthorizeURLString)
if err != nil {
t.Fatal(err)
}
authenticate, err := url.Parse(goodOptions.AuthenticateURLString)
if err != nil {
t.Fatal(err)
}
goodOptions.AuthorizeURL = authorize
goodOptions.AuthenticateURL = authenticate
if err := goodOptions.Validate(); err != nil {
t.Fatal(err)
}
badConfigBytes := []byte("badjson!")
badUnmarshalConfigBytes := []byte(`"debug": "blue"`)
tests := []struct {
name string
configBytes []byte
want *Options
wantErr bool
}{
{"good", goodConfigBytes, &goodOptions, false},
{"bad json", badConfigBytes, nil, true},
{"bad unmarshal", badUnmarshalConfigBytes, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
viper.Reset()
os.Clearenv()
os.Setenv("COOKIE_NAME", "oatmeal")
defer os.Unsetenv("COOKIE_NAME")
tempFile, _ := ioutil.TempFile("", "*.json")
defer tempFile.Close()
defer os.Remove(tempFile.Name())
tempFile.Write(tt.configBytes)
got, err := OptionsFromViper(tempFile.Name())
if (err != nil) != tt.wantErr {
t.Errorf("OptionsFromViper() error = \n%v, wantErr \n%v", err, tt.wantErr)
}
if tt.want != nil {
if err := tt.want.Validate(); err != nil {
t.Fatal(err)
}
}
if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("OptionsFromViper() = \n%s\n, \ngot\n%+v\n, want \n%+v", diff, got, tt.want)
}
})
}
// Test for missing config file
_, err = OptionsFromViper("filedoesnotexist")
if err == nil {
t.Errorf("OptionsFromViper(): Did when loading missing file")
}
}
func Test_parsePolicyEnv(t *testing.T) {
t.Parallel()
viper.Reset()
source := "https://pomerium.io"
sourceURL, _ := url.ParseRequestURI(source)
dest := "https://httpbin.org"
destURL, _ := url.ParseRequestURI(dest)
tests := []struct {
name string
policyBytes []byte
want []Policy
wantErr bool
}{
{"simple json", []byte(fmt.Sprintf(`[{"from": "%s","to":"%s"}]`, source, dest)), []Policy{{From: source, To: dest, Source: sourceURL, Destination: destURL}}, false},
{"bad from", []byte(`[{"from": "%","to":"httpbin.org"}]`), []Policy{{From: "%", To: "httpbin.org"}}, true},
{"bad to", []byte(`[{"from": "pomerium.io","to":"%"}]`), []Policy{{From: "pomerium.io", To: "%"}}, true},
{"simple error", []byte(`{}`), nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := new(Options)
o.PolicyEnv = base64.StdEncoding.EncodeToString(tt.policyBytes)
err := o.parsePolicy()
if (err != nil) != tt.wantErr {
t.Errorf("parsePolicyEnv() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(o.Policies, tt.want); diff != "" {
t.Errorf("parsePolicyEnv() = %s", diff)
}
})
}
// Catch bad base64
o := new(Options)
o.PolicyEnv = "foo"
err := o.parsePolicy()
if err == nil {
t.Errorf("parsePolicyEnv() did not catch bad base64 %v", o)
}
}
func Test_parsePolicyFile(t *testing.T) {
viper.Reset()
source := "https://pomerium.io"
sourceURL, _ := url.ParseRequestURI(source)
dest := "https://httpbin.org"
destURL, _ := url.ParseRequestURI(dest)
tests := []struct {
name string
policyBytes []byte
want []Policy
wantErr bool
}{
{"simple json", []byte(fmt.Sprintf(`{"policy":[{"from": "%s","to":"%s"}]}`, source, dest)), []Policy{{From: source, To: dest, Source: sourceURL, Destination: destURL}}, false},
{"bad from", []byte(`{"policy":[{"from": "%","to":"httpbin.org"}]}`), nil, true},
{"bad to", []byte(`{"policy":[{"from": "pomerium.io","to":"%"}]}`), nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "*.json")
defer tempFile.Close()
defer os.Remove(tempFile.Name())
tempFile.Write(tt.policyBytes)
o := new(Options)
viper.SetConfigFile(tempFile.Name())
if err := viper.ReadInConfig(); err != nil {
t.Fatal(err)
}
err := o.parsePolicy()
if (err != nil) != tt.wantErr {
t.Errorf("parsePolicyEnv() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil {
if diff := cmp.Diff(o.Policies, tt.want); diff != "" {
t.Errorf("parsePolicyEnv() = diff:%s", diff)
}
}
})
}
}
func Test_Checksum(t *testing.T) {
o := defaultOptions
oldChecksum := o.Checksum()
o.SharedKey = "changemeplease"
newChecksum := o.Checksum()
if newChecksum == oldChecksum {
t.Errorf("Checksum() failed to update old = %s, new = %s", oldChecksum, newChecksum)
}
if newChecksum == "" || oldChecksum == "" {
t.Error("Checksum() not returning data")
}
if o.Checksum() != newChecksum {
t.Error("Checksum() inconsistent")
}
}
func TestNewOptions(t *testing.T) {
viper.Reset()
tests := []struct {
name string
authenticateURL string
authorizeURL string
want *Options
wantErr bool
}{
{"good", "https://authenticate.example", "https://authorize.example", nil, false},
{"bad authenticate url no scheme", "authenticate.example", "https://authorize.example", nil, true},
{"bad authenticate url no host", "https://", "https://authorize.example", nil, true},
{"bad authorize url no scheme", "https://authenticate.example", "authorize.example", nil, true},
{"bad authorize url no host", "https://authenticate.example", "https://", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewOptions(tt.authenticateURL, tt.authorizeURL)
if (err != nil) != tt.wantErr {
t.Errorf("NewOptions() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestOptionsFromViper(t *testing.T) {
opts := []cmp.Option{
cmpopts.IgnoreFields(Options{}, "AuthenticateInternalAddr", "DefaultUpstreamTimeout", "CookieRefresh", "CookieExpire", "Services", "Addr", "RefreshCooldown", "LogLevel", "KeyFile", "CertFile", "SharedKey", "ReadTimeout", "ReadHeaderTimeout", "IdleTimeout"),
cmpopts.IgnoreFields(Policy{}, "Source", "Destination"),
}
tests := []struct {
name string
configBytes []byte
want *Options
wantErr bool
}{
{"good",
[]byte(`{"policy":[{"from": "https://from.example","to":"https://to.example"}]}`),
&Options{
Policies: []Policy{{From: "https://from.example", To: "https://to.example"}},
CookieName: "_pomerium",
CookieSecure: true,
CookieHTTPOnly: true,
Headers: map[string]string{
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"X-XSS-Protection": "1; mode=block",
}},
false},
{"good with authenticate internal url",
[]byte(`{"authenticate_internal_url": "https://internal.example","policy":[{"from": "https://from.example","to":"https://to.example"}]}`),
&Options{
AuthenticateInternalAddrString: "https://internal.example",
Policies: []Policy{{From: "https://from.example", To: "https://to.example"}},
CookieName: "_pomerium",
CookieSecure: true,
CookieHTTPOnly: true,
Headers: map[string]string{
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"X-XSS-Protection": "1; mode=block",
}},
false},
{"good disable header",
[]byte(`{"headers": {"disable":"true"},"policy":[{"from": "https://from.example","to":"https://to.example"}]}`),
&Options{
Policies: []Policy{{From: "https://from.example", To: "https://to.example"}},
CookieName: "_pomerium",
CookieSecure: true,
CookieHTTPOnly: true,
Headers: map[string]string{}},
false},
{"bad authenticate internal url", []byte(`{"authenticate_internal_url": "internal.example","policy":[{"from": "https://from.example","to":"https://to.example"}]}`), nil, true},
{"bad url", []byte(`{"policy":[{"from": "https://","to":"https://to.example"}]}`), nil, true},
{"bad policy", []byte(`{"policy":[{"allow_public_unauthenticated_access": "dog","to":"https://to.example"}]}`), nil, true},
{"bad file", []byte(`{''''}`), nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "*.json")
defer tempFile.Close()
defer os.Remove(tempFile.Name())
tempFile.Write(tt.configBytes)
got, err := OptionsFromViper(tempFile.Name())
if (err != nil) != tt.wantErr {
t.Errorf("OptionsFromViper() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(got, tt.want, opts...); diff != "" {
t.Errorf("NewOptions() = %s", diff)
}
})
}
}