mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-01 18:33:19 +02:00
Merge pull request #202 from desimone/feature/self-signed-support
internal/config: refactor option parsing (#155)
This commit is contained in:
commit
5981f0510c
38 changed files with 1354 additions and 1079 deletions
|
@ -6,11 +6,15 @@
|
|||
|
||||
- Add programmatic authentication support. [GH-177]
|
||||
- Add Prometheus format metrics endpoint. [GH-35]
|
||||
- Add policy setting to enable self-signed certificate support. [GH-179]
|
||||
- Add policy setting to skip tls certificate verification. [GH-179]
|
||||
|
||||
### CHANGED
|
||||
|
||||
- Policy `to` and `from` settings must be set to valid HTTP URLs including [schemes](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) and hostnames (e.g. `http.corp.domain.example` should now be `https://http.corp.domain.example`).
|
||||
- Proxy's sign out handler `{}/.pomerium/sign_out` now accepts an optional `redirect_uri` parameter which can be used to specify a custom redirect page, so long as it is under the same top-level domain. [GH-183]
|
||||
- Policy configuration can now be empty at startup [GH-190]
|
||||
- Policy configuration can now be empty at startup. [GH-190]
|
||||
- Websocket support is now set per-route instead of globally. [GH-204]
|
||||
|
||||
### FIXED
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import (
|
|||
// The checks do not modify the internal state of the Option structure. Returns
|
||||
// on first error found.
|
||||
func ValidateOptions(o config.Options) error {
|
||||
if o.AuthenticateURL.Hostname() == "" {
|
||||
if o.AuthenticateURL == nil {
|
||||
return errors.New("authenticate: 'AUTHENTICATE_SERVICE_URL' missing")
|
||||
}
|
||||
if o.ClientID == "" {
|
||||
|
@ -35,7 +35,7 @@ func ValidateOptions(o config.Options) error {
|
|||
return fmt.Errorf("authenticate: 'COOKIE_SECRET' must be base64 encoded: %v", err)
|
||||
}
|
||||
if len(decodedCookieSecret) != 32 {
|
||||
return fmt.Errorf("authenticate: 'COOKIE_SECRET' should be 32; got %d", len(decodedCookieSecret))
|
||||
return fmt.Errorf("authenticate: 'COOKIE_SECRET' %s be 32; got %d", o.CookieSecret, len(decodedCookieSecret))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ func New(opts config.Options) (*Authenticate, error) {
|
|||
provider, err := identity.New(
|
||||
opts.Provider,
|
||||
&identity.Provider{
|
||||
RedirectURL: &redirectURL,
|
||||
RedirectURL: redirectURL,
|
||||
ProviderName: opts.Provider,
|
||||
ProviderURL: opts.ProviderURL,
|
||||
ClientID: opts.ClientID,
|
||||
|
@ -97,7 +97,7 @@ func New(opts config.Options) (*Authenticate, error) {
|
|||
}
|
||||
return &Authenticate{
|
||||
SharedKey: opts.SharedKey,
|
||||
RedirectURL: &redirectURL,
|
||||
RedirectURL: redirectURL,
|
||||
templates: templates.New(),
|
||||
csrfStore: cookieStore,
|
||||
sessionStore: cookieStore,
|
||||
|
|
|
@ -1,53 +1,49 @@
|
|||
package authenticate
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
)
|
||||
|
||||
func testOptions() config.Options {
|
||||
redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
|
||||
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,
|
||||
CookieName: "pomerium",
|
||||
func newTestOptions(t *testing.T) *config.Options {
|
||||
opts, err := config.NewOptions("https://authenticate.example", "https://authorize.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
opts.ClientID = "client-id"
|
||||
opts.Provider = "google"
|
||||
opts.ClientSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
|
||||
opts.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
|
||||
return opts
|
||||
}
|
||||
|
||||
func TestOptions_Validate(t *testing.T) {
|
||||
good := testOptions()
|
||||
badRedirectURL := testOptions()
|
||||
badRedirectURL.AuthenticateURL = url.URL{}
|
||||
emptyClientID := testOptions()
|
||||
good := newTestOptions(t)
|
||||
badRedirectURL := newTestOptions(t)
|
||||
badRedirectURL.AuthenticateURL = nil
|
||||
emptyClientID := newTestOptions(t)
|
||||
emptyClientID.ClientID = ""
|
||||
emptyClientSecret := testOptions()
|
||||
emptyClientSecret := newTestOptions(t)
|
||||
emptyClientSecret.ClientSecret = ""
|
||||
emptyCookieSecret := testOptions()
|
||||
emptyCookieSecret := newTestOptions(t)
|
||||
emptyCookieSecret.CookieSecret = ""
|
||||
invalidCookieSecret := testOptions()
|
||||
invalidCookieSecret := newTestOptions(t)
|
||||
invalidCookieSecret.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw^"
|
||||
shortCookieLength := testOptions()
|
||||
shortCookieLength := newTestOptions(t)
|
||||
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
|
||||
badSharedKey := testOptions()
|
||||
badSharedKey := newTestOptions(t)
|
||||
badSharedKey.SharedKey = ""
|
||||
badAuthenticateURL := testOptions()
|
||||
badAuthenticateURL.AuthenticateURL = url.URL{}
|
||||
badAuthenticateURL := newTestOptions(t)
|
||||
badAuthenticateURL.AuthenticateURL = nil
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
o config.Options
|
||||
o *config.Options
|
||||
wantErr bool
|
||||
}{
|
||||
{"minimum options", good, false},
|
||||
{"nil options", config.Options{}, true},
|
||||
{"nil options", &config.Options{}, true},
|
||||
{"bad redirect url", badRedirectURL, true},
|
||||
{"no cookie secret", emptyCookieSecret, true},
|
||||
{"invalid cookie secret", invalidCookieSecret, true},
|
||||
|
@ -59,8 +55,7 @@ func TestOptions_Validate(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := tt.o
|
||||
if err := ValidateOptions(o); (err != nil) != tt.wantErr {
|
||||
if err := ValidateOptions(*tt.o); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
|
@ -68,25 +63,24 @@ func TestOptions_Validate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
good := testOptions()
|
||||
good.Provider = "google"
|
||||
good := newTestOptions(t)
|
||||
|
||||
badRedirectURL := testOptions()
|
||||
badRedirectURL.AuthenticateURL = url.URL{}
|
||||
badRedirectURL := newTestOptions(t)
|
||||
badRedirectURL.AuthenticateURL = nil
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts config.Options
|
||||
opts *config.Options
|
||||
// want *Authenticate
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", good, false},
|
||||
{"empty opts", config.Options{}, true},
|
||||
{"empty opts", &config.Options{}, true},
|
||||
{"fails to validate", badRedirectURL, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := New(tt.opts)
|
||||
_, err := New(*tt.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
|
|
@ -309,5 +309,4 @@ func (a *Authenticate) ExchangeToken(w http.ResponseWriter, r *http.Request) {
|
|||
httputil.ErrorResponse(w, r, &httputil.Error{Code: http.StatusInternalServerError, Message: "authenticate: failed returning new session"})
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,11 +4,8 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
)
|
||||
|
||||
// ValidateOptions checks to see if configuration values are valid for the
|
||||
|
@ -21,7 +18,6 @@ func ValidateOptions(o config.Options) error {
|
|||
if len(decoded) != 32 {
|
||||
return fmt.Errorf("authorize: `SHARED_SECRET` want 32 but got %d bytes", len(decoded))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -50,7 +46,7 @@ func New(opts config.Options) (*Authorize, error) {
|
|||
|
||||
// NewIdentityWhitelist returns an indentity validator.
|
||||
// todo(bdd) : a radix-tree implementation is probably more efficient
|
||||
func NewIdentityWhitelist(policies []policy.Policy, admins []string) IdentityValidator {
|
||||
func NewIdentityWhitelist(policies []config.Policy, admins []string) IdentityValidator {
|
||||
return newIdentityWhitelistMap(policies, admins)
|
||||
}
|
||||
|
||||
|
@ -59,7 +55,7 @@ func (a *Authorize) ValidIdentity(route string, identity *Identity) bool {
|
|||
return a.identityAccess.Valid(route, identity)
|
||||
}
|
||||
|
||||
// UpdateOptions updates internal structres based on config.Options
|
||||
// UpdateOptions updates internal structures based on config.Options
|
||||
func (a *Authorize) UpdateOptions(o config.Options) error {
|
||||
log.Info().Msg("authorize: updating options")
|
||||
a.identityAccess = NewIdentityWhitelist(o.Policies, o.Administrators)
|
||||
|
|
|
@ -4,26 +4,24 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
policies := testPolicies()
|
||||
policies := testPolicies(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
SharedKey string
|
||||
Policies []policy.Policy
|
||||
Policies []config.Policy
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", policies, false},
|
||||
{"bad shared secret", "AZA85podM73CjLCjViDNz1EUvvejKpWp7Hysr0knXA==", policies, true},
|
||||
{"really bad shared secret", "sup", policies, true},
|
||||
{"validation error, short secret", "AZA85podM73CjLCjViDNz1EUvvejKpWp7Hysr0knXA==", policies, true},
|
||||
{"empty options", "", []policy.Policy{}, true}, // special case
|
||||
{"missing policies", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", []policy.Policy{}, false}, // special case
|
||||
{"empty options", "", []config.Policy{}, true}, // special case
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -43,10 +41,13 @@ func TestNew(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func testPolicies() []policy.Policy {
|
||||
testPolicy := policy.Policy{From: "pomerium.io", To: "httpbin.org", AllowedEmails: []string{"test@gmail.com"}}
|
||||
testPolicy.Validate()
|
||||
policies := []policy.Policy{
|
||||
func testPolicies(t *testing.T) []config.Policy {
|
||||
testPolicy := config.Policy{From: "https://pomerium.io", To: "http://httpbin.org", AllowedEmails: []string{"test@gmail.com"}}
|
||||
err := testPolicy.Validate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
policies := []config.Policy{
|
||||
testPolicy,
|
||||
}
|
||||
|
||||
|
@ -55,31 +56,39 @@ func testPolicies() []policy.Policy {
|
|||
|
||||
func Test_UpdateOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
policies := testPolicies()
|
||||
newPolicy := policy.Policy{From: "foo.notatld", To: "bar.notatld", AllowedEmails: []string{"test@gmail.com"}}
|
||||
newPolicy.Validate()
|
||||
newPolicies := []policy.Policy{
|
||||
policies := testPolicies(t)
|
||||
newPolicy := config.Policy{From: "https://source.example", To: "http://destination.example", AllowedEmails: []string{"test@gmail.com"}}
|
||||
if err := newPolicy.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newPolicies := []config.Policy{
|
||||
newPolicy,
|
||||
}
|
||||
identity := &Identity{Email: "test@gmail.com"}
|
||||
tests := []struct {
|
||||
name string
|
||||
SharedKey string
|
||||
Policies []policy.Policy
|
||||
newPolices []policy.Policy
|
||||
Policies []config.Policy
|
||||
newPolices []config.Policy
|
||||
route string
|
||||
wantAllowed bool
|
||||
}{
|
||||
{"good", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", policies, policies, "pomerium.io", true},
|
||||
{"changed", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", policies, newPolicies, "foo.notatld", true},
|
||||
{"changed", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", policies, newPolicies, "source.example", true},
|
||||
{"changed and missing", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", policies, newPolicies, "pomerium.io", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := config.Options{SharedKey: tt.SharedKey, Policies: tt.Policies}
|
||||
authorize, _ := New(o)
|
||||
authorize, err := New(o)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
o.Policies = tt.newPolices
|
||||
authorize.UpdateOptions(o)
|
||||
if err := authorize.UpdateOptions(o); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
allowed := authorize.ValidIdentity(tt.route, identity)
|
||||
if allowed != tt.wantAllowed {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
)
|
||||
|
||||
// Identity contains a user's identity information.
|
||||
|
@ -55,28 +55,24 @@ type whitelist struct {
|
|||
|
||||
// newIdentityWhitelistMap takes a slice of policies and creates a hashmap of identity
|
||||
// authorizations per-route for each allowed group, domain, and email.
|
||||
func newIdentityWhitelistMap(policies []policy.Policy, admins []string) *whitelist {
|
||||
|
||||
policyCount := len(policies)
|
||||
if policyCount == 0 {
|
||||
log.Warn().Msg("authorize: loaded configuration with no policies specified")
|
||||
func newIdentityWhitelistMap(policies []config.Policy, admins []string) *whitelist {
|
||||
if len(policies) == 0 {
|
||||
log.Warn().Msg("authorize: loaded configuration with no policies")
|
||||
}
|
||||
log.Info().Int("policy-count", policyCount).Msg("authorize: updated policies")
|
||||
|
||||
var wl whitelist
|
||||
wl.access = make(map[string]bool, len(policies)*3)
|
||||
for _, p := range policies {
|
||||
for _, group := range p.AllowedGroups {
|
||||
wl.PutGroup(p.From, group)
|
||||
log.Debug().Str("route", p.From).Str("group", group).Msg("add group")
|
||||
wl.PutGroup(p.Source.Host, group)
|
||||
log.Debug().Str("route", p.Source.Host).Str("group", group).Msg("add group")
|
||||
}
|
||||
for _, domain := range p.AllowedDomains {
|
||||
wl.PutDomain(p.From, domain)
|
||||
log.Debug().Str("route", p.From).Str("domain", domain).Msg("add domain")
|
||||
wl.PutDomain(p.Source.Host, domain)
|
||||
log.Debug().Str("route", p.Source.Host).Str("domain", domain).Msg("add domain")
|
||||
}
|
||||
for _, email := range p.AllowedEmails {
|
||||
wl.PutEmail(p.From, email)
|
||||
log.Debug().Str("route", p.From).Str("email", email).Msg("add email")
|
||||
wl.PutEmail(p.Source.Host, email)
|
||||
log.Debug().Str("route", p.Source.Host).Str("email", email).Msg("add email")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package authorize
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
)
|
||||
|
||||
func TestIdentity_EmailDomain(t *testing.T) {
|
||||
|
@ -32,40 +32,46 @@ func Test_IdentityWhitelistMap(t *testing.T) {
|
|||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
policies []policy.Policy
|
||||
policies []config.Policy
|
||||
route string
|
||||
Identity *Identity
|
||||
admins []string
|
||||
want bool
|
||||
}{
|
||||
{"valid domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, nil, true},
|
||||
{"valid domain with admins", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, []string{"admin@example.com"}, true},
|
||||
{"invalid domain prepend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "a@1example.com"}, nil, false},
|
||||
{"invalid domain postpend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com2"}, nil, false},
|
||||
{"valid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, nil, true},
|
||||
{"invalid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, nil, false},
|
||||
{"invalid empty", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{""}}, nil, false},
|
||||
{"valid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, nil, true},
|
||||
{"invalid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, nil, false},
|
||||
{"valid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, nil, true},
|
||||
{"invalid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user2@example.com"}, nil, false},
|
||||
{"empty everything", []policy.Policy{{From: "example.com"}}, "example.com", &Identity{Email: "user2@example.com"}, nil, false},
|
||||
{"empty policy", []policy.Policy{}, "example.com", &Identity{Email: "user@example.com"}, nil, false},
|
||||
{"valid domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, true},
|
||||
{"valid domain with admins", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, []string{"admin@example.com"}, true},
|
||||
{"invalid domain prepend", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "a@1example.com"}, nil, false},
|
||||
{"invalid domain postpend", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "user@example.com2"}, nil, false},
|
||||
{"valid group", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, nil, true},
|
||||
{"invalid group", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, nil, false},
|
||||
{"invalid empty", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{""}}, nil, false},
|
||||
{"valid group multiple", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, nil, true},
|
||||
{"invalid group multiple", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"admin"}}}, "from.example", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, nil, false},
|
||||
{"valid user email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedEmails: []string{"user@example.com"}}}, "from.example", &Identity{Email: "user@example.com"}, nil, true},
|
||||
{"invalid user email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedEmails: []string{"user@example.com"}}}, "from.example", &Identity{Email: "user2@example.com"}, nil, false},
|
||||
{"empty everything", []config.Policy{{From: "https://from.example", To: "https://to.example"}}, "from.example", &Identity{Email: "user2@example.com"}, nil, false},
|
||||
{"empty policy", []config.Policy{}, "from.example", &Identity{Email: "user2@example.com"}, nil, false},
|
||||
// impersonation related
|
||||
{"admin not impersonating allowed", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@example.com"}, []string{"admin@example.com"}, true},
|
||||
{"admin not impersonating denied", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com"}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating match domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating does not match domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating match email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating does not match email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating match groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"support"}}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating match many groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"a", "b", "c", "support"}}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating does not match groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support"}}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating does not match many groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support", "b", "c"}}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating does not match empty groups", []policy.Policy{{From: "example.com", AllowedGroups: []string{"support"}}}, "example.com", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{""}}, []string{"admin@admin-domain.com"}, false},
|
||||
{"admin not impersonating allowed", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@example.com"}, []string{"admin@example.com"}, true},
|
||||
{"admin not impersonating denied", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com"}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating match domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating does not match domain", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedDomains: []string{"example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating match email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedEmails: []string{"user@example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@example.com"}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating does not match email", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedEmails: []string{"user@example.com"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateEmail: "user@not-example.com"}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating match groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"support"}}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating match many groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"a", "b", "c", "support"}}, []string{"admin@admin-domain.com"}, true},
|
||||
{"impersonating does not match groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support"}}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating does not match many groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{"not support", "b", "c"}}, []string{"admin@admin-domain.com"}, false},
|
||||
{"impersonating does not match empty groups", []config.Policy{{From: "https://from.example", To: "https://to.example", AllowedGroups: []string{"support"}}}, "from.example", &Identity{Email: "admin@admin-domain.com", ImpersonateGroups: []string{""}}, []string{"admin@admin-domain.com"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for i := range tt.policies {
|
||||
if err := (&tt.policies[i]).Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
wl := NewIdentityWhitelist(tt.policies, tt.admins)
|
||||
if got := wl.Valid(tt.route, tt.Identity); got != tt.want {
|
||||
t.Errorf("wl.Valid() = %v, want %v", got, tt.want)
|
||||
|
|
|
@ -15,7 +15,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/httputil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/metrics"
|
||||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
|
@ -46,17 +46,17 @@ func main() {
|
|||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
_, err = newAuthenticateService(opt, mux, grpcServer)
|
||||
_, err = newAuthenticateService(*opt, mux, grpcServer)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cmd/pomerium: authenticate")
|
||||
}
|
||||
|
||||
authz, err := newAuthorizeService(opt, grpcServer)
|
||||
authz, err := newAuthorizeService(*opt, grpcServer)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cmd/pomerium: authorize")
|
||||
}
|
||||
|
||||
proxy, err := newProxyService(opt, mux)
|
||||
proxy, err := newProxyService(*opt, mux)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cmd/pomerium: proxy")
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ func main() {
|
|||
// defer proxyService.AuthenticateClient.Close()
|
||||
// defer proxyService.AuthorizeClient.Close()
|
||||
|
||||
httpOpts := &https.Options{
|
||||
httpOpts := &httputil.Options{
|
||||
Addr: opt.Addr,
|
||||
Cert: opt.Cert,
|
||||
Key: opt.Key,
|
||||
|
@ -95,7 +95,7 @@ func main() {
|
|||
defer srv.Close()
|
||||
}
|
||||
|
||||
if err := https.ListenAndServeTLS(httpOpts, wrapMiddleware(opt, mux), grpcServer); err != nil {
|
||||
if err := httputil.ListenAndServeTLS(httpOpts, wrapMiddleware(opt, mux), grpcServer); err != nil {
|
||||
log.Fatal().Err(err).Msg("cmd/pomerium: https server")
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ func newPromListener(addr string) {
|
|||
log.Error().Err(metrics.NewPromHTTPListener(addr)).Str("MetricsAddr", addr).Msg("cmd/pomerium: could not start metrics exporter")
|
||||
}
|
||||
|
||||
func wrapMiddleware(o config.Options, mux *http.ServeMux) http.Handler {
|
||||
func wrapMiddleware(o *config.Options, mux *http.ServeMux) http.Handler {
|
||||
c := middleware.NewChain()
|
||||
c = c.Append(metrics.HTTPMetricsHandler("proxy"))
|
||||
c = c.Append(log.NewHandler(log.Logger))
|
||||
|
@ -194,10 +194,10 @@ func wrapMiddleware(o config.Options, mux *http.ServeMux) http.Handler {
|
|||
return c.Then(mux)
|
||||
}
|
||||
|
||||
func parseOptions(configFile string) (config.Options, error) {
|
||||
func parseOptions(configFile string) (*config.Options, error) {
|
||||
o, err := config.OptionsFromViper(configFile)
|
||||
if err != nil {
|
||||
return o, err
|
||||
return nil, err
|
||||
}
|
||||
if o.Debug {
|
||||
log.SetDebugMode()
|
||||
|
@ -209,8 +209,12 @@ func parseOptions(configFile string) (config.Options, error) {
|
|||
return o, nil
|
||||
}
|
||||
|
||||
func handleConfigUpdate(opt config.Options, services []config.OptionsUpdater) config.Options {
|
||||
func handleConfigUpdate(opt *config.Options, services []config.OptionsUpdater) *config.Options {
|
||||
newOpt, err := parseOptions(*configFile)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cmd/pomerium: could not reload configuration")
|
||||
return opt
|
||||
}
|
||||
optChecksum := opt.Checksum()
|
||||
newOptChecksum := newOpt.Checksum()
|
||||
|
||||
|
@ -224,22 +228,10 @@ func handleConfigUpdate(opt config.Options, services []config.OptionsUpdater) co
|
|||
return opt
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("cmd/pomerium: could not reload configuration")
|
||||
return opt
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("config-checksum", newOptChecksum).
|
||||
Msg("cmd/pomerium: running configuration has changed")
|
||||
log.Info().Str("checksum", newOptChecksum).Msg("cmd/pomerium: checksum changed")
|
||||
for _, service := range services {
|
||||
err := service.UpdateOptions(newOpt)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("cmd/pomerium: could not update options")
|
||||
if err := service.UpdateOptions(*newOpt); err != nil {
|
||||
log.Error().Err(err).Msg("cmd/pomerium: could not update options")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,6 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/config"
|
||||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"google.golang.org/grpc"
|
||||
|
@ -72,21 +70,22 @@ func Test_newAuthenticateService(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authURL, _ := url.Parse("http://auth.server.com")
|
||||
testOpts := config.NewOptions()
|
||||
testOpts, err := config.NewOptions("https://authenticate.example", "https://authorize.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testOpts.Provider = "google"
|
||||
testOpts.ClientSecret = "TEST"
|
||||
testOpts.SharedKey = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
|
||||
testOpts.CookieSecret = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
|
||||
testOpts.AuthenticateURL = *authURL
|
||||
testOpts.Services = tt.s
|
||||
|
||||
if tt.Field != "" {
|
||||
testOptsField := reflect.ValueOf(&testOpts).Elem().FieldByName(tt.Field)
|
||||
testOptsField := reflect.ValueOf(testOpts).Elem().FieldByName(tt.Field)
|
||||
testOptsField.Set(reflect.ValueOf(tt).FieldByName("Value"))
|
||||
}
|
||||
|
||||
_, err := newAuthenticateService(testOpts, mux, grpcServer)
|
||||
_, err = newAuthenticateService(*testOpts, mux, grpcServer)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("newAuthenticateService() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
@ -116,21 +115,26 @@ func Test_newAuthorizeService(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testOpts := config.NewOptions()
|
||||
testOpts, err := config.NewOptions("https://some.example", "https://some.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testOpts.Services = tt.s
|
||||
testOpts.CookieSecret = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
|
||||
testPolicy := policy.Policy{From: "pomerium.io", To: "httpbin.org"}
|
||||
testPolicy.Validate()
|
||||
testOpts.Policies = []policy.Policy{
|
||||
testPolicy := config.Policy{From: "http://some.example", To: "https://some.example"}
|
||||
if err := testPolicy.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testOpts.Policies = []config.Policy{
|
||||
testPolicy,
|
||||
}
|
||||
|
||||
if tt.Field != "" {
|
||||
testOptsField := reflect.ValueOf(&testOpts).Elem().FieldByName(tt.Field)
|
||||
testOptsField := reflect.ValueOf(testOpts).Elem().FieldByName(tt.Field)
|
||||
testOptsField.Set(reflect.ValueOf(tt).FieldByName("Value"))
|
||||
}
|
||||
|
||||
_, err := newAuthorizeService(testOpts, grpcServer)
|
||||
_, err = newAuthorizeService(*testOpts, grpcServer)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("newAuthorizeService() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
@ -156,26 +160,31 @@ func Test_newProxyeService(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
testOpts := config.NewOptions()
|
||||
testPolicy := policy.Policy{From: "pomerium.io", To: "httpbin.org"}
|
||||
testPolicy.Validate()
|
||||
testOpts.Policies = []policy.Policy{
|
||||
testOpts, err := config.NewOptions("https://authenticate.example", "https://authorize.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testPolicy := config.Policy{From: "http://some.example", To: "http://some.example"}
|
||||
if err := testPolicy.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testOpts.Policies = []config.Policy{
|
||||
testPolicy,
|
||||
}
|
||||
|
||||
AuthenticateURL, _ := url.Parse("https://authenticate.example.com")
|
||||
AuthorizeURL, _ := url.Parse("https://authorize.example.com")
|
||||
|
||||
testOpts.AuthenticateURL = *AuthenticateURL
|
||||
testOpts.AuthorizeURL = *AuthorizeURL
|
||||
testOpts.AuthenticateURL = AuthenticateURL
|
||||
testOpts.AuthorizeURL = AuthorizeURL
|
||||
testOpts.CookieSecret = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
|
||||
testOpts.Services = tt.s
|
||||
|
||||
if tt.Field != "" {
|
||||
testOptsField := reflect.ValueOf(&testOpts).Elem().FieldByName(tt.Field)
|
||||
testOptsField := reflect.ValueOf(testOpts).Elem().FieldByName(tt.Field)
|
||||
testOptsField.Set(reflect.ValueOf(tt).FieldByName("Value"))
|
||||
}
|
||||
_, err := newProxyService(testOpts, mux)
|
||||
_, err = newProxyService(*testOpts, mux)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("newProxyService() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
@ -205,7 +214,7 @@ func Test_wrapMiddleware(t *testing.T) {
|
|||
})
|
||||
|
||||
mux.Handle("/404", h)
|
||||
out := wrapMiddleware(o, mux)
|
||||
out := wrapMiddleware(&o, mux)
|
||||
out.ServeHTTP(rr, req)
|
||||
expected := fmt.Sprintf("OK")
|
||||
body := rr.Body.String()
|
||||
|
@ -216,27 +225,31 @@ func Test_wrapMiddleware(t *testing.T) {
|
|||
}
|
||||
func Test_parseOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
envKey string
|
||||
envValue string
|
||||
|
||||
wantSharedKey string
|
||||
wantErr bool
|
||||
name string
|
||||
envKey string
|
||||
envValue string
|
||||
servicesEnvKey string
|
||||
servicesEnvValue string
|
||||
wantSharedKey string
|
||||
wantErr bool
|
||||
}{
|
||||
{"no shared secret", "", "", "", true},
|
||||
{"good", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
|
||||
{"no shared secret", "", "", "SERVICES", "authenticate", "skip", true},
|
||||
{"no shared secret in all mode", "", "", "", "", "", false},
|
||||
{"good", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", "", "", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
os.Setenv(tt.servicesEnvKey, tt.servicesEnvValue)
|
||||
os.Setenv(tt.envKey, tt.envValue)
|
||||
defer os.Unsetenv(tt.envKey)
|
||||
defer os.Unsetenv(tt.servicesEnvKey)
|
||||
|
||||
got, err := parseOptions("")
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseOptions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got.SharedKey != tt.wantSharedKey {
|
||||
if got != nil && got.Services != "all" && got.SharedKey != tt.wantSharedKey {
|
||||
t.Errorf("parseOptions()\n")
|
||||
t.Errorf("got: %+v\n", got.SharedKey)
|
||||
t.Errorf("want: %+v\n", tt.wantSharedKey)
|
||||
|
@ -265,22 +278,29 @@ func Test_handleConfigUpdate(t *testing.T) {
|
|||
os.Setenv("SHARED_SECRET", "foo")
|
||||
defer os.Unsetenv("SHARED_SECRET")
|
||||
|
||||
blankOpts := config.NewOptions()
|
||||
goodOpts, _ := config.OptionsFromViper("")
|
||||
blankOpts, err := config.NewOptions("https://authenticate.example", "https://authorize.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
goodOpts, err := config.OptionsFromViper("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
service *mockService
|
||||
oldOpts config.Options
|
||||
wantUpdate bool
|
||||
}{
|
||||
{"good", &mockService{fail: false}, blankOpts, true},
|
||||
{"bad", &mockService{fail: true}, blankOpts, true},
|
||||
{"no change", &mockService{fail: false}, goodOpts, false},
|
||||
{"good", &mockService{fail: false}, *blankOpts, true},
|
||||
{"bad", &mockService{fail: true}, *blankOpts, true},
|
||||
{"no change", &mockService{fail: false}, *goodOpts, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
handleConfigUpdate(tt.oldOpts, []config.OptionsUpdater{tt.service})
|
||||
handleConfigUpdate(&tt.oldOpts, []config.OptionsUpdater{tt.service})
|
||||
if tt.service.Updated != tt.wantUpdate {
|
||||
t.Errorf("Failed to update config on service")
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ If you are coming from a kubernetes or docker background this should feel famili
|
|||
|
||||
Using both [environmental variables] and config file keys is allowed and encouraged (for instance, secret keys are probably best set as environmental variables). However, if duplicate configuration keys are found, environment variables take precedence.
|
||||
|
||||
Pomerium will automatically reload the configuration file if it is changed. At this time, only policy is re-configured when this reload occurs, but additional options may be added in the future. It is suggested that your policy is stored in a configuration file so that you can take advantage of this feature.
|
||||
Pomerium will automatically reload the configuration file if it is changed. At this time, only policy is re-configured when this reload occurs, but additional options may be added in the future. It is suggested that your policy is stored in a configuration file so that you can take advantage of this feature.
|
||||
|
||||
## Global settings
|
||||
|
||||
|
@ -73,7 +73,7 @@ head -c32 /dev/urandom | base64
|
|||
|
||||
::: danger
|
||||
|
||||
Enabling the debug flag will result in sensitive information being logged!!!
|
||||
Enabling the debug flag will result in sensitive information being logged!!!
|
||||
|
||||
:::
|
||||
|
||||
|
@ -149,19 +149,6 @@ Timeouts set the global server timeouts. For route-specific timeouts, see [polic
|
|||
|
||||
If set, the HTTP Redirect Address specifies the host and port to redirect http to https traffic on. If unset, no redirect server is started.
|
||||
|
||||
### Websocket Connections
|
||||
|
||||
- Environmental Variable: `ALLOW_WEBSOCKETS`
|
||||
- Config File Key: `allow_websockets`
|
||||
- Type: `bool`
|
||||
- Default: `false`
|
||||
|
||||
If set, enables proxying of websocket connections.
|
||||
Otherwise the proxy responds with `400 Bad Request` to all websocket connections.
|
||||
|
||||
**Use with caution:** By definition, websockets are long-lived connections, so [global timeouts](#global-timeouts) are not enforced.
|
||||
Allowing websocket connections to the proxy could result in abuse via DOS attacks.
|
||||
|
||||
### Metrics Address
|
||||
|
||||
- Environmental Variable: `METRICS_ADDRESS`
|
||||
|
@ -171,31 +158,32 @@ Allowing websocket connections to the proxy could result in abuse via DOS attack
|
|||
- Default: `disabled`
|
||||
- Optional
|
||||
|
||||
Expose a prometheus format HTTP endpoint on the specified port. Disabled by default.
|
||||
Expose a prometheus format HTTP endpoint on the specified port. Disabled by default.
|
||||
|
||||
**Use with caution:** the endpoint can expose frontend and backend server names or addresses. Do not expose the metrics port publicly.
|
||||
**Use with caution:** the endpoint can expose frontend and backend server names or addresses. Do not expose the metrics port publicly.
|
||||
|
||||
#### Metrics tracked
|
||||
|
||||
| Name | Type | Description |
|
||||
|:------------- |:-------------|:-----|
|
||||
|http_server_requests_total| Counter | Total HTTP server requests handled by service|
|
||||
|http_server_response_size_bytes| Histogram | HTTP server response size by service|
|
||||
|http_server_request_duration_ms| Histogram | HTTP server request duration by service|
|
||||
|http_client_requests_total| Counter | Total HTTP client requests made by service|
|
||||
|http_client_response_size_bytes| Histogram | HTTP client response size by service|
|
||||
|http_client_request_duration_ms| Histogram | HTTP client request duration by service|
|
||||
|grpc_client_requests_total| Counter | Total GRPC client requests made by service|
|
||||
|grpc_client_response_size_bytes| Histogram | GRPC client response size by service|
|
||||
|grpc_client_request_duration_ms| Histogram | GRPC client request duration by service|
|
||||
Name | Type | Description
|
||||
:------------------------------ | :-------- | :--------------------------------------------
|
||||
http_server_requests_total | Counter | Total HTTP server requests handled by service
|
||||
http_server_response_size_bytes | Histogram | HTTP server response size by service
|
||||
http_server_request_duration_ms | Histogram | HTTP server request duration by service
|
||||
http_client_requests_total | Counter | Total HTTP client requests made by service
|
||||
http_client_response_size_bytes | Histogram | HTTP client response size by service
|
||||
http_client_request_duration_ms | Histogram | HTTP client request duration by service
|
||||
grpc_client_requests_total | Counter | Total GRPC client requests made by service
|
||||
grpc_client_response_size_bytes | Histogram | GRPC client response size by service
|
||||
grpc_client_request_duration_ms | Histogram | GRPC client request duration by service
|
||||
|
||||
### Policy
|
||||
|
||||
- Environmental Variable: `POLICY`
|
||||
- Config File Key: `policy`
|
||||
- Type: [base64 encoded] `string` or inline policy structure in config file
|
||||
- Required
|
||||
- Required to forward traffic. Pomerium will safely start without a policy configured, but will be unable to authorize or proxy traffic until the configuration is updated to contain a policy.
|
||||
- Required
|
||||
|
||||
- Required to forward traffic. Pomerium will safely start without a policy configured, but will be unable to authorize or proxy traffic until the configuration is updated to contain a policy.
|
||||
|
||||
Policy contains route specific settings, and access control details. If you are configuring via POLICY environment variable, just the contents of the policy needs to be passed. If you are configuring via file, the policy should be present under the policy key. For example,
|
||||
|
||||
|
@ -277,6 +265,34 @@ If this setting is enabled, no whitelists (e.g. Allowed Users) should be provide
|
|||
|
||||
Policy timeout establishes the per-route timeout value. Cannot exceed global timeout values.
|
||||
|
||||
#### Websocket Connections
|
||||
|
||||
- Config File Key: `allow_websockets`
|
||||
- Type: `bool`
|
||||
- Default: `false`
|
||||
|
||||
If set, enables proxying of websocket connections.
|
||||
|
||||
**Use with caution:** By definition, websockets are long-lived connections, so [global timeouts](#global-timeouts) are not enforced. Allowing websocket connections to the proxy could result in abuse via [DOS attacks](https://www.cloudflare.com/learning/ddos/ddos-attack-tools/slowloris/).
|
||||
|
||||
#### TLS Skip Verification
|
||||
|
||||
- Config File Key: `tls_skip_verify`
|
||||
- Type: `bool`
|
||||
- Default: `false`
|
||||
|
||||
TLS Skip Verification controls whether a client verifies the server's certificate chain and host name. If enabled, TLS accepts any certificate presented by the server and any host name in that certificate. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing.
|
||||
|
||||
#### TLS Custom Certificate Authority
|
||||
|
||||
- Config File Key: `tls_custom_ca`
|
||||
- Type: [base64 encoded] `string`
|
||||
- Optional
|
||||
|
||||
TLS Custom Certificate Authority defines the set of root certificate authorities that clients use when verifying server certificates.
|
||||
|
||||
Note: This setting will replace (not append) the system's trust store for a given route.
|
||||
|
||||
## Authenticate Service
|
||||
|
||||
### Authenticate Service URL
|
||||
|
@ -398,7 +414,7 @@ If your load balancer does not support gRPC pass-through you'll need to set this
|
|||
- Optional (but typically required if Authenticate Internal Service Address is set)
|
||||
- Example: `*.corp.example.com` if wild card or `authenticate.corp.example.com`/`authorize.corp.example.com`
|
||||
|
||||
When Authenticate Internal Service Address is set, secure service communication can fail because the external certificate name will not match the internally routed service hostname/[SNI](<https://en.wikipedia.org/wiki/Server_Name_Indication>). This setting allows you to override that check.
|
||||
When Authenticate Internal Service Address is set, secure service communication can fail because the external certificate name will not match the internally routed service hostname/[SNI](https://en.wikipedia.org/wiki/Server_Name_Indication). This setting allows you to override that check.
|
||||
|
||||
### Certificate Authority
|
||||
|
||||
|
@ -414,17 +430,19 @@ Certificate Authority is set when behind-the-ingress service communication uses
|
|||
- Environmental Variable: `HEADERS`
|
||||
- Config File Key: `headers`
|
||||
- Type: map of `strings` key value pairs
|
||||
- Examples:
|
||||
- Comma Separated:
|
||||
`X-Content-Type-Options:nosniff,X-Frame-Options:SAMEORIGIN`
|
||||
- JSON: `'{"X-Test": "X-Value"}'`
|
||||
- YAML:
|
||||
```yaml
|
||||
headers:
|
||||
X-Test: X-Value
|
||||
```
|
||||
- Examples:
|
||||
|
||||
- Comma Separated: `X-Content-Type-Options:nosniff,X-Frame-Options:SAMEORIGIN`
|
||||
- JSON: `'{"X-Test": "X-Value"}'`
|
||||
- YAML:
|
||||
|
||||
```yaml
|
||||
headers:
|
||||
X-Test: X-Value
|
||||
```
|
||||
|
||||
- To disable: `disable:true`
|
||||
|
||||
- Default :
|
||||
|
||||
```javascript
|
||||
|
@ -460,7 +478,6 @@ Refresh cooldown is the minimum amount of time between allowed manually refreshe
|
|||
|
||||
Default Upstream Timeout is the default timeout applied to a proxied route when no `timeout` key is specified by the policy.
|
||||
|
||||
|
||||
[base64 encoded]: https://en.wikipedia.org/wiki/Base64
|
||||
[environmental variables]: https://en.wikipedia.org/wiki/Environment_variable
|
||||
[identity provider]: ./identity-providers.md
|
||||
|
|
58
internal/config/helpers.go
Normal file
58
internal/config/helpers.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package config // import "github.com/pomerium/pomerium/internal/config"
|
||||
|
||||
import "os"
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// IsAuthenticate checks to see if we should be running the authenticate service
|
||||
func IsAuthenticate(s string) bool {
|
||||
switch s {
|
||||
case
|
||||
"all",
|
||||
"authenticate":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsAuthorize checks to see if we should be running the authorize service
|
||||
func IsAuthorize(s string) bool {
|
||||
switch s {
|
||||
case
|
||||
"all",
|
||||
"authorize":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsProxy checks to see if we should be running the proxy service
|
||||
func IsProxy(s string) bool {
|
||||
switch s {
|
||||
case
|
||||
"all",
|
||||
"proxy":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
94
internal/config/helpers_test.go
Normal file
94
internal/config/helpers_test.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package config // import "github.com/pomerium/pomerium/internal/config"
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_isValidService(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
service string
|
||||
want bool
|
||||
}{
|
||||
{"proxy", "proxy", true},
|
||||
{"all", "all", true},
|
||||
{"authenticate", "authenticate", true},
|
||||
{"authenticate bad case", "AuThenticate", false},
|
||||
{"authorize implemented", "authorize", true},
|
||||
{"jiberish", "xd23", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValidService(tt.service); got != tt.want {
|
||||
t.Errorf("isValidService() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isAuthenticate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
service string
|
||||
want bool
|
||||
}{
|
||||
{"proxy", "proxy", false},
|
||||
{"all", "all", true},
|
||||
{"authenticate", "authenticate", true},
|
||||
{"authenticate bad case", "AuThenticate", false},
|
||||
{"authorize implemented", "authorize", false},
|
||||
{"jiberish", "xd23", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsAuthenticate(tt.service); got != tt.want {
|
||||
t.Errorf("isAuthenticate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isAuthorize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
service string
|
||||
want bool
|
||||
}{
|
||||
{"proxy", "proxy", false},
|
||||
{"all", "all", true},
|
||||
{"authorize", "authorize", true},
|
||||
{"authorize bad case", "AuThorize", false},
|
||||
{"authenticate implemented", "authenticate", false},
|
||||
{"jiberish", "xd23", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsAuthorize(tt.service); got != tt.want {
|
||||
t.Errorf("isAuthenticate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func Test_IsProxy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
service string
|
||||
want bool
|
||||
}{
|
||||
{"proxy", "proxy", true},
|
||||
{"all", "all", true},
|
||||
{"authorize", "authorize", false},
|
||||
{"proxy bad case", "PrOxY", false},
|
||||
{"jiberish", "xd23", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsProxy(tt.service); got != tt.want {
|
||||
t.Errorf("IsProxy() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
package config
|
||||
package config // import "github.com/pomerium/pomerium/internal/config"
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/hashstructure"
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
|
||||
"github.com/mitchellh/hashstructure"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
@ -22,9 +23,6 @@ import (
|
|||
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 `mapstructure:"pomerium_debug"`
|
||||
|
@ -42,10 +40,10 @@ type Options struct {
|
|||
Services string `mapstructure:"services"`
|
||||
|
||||
// Addr specifies the host and port on which the server should serve
|
||||
// HTTPS requests. If empty, ":https" is used.
|
||||
// HTTPS requests. If empty, ":https" (localhost:443) is used.
|
||||
Addr string `mapstructure:"address"`
|
||||
|
||||
// Cert and Key specifies the base64 encoded TLS certificates to use.
|
||||
// Cert and Key specifies the TLS certificates to use.
|
||||
Cert string `mapstructure:"certificate"`
|
||||
Key string `mapstructure:"certificate_key"`
|
||||
|
||||
|
@ -64,15 +62,15 @@ type Options struct {
|
|||
ReadHeaderTimeout time.Duration `mapstructure:"timeout_read_header"`
|
||||
IdleTimeout time.Duration `mapstructure:"timeout_idle"`
|
||||
|
||||
// Policy is a base64 encoded yaml blob which enumerates
|
||||
// per-route access control policies.
|
||||
// Policies define per-route configuration and access control policies.
|
||||
Policies []Policy
|
||||
PolicyEnv string
|
||||
PolicyFile string `mapstructure:"policy_file"`
|
||||
|
||||
// AuthenticateURL represents the externally accessible http endpoints
|
||||
// used for authentication requests and callbacks
|
||||
AuthenticateURLString string `mapstructure:"authenticate_service_url"`
|
||||
AuthenticateURL url.URL
|
||||
AuthenticateURL *url.URL
|
||||
|
||||
// Session/Cookie management
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
|
@ -93,8 +91,6 @@ type Options struct {
|
|||
Scopes []string `mapstructure:"idp_scopes"`
|
||||
ServiceAccount string `mapstructure:"idp_service_account"`
|
||||
|
||||
Policies []policy.Policy
|
||||
|
||||
// Administrators contains a set of emails with users who have super user
|
||||
// (sudo) access including the ability to impersonate other users' access
|
||||
Administrators []string `mapstructure:"administrators"`
|
||||
|
@ -104,20 +100,20 @@ type Options struct {
|
|||
// NOTE: As many load balancers do not support externally routed gRPC so
|
||||
// this may be an internal location.
|
||||
AuthenticateInternalAddrString string `mapstructure:"authenticate_internal_url"`
|
||||
AuthenticateInternalAddr url.URL
|
||||
AuthenticateInternalAddr *url.URL
|
||||
|
||||
// AuthorizeURL is the routable destination of the authorize service's
|
||||
// gRPC endpoint. NOTE: As many load balancers do not support
|
||||
// externally routed gRPC so this may be an internal location.
|
||||
AuthorizeURLString string `mapstructure:"authorize_service_url"`
|
||||
AuthorizeURL url.URL
|
||||
AuthorizeURL *url.URL
|
||||
|
||||
// Settings to enable custom behind-the-ingress service communication
|
||||
OverrideCertificateName string `mapstructure:"override_certificate_name"`
|
||||
CA string `mapstructure:"certificate_authority"`
|
||||
CAFile string `mapstructure:"certificate_authority_file"`
|
||||
|
||||
// SigningKey is a base64 encoded private key used to add a JWT-signature.
|
||||
// SigningKey is the private key used to add a JWT-signature.
|
||||
// https://www.pomerium.io/docs/signed-headers.html
|
||||
SigningKey string `mapstructure:"signing_key"`
|
||||
|
||||
|
@ -128,219 +124,170 @@ type Options struct {
|
|||
// RefreshCooldown limits the rate a user can refresh her session
|
||||
RefreshCooldown time.Duration `mapstructure:"refresh_cooldown"`
|
||||
|
||||
// Sub-routes
|
||||
Routes map[string]string `mapstructure:"routes"`
|
||||
DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout"`
|
||||
|
||||
// Enable proxying of websocket connections. Defaults to "false".
|
||||
// Caution: Enabling this feature could result in abuse via DOS attacks.
|
||||
AllowWebsockets bool `mapstructure:"allow_websockets"`
|
||||
//Routes map[string]string `mapstructure:"routes"`
|
||||
DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout"`
|
||||
|
||||
// Address/Port to bind to for prometheus metrics
|
||||
MetricsAddr string `mapstructure:"metrics_address"`
|
||||
}
|
||||
|
||||
// NewOptions returns a new options struct with default values
|
||||
func NewOptions() Options {
|
||||
o := Options{
|
||||
Debug: false,
|
||||
LogLevel: "debug",
|
||||
Services: "all",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSecure: true,
|
||||
CookieExpire: time.Duration(14) * time.Hour,
|
||||
CookieRefresh: time.Duration(30) * time.Minute,
|
||||
CookieName: "_pomerium",
|
||||
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,
|
||||
RefreshCooldown: time.Duration(5 * time.Minute),
|
||||
AllowWebsockets: false,
|
||||
var defaultOptions = Options{
|
||||
Debug: false,
|
||||
LogLevel: "debug",
|
||||
Services: "all",
|
||||
CookieHTTPOnly: true,
|
||||
CookieSecure: true,
|
||||
CookieExpire: time.Duration(14) * time.Hour,
|
||||
CookieRefresh: time.Duration(30) * time.Minute,
|
||||
CookieName: "_pomerium",
|
||||
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,
|
||||
RefreshCooldown: time.Duration(5 * time.Minute),
|
||||
}
|
||||
|
||||
// NewOptions returns a minimal options configuration built from default options.
|
||||
// Any modifications to the structure should be followed up by a subsequent
|
||||
// call to validate.
|
||||
func NewOptions(authenticateURL, authorizeURL string) (*Options, error) {
|
||||
o := defaultOptions
|
||||
o.AuthenticateURLString = authenticateURL
|
||||
o.AuthorizeURLString = authorizeURL
|
||||
if err := o.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("internal/config: validation error %s", err)
|
||||
}
|
||||
return o
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
// OptionsFromViper builds the main binary's configuration
|
||||
// options by parsing environmental variables and config file
|
||||
func OptionsFromViper(configFile string) (Options, error) {
|
||||
o := NewOptions()
|
||||
|
||||
func OptionsFromViper(configFile string) (*Options, error) {
|
||||
// start a copy of the default options
|
||||
o := defaultOptions
|
||||
// Load up config
|
||||
o.bindEnvs()
|
||||
if configFile != "" {
|
||||
log.Info().
|
||||
Str("file", configFile).
|
||||
Msg("loading config from file")
|
||||
|
||||
viper.SetConfigFile(configFile)
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
return o, fmt.Errorf("failed to read config: %s", err)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("internal/config: failed to read config: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
err := viper.Unmarshal(&o)
|
||||
if err != nil {
|
||||
return o, fmt.Errorf("failed to load options from config: %s", err)
|
||||
if err := viper.Unmarshal(&o); err != nil {
|
||||
return nil, fmt.Errorf("internal/config: failed to unmarshal config: %s", err)
|
||||
}
|
||||
|
||||
// Turn URL strings into url structs
|
||||
err = o.parseURLs()
|
||||
if err != nil {
|
||||
return o, fmt.Errorf("failed to parse URLs: %s", err)
|
||||
if err := o.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("internal/config: validation error %s", err)
|
||||
}
|
||||
|
||||
// Load and initialize policy
|
||||
err = o.parsePolicy()
|
||||
if err != nil {
|
||||
return o, fmt.Errorf("failed to parse Policy: %s", err)
|
||||
}
|
||||
|
||||
// Parse Headers
|
||||
err = o.parseHeaders()
|
||||
if err != nil {
|
||||
return o, fmt.Errorf("failed to parse Headers: %s", err)
|
||||
}
|
||||
|
||||
if o.Debug {
|
||||
log.SetDebugMode()
|
||||
}
|
||||
if o.LogLevel != "" {
|
||||
log.SetLevel(o.LogLevel)
|
||||
}
|
||||
if _, disable := o.Headers[DisableHeaderKey]; disable {
|
||||
o.Headers = make(map[string]string)
|
||||
}
|
||||
|
||||
err = o.validate()
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("config-checksum", o.Checksum()).
|
||||
Msg("read configuration with checksum")
|
||||
return o, nil
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
// validate ensures the Options fields are properly formed and present
|
||||
func (o *Options) validate() error {
|
||||
|
||||
// Validate ensures the Options fields are properly formed, present, and hydrated.
|
||||
func (o *Options) Validate() error {
|
||||
if !IsValidService(o.Services) {
|
||||
return fmt.Errorf("%s is an invalid service type", o.Services)
|
||||
}
|
||||
|
||||
// shared key must be set for all modes other than "all"
|
||||
if o.SharedKey == "" {
|
||||
return errors.New("shared-key cannot be empty")
|
||||
if o.Services == "all" {
|
||||
o.SharedKey = cryptutil.GenerateRandomString(32)
|
||||
} else {
|
||||
return errors.New("shared-key cannot be empty")
|
||||
}
|
||||
}
|
||||
|
||||
if len(o.Routes) != 0 {
|
||||
return errors.New("routes setting is deprecated, use policy instead")
|
||||
if o.AuthenticateURLString != "" {
|
||||
u, err := urlutil.ParseAndValidateURL(o.AuthenticateURLString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad authenticate-url %s : %v", o.AuthenticateURLString, err)
|
||||
}
|
||||
o.AuthenticateURL = u
|
||||
}
|
||||
|
||||
if o.AuthorizeURLString != "" {
|
||||
u, err := urlutil.ParseAndValidateURL(o.AuthorizeURLString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad authorize-url %s : %v", o.AuthorizeURLString, err)
|
||||
}
|
||||
o.AuthorizeURL = u
|
||||
}
|
||||
|
||||
if o.AuthenticateInternalAddrString != "" {
|
||||
u, err := urlutil.ParseAndValidateURL(o.AuthenticateInternalAddrString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad authenticate-internal-addr %s : %v", o.AuthenticateInternalAddrString, err)
|
||||
}
|
||||
o.AuthenticateInternalAddr = u
|
||||
}
|
||||
if o.PolicyFile != "" {
|
||||
return errors.New("Setting POLICY_FILE is deprecated, use policy env var or config file instead")
|
||||
return errors.New("policy file setting is deprecated")
|
||||
}
|
||||
if err := o.parsePolicy(); err != nil {
|
||||
return fmt.Errorf("failed to parse policy: %s", err)
|
||||
}
|
||||
|
||||
if err := o.parseHeaders(); err != nil {
|
||||
return fmt.Errorf("failed to parse headers: %s", err)
|
||||
}
|
||||
|
||||
if _, disable := o.Headers[DisableHeaderKey]; disable {
|
||||
o.Headers = make(map[string]string)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePolicy initializes policy
|
||||
// parsePolicy initializes policy to the options from either base64 environmental
|
||||
// variables or from a file
|
||||
func (o *Options) parsePolicy() error {
|
||||
var policies []policy.Policy
|
||||
var policies []Policy
|
||||
// Parse from base64 env var
|
||||
if o.PolicyEnv != "" {
|
||||
policyBytes, err := base64.StdEncoding.DecodeString(o.PolicyEnv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not decode POLICY env var: %s", err)
|
||||
return fmt.Errorf("could not decode POLICY env var: %s", err)
|
||||
}
|
||||
if err := yaml.Unmarshal(policyBytes, &policies); err != nil {
|
||||
return fmt.Errorf("Could not parse POLICY env var: %s", err)
|
||||
return fmt.Errorf("could not unmarshal policy yaml: %s", err)
|
||||
}
|
||||
// Parse from file
|
||||
} else {
|
||||
err := viper.UnmarshalKey("policy", &policies)
|
||||
if err != nil {
|
||||
// Parse from file
|
||||
if err := viper.UnmarshalKey("policy", &policies); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(policies) != 0 {
|
||||
o.Policies = policies
|
||||
}
|
||||
// Finish initializing policies
|
||||
for i := range policies {
|
||||
err := (&policies[i]).Validate()
|
||||
if err != nil {
|
||||
for i := range o.Policies {
|
||||
if err := (&o.Policies[i]).Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
o.Policies = policies
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseAndValidateURL wraps standard library's default url.Parse because it's much more
|
||||
// lenient about what type of urls it accepts than pomerium can be.
|
||||
func parseAndValidateURL(rawurl string) (*url.URL, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Host == "" {
|
||||
return nil, fmt.Errorf("%s does have a valid hostname", rawurl)
|
||||
}
|
||||
if u.Scheme == "" || u.Scheme != "https" {
|
||||
return nil, fmt.Errorf("%s does have a valid https scheme", rawurl)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// parseURLs parses URL strings into actual URL pointers
|
||||
func (o *Options) parseURLs() error {
|
||||
if o.AuthenticateURLString != "" {
|
||||
AuthenticateURL, err := parseAndValidateURL(o.AuthenticateURLString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal/config: bad authenticate-url %s : %v", o.AuthenticateURLString, err)
|
||||
}
|
||||
o.AuthenticateURL = *AuthenticateURL
|
||||
}
|
||||
|
||||
if o.AuthorizeURLString != "" {
|
||||
AuthorizeURL, err := parseAndValidateURL(o.AuthorizeURLString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal/config: bad authorize-url %s : %v", o.AuthorizeURLString, err)
|
||||
}
|
||||
o.AuthorizeURL = *AuthorizeURL
|
||||
}
|
||||
|
||||
if o.AuthenticateInternalAddrString != "" {
|
||||
AuthenticateInternalAddr, err := parseAndValidateURL(o.AuthenticateInternalAddrString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal/config: bad authenticate-internal-addr %s : %v", o.AuthenticateInternalAddrString, err)
|
||||
}
|
||||
o.AuthenticateInternalAddr = *AuthenticateInternalAddr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseHeaders handles unmarshalling any custom headers correctly from the environment or
|
||||
// viper's parsed keys
|
||||
// parseHeaders handles unmarshalling any custom headers correctly from the
|
||||
// environment or viper's parsed keys
|
||||
func (o *Options) parseHeaders() error {
|
||||
var headers map[string]string
|
||||
if o.HeadersEnv != "" {
|
||||
|
||||
// Handle JSON by default via viper
|
||||
if headers = viper.GetStringMapString("HeadersEnv"); len(headers) == 0 {
|
||||
|
||||
// Try to parse "Key1:Value1,Key2:Value2" syntax
|
||||
headerSlice := strings.Split(o.HeadersEnv, ",")
|
||||
for n := range headerSlice {
|
||||
|
@ -350,7 +297,7 @@ func (o *Options) parseHeaders() error {
|
|||
|
||||
} else {
|
||||
// Something went wrong
|
||||
return fmt.Errorf("Failed to decode headers environment variable from '%s'", o.HeadersEnv)
|
||||
return fmt.Errorf("failed to decode headers from '%s'", o.HeadersEnv)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,7 +305,7 @@ func (o *Options) parseHeaders() error {
|
|||
o.Headers = headers
|
||||
} else if viper.IsSet("headers") {
|
||||
if err := viper.UnmarshalKey("headers", &headers); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("header %s failed to parse: %s", viper.Get("headers"), err)
|
||||
}
|
||||
o.Headers = headers
|
||||
}
|
||||
|
@ -381,61 +328,6 @@ func (o *Options) bindEnvs() {
|
|||
viper.BindEnv("HeadersEnv", "HEADERS")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// IsAuthenticate checks to see if we should be running the authenticate service
|
||||
func IsAuthenticate(s string) bool {
|
||||
switch s {
|
||||
case
|
||||
"all",
|
||||
"authenticate":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsAuthorize checks to see if we should be running the authorize service
|
||||
func IsAuthorize(s string) bool {
|
||||
switch s {
|
||||
case
|
||||
"all",
|
||||
"authorize":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsProxy checks to see if we should be running the proxy service
|
||||
func IsProxy(s string) bool {
|
||||
switch s {
|
||||
case
|
||||
"all",
|
||||
"proxy":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OptionsUpdater updates local state based on an Options struct
|
||||
type OptionsUpdater interface {
|
||||
UpdateOptions(Options) error
|
||||
|
@ -444,10 +336,9 @@ type OptionsUpdater interface {
|
|||
// Checksum returns the checksum of the current options struct
|
||||
func (o *Options) Checksum() string {
|
||||
hash, err := hashstructure.Hash(o, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Warn().Msg("could not calculate Option checksum")
|
||||
return "no checksum availablle"
|
||||
log.Warn().Err(err).Msg("internal/config: checksum failure")
|
||||
return "no checksum available"
|
||||
}
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
|
|
|
@ -6,18 +6,16 @@ import (
|
|||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pomerium/pomerium/internal/policy"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func Test_validate(t *testing.T) {
|
||||
|
||||
testOptions := func() Options {
|
||||
o := NewOptions()
|
||||
o := defaultOptions
|
||||
o.SharedKey = "test"
|
||||
o.Services = "all"
|
||||
return o
|
||||
|
@ -27,8 +25,10 @@ func Test_validate(t *testing.T) {
|
|||
badServices.Services = "blue"
|
||||
badSecret := testOptions()
|
||||
badSecret.SharedKey = ""
|
||||
badRoutes := testOptions()
|
||||
badRoutes.Routes = map[string]string{"foo": "bar"}
|
||||
badSecret.Services = "authenticate"
|
||||
badSecretAllServices := testOptions()
|
||||
badSecretAllServices.SharedKey = ""
|
||||
|
||||
badPolicyFile := testOptions()
|
||||
badPolicyFile.PolicyFile = "file"
|
||||
|
||||
|
@ -40,12 +40,12 @@ func Test_validate(t *testing.T) {
|
|||
{"good default with no env settings", good, false},
|
||||
{"invalid service type", badServices, true},
|
||||
{"missing shared secret", badSecret, true},
|
||||
{"routes present", badRoutes, 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()
|
||||
err := tt.testOpts.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("optionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
@ -54,95 +54,6 @@ func Test_validate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_isValidService(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
service string
|
||||
want bool
|
||||
}{
|
||||
{"proxy", "proxy", true},
|
||||
{"all", "all", true},
|
||||
{"authenticate", "authenticate", true},
|
||||
{"authenticate bad case", "AuThenticate", false},
|
||||
{"authorize implemented", "authorize", true},
|
||||
{"jiberish", "xd23", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValidService(tt.service); got != tt.want {
|
||||
t.Errorf("isValidService() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isAuthenticate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
service string
|
||||
want bool
|
||||
}{
|
||||
{"proxy", "proxy", false},
|
||||
{"all", "all", true},
|
||||
{"authenticate", "authenticate", true},
|
||||
{"authenticate bad case", "AuThenticate", false},
|
||||
{"authorize implemented", "authorize", false},
|
||||
{"jiberish", "xd23", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsAuthenticate(tt.service); got != tt.want {
|
||||
t.Errorf("isAuthenticate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isAuthorize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
service string
|
||||
want bool
|
||||
}{
|
||||
{"proxy", "proxy", false},
|
||||
{"all", "all", true},
|
||||
{"authorize", "authorize", true},
|
||||
{"authorize bad case", "AuThorize", false},
|
||||
{"authenticate implemented", "authenticate", false},
|
||||
{"jiberish", "xd23", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsAuthorize(tt.service); got != tt.want {
|
||||
t.Errorf("isAuthenticate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func Test_IsProxy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
service string
|
||||
want bool
|
||||
}{
|
||||
{"proxy", "proxy", true},
|
||||
{"all", "all", true},
|
||||
{"authorize", "authorize", false},
|
||||
{"proxy bad case", "PrOxY", false},
|
||||
{"jiberish", "xd23", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsProxy(tt.service); got != tt.want {
|
||||
t.Errorf("IsProxy() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_bindEnvs(t *testing.T) {
|
||||
o := &Options{}
|
||||
os.Clearenv()
|
||||
|
@ -171,44 +82,6 @@ func Test_bindEnvs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_parseURLs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
authorizeURL string
|
||||
authenticateURL string
|
||||
authenticateInternalURL string
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", "https://authz.mydomain.example", "https://authn.mydomain.example", "https://internal.svc.local", false},
|
||||
{"bad not https scheme", "http://authz.mydomain.example", "http://authn.mydomain.example", "http://internal.svc.local", true},
|
||||
{"missing scheme", "authz.mydomain.example", "authn.mydomain.example", "internal.svc.local", true},
|
||||
{"bad authorize", "notaurl", "https://authn.mydomain.example", "", true},
|
||||
{"bad authenticate", "https://authz.mydomain.example", "notaurl", "", true},
|
||||
{"bad authenticate internal", "", "", "just.some.naked.domain.example", true},
|
||||
{"only authn", "", "https://authn.mydomain.example", "", false},
|
||||
{"only authz", "https://authz.mydomain.example", "", "", false},
|
||||
{"malformed", "http://a b.com/", "", "", true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
o := &Options{
|
||||
AuthenticateURLString: test.authenticateURL,
|
||||
AuthorizeURLString: test.authorizeURL,
|
||||
AuthenticateInternalAddrString: test.authenticateInternalURL,
|
||||
}
|
||||
err := o.parseURLs()
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf("Failed to parse URLs %v: %s", test, err)
|
||||
}
|
||||
if err == nil && o.AuthenticateURL.String() != test.authenticateURL {
|
||||
t.Errorf("Failed to update AuthenticateURL: %v", test)
|
||||
}
|
||||
if err == nil && o.AuthorizeURL.String() != test.authorizeURL {
|
||||
t.Errorf("Failed to update AuthorizeURL: %v", test)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_parseHeaders(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -219,15 +92,15 @@ func Test_parseHeaders(t *testing.T) {
|
|||
}{
|
||||
{"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},
|
||||
{"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},
|
||||
{"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 := NewOptions()
|
||||
o := defaultOptions
|
||||
viper.Set("headers", tt.viperHeaders)
|
||||
viper.Set("HeadersEnv", tt.envHeaders)
|
||||
o.HeadersEnv = tt.envHeaders
|
||||
|
@ -241,23 +114,28 @@ func Test_parseHeaders(t *testing.T) {
|
|||
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) {
|
||||
testPolicy := policy.Policy{
|
||||
viper.Reset()
|
||||
|
||||
testPolicy := Policy{
|
||||
To: "https://httpbin.org",
|
||||
From: "https://pomerium.io",
|
||||
}
|
||||
testPolicy.Validate()
|
||||
testPolicies := []policy.Policy{
|
||||
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 := NewOptions()
|
||||
goodOptions := defaultOptions
|
||||
goodOptions.SharedKey = "Setec Astronomy"
|
||||
goodOptions.Services = "all"
|
||||
goodOptions.Policies = testPolicies
|
||||
|
@ -272,21 +150,23 @@ func Test_OptionsFromViper(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
goodOptions.AuthorizeURL = *authorize
|
||||
goodOptions.AuthenticateURL = *authenticate
|
||||
|
||||
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
|
||||
want *Options
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", goodConfigBytes, goodOptions, false},
|
||||
{"bad json", badConfigBytes, NewOptions(), true},
|
||||
{"bad unmarshal", badUnmarshalConfigBytes, NewOptions(), true},
|
||||
{"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) {
|
||||
|
@ -302,8 +182,13 @@ func Test_OptionsFromViper(t *testing.T) {
|
|||
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)
|
||||
t.Errorf("OptionsFromViper() = \n%s\n, \ngot\n%+v\n, want \n%+v", diff, got, tt.want)
|
||||
}
|
||||
|
||||
})
|
||||
|
@ -318,6 +203,8 @@ func Test_OptionsFromViper(t *testing.T) {
|
|||
|
||||
func Test_parsePolicyEnv(t *testing.T) {
|
||||
t.Parallel()
|
||||
viper.Reset()
|
||||
|
||||
source := "https://pomerium.io"
|
||||
sourceURL, _ := url.ParseRequestURI(source)
|
||||
dest := "https://httpbin.org"
|
||||
|
@ -326,12 +213,12 @@ func Test_parsePolicyEnv(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
policyBytes []byte
|
||||
want []policy.Policy
|
||||
want []Policy
|
||||
wantErr bool
|
||||
}{
|
||||
{"simple json", []byte(fmt.Sprintf(`[{"from": "%s","to":"%s"}]`, source, dest)), []policy.Policy{{From: source, To: dest, Source: sourceURL, Destination: destURL}}, false},
|
||||
{"bad from", []byte(`[{"from": "%","to":"httpbin.org"}]`), nil, true},
|
||||
{"bad to", []byte(`[{"from": "pomerium.io","to":"%"}]`), nil, true},
|
||||
{"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 {
|
||||
|
@ -341,11 +228,11 @@ func Test_parsePolicyEnv(t *testing.T) {
|
|||
o.PolicyEnv = base64.StdEncoding.EncodeToString(tt.policyBytes)
|
||||
err := o.parsePolicy()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parasePolicy() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("parsePolicyEnv() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(o.Policies, tt.want) {
|
||||
t.Errorf("parasePolicy() = \n%v, want \n%v", o, tt.want)
|
||||
if diff := cmp.Diff(o.Policies, tt.want); diff != "" {
|
||||
t.Errorf("parsePolicyEnv() = %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -355,11 +242,12 @@ func Test_parsePolicyEnv(t *testing.T) {
|
|||
o.PolicyEnv = "foo"
|
||||
err := o.parsePolicy()
|
||||
if err == nil {
|
||||
t.Errorf("parasePolicy() did not catch bad base64 %v", o)
|
||||
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"
|
||||
|
@ -368,38 +256,41 @@ func Test_parsePolicyFile(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
policyBytes []byte
|
||||
want []policy.Policy
|
||||
want []Policy
|
||||
wantErr bool
|
||||
}{
|
||||
{"simple json", []byte(fmt.Sprintf(`{"policy":[{"from": "%s","to":"%s"}]}`, source, dest)), []policy.Policy{{From: source, To: dest, Source: sourceURL, Destination: destURL}}, false},
|
||||
{"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) {
|
||||
o := new(Options)
|
||||
|
||||
tempFile, _ := ioutil.TempFile("", "*.json")
|
||||
defer tempFile.Close()
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Write(tt.policyBytes)
|
||||
o = new(Options)
|
||||
o := new(Options)
|
||||
viper.SetConfigFile(tempFile.Name())
|
||||
err := viper.ReadInConfig()
|
||||
err = o.parsePolicy()
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := o.parsePolicy()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parasePolicy() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("parsePolicyEnv() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(o.Policies, tt.want) {
|
||||
t.Errorf("parasePolicy() = \n%v, want \n%v", o, tt.want)
|
||||
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 := NewOptions()
|
||||
o := defaultOptions
|
||||
|
||||
oldChecksum := o.Checksum()
|
||||
o.SharedKey = "changemeplease"
|
||||
|
@ -417,3 +308,103 @@ func Test_Checksum(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
71
internal/config/policy.go
Normal file
71
internal/config/policy.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package config // import "github.com/pomerium/pomerium/internal/config"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
)
|
||||
|
||||
// Policy contains route specific configuration and access settings.
|
||||
type Policy struct {
|
||||
From string `mapstructure:"from" yaml:"from"`
|
||||
To string `mapstructure:"to" yaml:"to"`
|
||||
// Identity related policy
|
||||
AllowedEmails []string `mapstructure:"allowed_users" yaml:"allowed_users"`
|
||||
AllowedGroups []string `mapstructure:"allowed_groups" yaml:"allowed_groups"`
|
||||
AllowedDomains []string `mapstructure:"allowed_domains" yaml:"allowed_domains"`
|
||||
|
||||
Source *url.URL
|
||||
Destination *url.URL
|
||||
|
||||
// Allow unauthenticated HTTP OPTIONS requests as per the CORS spec
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests
|
||||
CORSAllowPreflight bool `mapstructure:"cors_allow_preflight" yaml:"cors_allow_preflight"`
|
||||
|
||||
// Allow any public request to access this route. **Bypasses authentication**
|
||||
AllowPublicUnauthenticatedAccess bool `mapstructure:"allow_public_unauthenticated_access" yaml:"allow_public_unauthenticated_access"`
|
||||
|
||||
// UpstreamTimeout is the route specific timeout. Must be less than the global
|
||||
// timeout. If unset, route will fallback to the proxy's DefaultUpstreamTimeout.
|
||||
UpstreamTimeout time.Duration `mapstructure:"timeout" yaml:"timeout"`
|
||||
|
||||
// Enable proxying of websocket connections by removing the default timeout handler.
|
||||
// Caution: Enabling this feature could result in abuse via DOS attacks.
|
||||
AllowWebsockets bool `mapstructure:"allow_websockets" yaml:"allow_websockets"`
|
||||
|
||||
// TLSSkipVerify controls whether a client verifies the server's certificate
|
||||
// chain and host name.
|
||||
// If TLSSkipVerify is true, TLS accepts any certificate presented by the
|
||||
// server and any host name in that certificate.
|
||||
// In this mode, TLS is susceptible to man-in-the-middle attacks.
|
||||
// This should be used only for testing.
|
||||
TLSSkipVerify bool `mapstructure:"tls_skip_verify" yaml:"tls_skip_verify"`
|
||||
|
||||
// TLSCustomCA defines the root certificate to use with a given
|
||||
// route when verifying server certificates.
|
||||
TLSCustomCA string `mapstructure:"tls_custom_ca" yaml:"tls_custom_ca"`
|
||||
}
|
||||
|
||||
// Validate checks the validity of a policy.
|
||||
func (p *Policy) Validate() error {
|
||||
var err error
|
||||
p.Source, err = urlutil.ParseAndValidateURL(p.From)
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal/config: bad source url %s", err)
|
||||
}
|
||||
|
||||
p.Destination, err = urlutil.ParseAndValidateURL(p.To)
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal/config: bad destination url %s", err)
|
||||
}
|
||||
|
||||
// Only allow public access if no other whitelists are in place
|
||||
if p.AllowPublicUnauthenticatedAccess && (p.AllowedDomains != nil || p.AllowedGroups != nil || p.AllowedEmails != nil) {
|
||||
return errors.New("internal/config: route marked as public but contains whitelists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
43
internal/config/policy_test.go
Normal file
43
internal/config/policy_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package config // import "github.com/pomerium/pomerium/internal/config"
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_Validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
basePolicy := Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld"}
|
||||
|
||||
corsPolicy := basePolicy
|
||||
corsPolicy.CORSAllowPreflight = true
|
||||
|
||||
publicPolicy := basePolicy
|
||||
publicPolicy.AllowPublicUnauthenticatedAccess = true
|
||||
|
||||
publicAndWhitelistPolicy := publicPolicy
|
||||
publicAndWhitelistPolicy.AllowedEmails = []string{"test@gmail.com"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
policy Policy
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", basePolicy, false},
|
||||
{"empty to host", Policy{From: "https://httpbin.corp.example", To: "https://"}, true},
|
||||
{"empty from host", Policy{From: "https://", To: "https://httpbin.corp.example"}, true},
|
||||
{"empty from scheme", Policy{From: "httpbin.corp.example", To: "https://httpbin.corp.example"}, true},
|
||||
{"empty to scheme", Policy{From: "https://httpbin.corp.example", To: "//httpbin.corp.example"}, true},
|
||||
{"cors policy", corsPolicy, false},
|
||||
{"public policy", publicPolicy, false},
|
||||
{"public and whitelist", publicAndWhitelistPolicy, true},
|
||||
{"route must have", publicAndWhitelistPolicy, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.policy.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, want %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -13,14 +13,32 @@ import (
|
|||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
const DefaultKeySize = 32
|
||||
|
||||
// GenerateKey generates a random 32-byte key.
|
||||
//
|
||||
// Panics if source of randomness fails.
|
||||
func GenerateKey() []byte {
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return randomBytes(DefaultKeySize)
|
||||
}
|
||||
|
||||
// GenerateRandomString returns base64 encoded securely generated random string
|
||||
// of a given set of bytes.
|
||||
//
|
||||
// Panics if source of randomness fails.
|
||||
func GenerateRandomString(c int) string {
|
||||
return base64.StdEncoding.EncodeToString(randomBytes(c))
|
||||
}
|
||||
|
||||
func randomBytes(c int) []byte {
|
||||
if c < 0 {
|
||||
c = DefaultKeySize
|
||||
}
|
||||
b := make([]byte, c)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return key
|
||||
return b
|
||||
}
|
||||
|
||||
// Cipher provides methods to encrypt and decrypt values.
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package cryptutil // import "github.com/pomerium/pomerium/internal/cryptutil"
|
||||
package cryptutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
@ -162,3 +163,29 @@ func TestCipherDataRace(t *testing.T) {
|
|||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestGenerateRandomString(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
c int
|
||||
want int
|
||||
}{
|
||||
{"simple", 32, 32},
|
||||
{"zero", 0, 0},
|
||||
{"negative", -1, 32},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := GenerateRandomString(tt.c)
|
||||
b, err := base64.StdEncoding.DecodeString(o)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
got := len(b)
|
||||
if got != tt.want {
|
||||
t.Errorf("GenerateRandomString() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package https // import "github.com/pomerium/pomerium/internal/https"
|
||||
package httputil // import "github.com/pomerium/pomerium/internal/httputil"
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/pomerium/pomerium/internal/fileutil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
|
@ -18,8 +18,7 @@ func newPromHTTPHandler() http.Handler {
|
|||
// TODO this is a cheap way to get thorough go process
|
||||
// stats. It will not work with additional exporters.
|
||||
// It should turn into an FR to the OC framework
|
||||
var reg *prom.Registry
|
||||
reg = prom.DefaultRegisterer.(*prom.Registry)
|
||||
reg := prom.DefaultRegisterer.(*prom.Registry)
|
||||
pe, _ := ocProm.NewExporter(ocProm.Options{
|
||||
Namespace: "pomerium",
|
||||
Registry: reg,
|
||||
|
|
|
@ -13,12 +13,6 @@ import (
|
|||
"go.opencensus.io/stats/view"
|
||||
)
|
||||
|
||||
type measure struct {
|
||||
Name string
|
||||
Tags map[string]string
|
||||
Measure int
|
||||
}
|
||||
|
||||
func newTestMux() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/good", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
42
internal/middleware/reverse_proxy.go
Normal file
42
internal/middleware/reverse_proxy.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package middleware // import "github.com/pomerium/pomerium/internal/middleware"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
)
|
||||
|
||||
func SignRequest(signer cryptutil.JWTSigner, id, email, groups, header string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
jwt, err := signer.SignJWT(
|
||||
r.Header.Get(id),
|
||||
r.Header.Get(email),
|
||||
r.Header.Get(groups))
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("internal/middleware: failed signing request")
|
||||
} else {
|
||||
r.Header.Set(header, jwt)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// StripPomeriumCookie ensures that every response includes some basic security headers
|
||||
func StripPomeriumCookie(cookieName string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
headers := make([]string, len(r.Cookies()))
|
||||
for _, cookie := range r.Cookies() {
|
||||
if cookie.Name != cookieName {
|
||||
headers = append(headers, cookie.String())
|
||||
}
|
||||
}
|
||||
r.Header.Set("Cookie", strings.Join(headers, ";"))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
94
internal/middleware/reverse_proxy_test.go
Normal file
94
internal/middleware/reverse_proxy_test.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package middleware // import "github.com/pomerium/pomerium/internal/middleware"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
)
|
||||
|
||||
const exampleKey = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIM3mpZIWXCX9yEgxU6s57CbtbUNDBSCEAtQF5fUWHpcQoAoGCCqGSM49
|
||||
AwEHoUQDQgAEhPQv+LACPVNmBTK0xSTzbpEPkRrk1eUt1BOa32SEfUPzNi4IWeZ/
|
||||
KKITt2q1IqpV2KMSbVDyr9ijv/Xh98iyEw==
|
||||
-----END EC PRIVATE KEY-----
|
||||
`
|
||||
|
||||
func TestSignRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
id string
|
||||
email string
|
||||
groups string
|
||||
header string
|
||||
}{
|
||||
{"good", "id", "email", "group", "Jwt"},
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set(fmt.Sprintf("%s-header", tt.id), tt.id)
|
||||
r.Header.Set(fmt.Sprintf("%s-header", tt.email), tt.email)
|
||||
r.Header.Set(fmt.Sprintf("%s-header", tt.groups), tt.groups)
|
||||
|
||||
})
|
||||
rr := httptest.NewRecorder()
|
||||
signer, err := cryptutil.NewES256Signer([]byte(exampleKey), "audience")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := SignRequest(signer, tt.id, tt.email, tt.groups, tt.header)(testHandler)
|
||||
handler.ServeHTTP(rr, req)
|
||||
jwt := req.Header["Jwt"]
|
||||
if len(jwt) != 1 {
|
||||
t.Errorf("no jwt found %v", req.Header)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripPomeriumCookie(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pomeriumCookie string
|
||||
otherCookies []string
|
||||
}{
|
||||
{"good", "pomerium", []string{"x", "y", "z"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
for _, cookie := range r.Cookies() {
|
||||
if cookie.Name == tt.pomeriumCookie {
|
||||
t.Errorf("cookie not stripped %s", r.Cookies())
|
||||
}
|
||||
}
|
||||
})
|
||||
rr := httptest.NewRecorder()
|
||||
for _, cn := range tt.otherCookies {
|
||||
http.SetCookie(rr, &http.Cookie{
|
||||
Name: cn,
|
||||
Value: "some other cookie",
|
||||
})
|
||||
}
|
||||
|
||||
http.SetCookie(rr, &http.Cookie{
|
||||
Name: tt.pomeriumCookie,
|
||||
Value: "pomerium cookie!",
|
||||
})
|
||||
req := &http.Request{Header: http.Header{"Cookie": rr.HeaderMap["Set-Cookie"]}}
|
||||
|
||||
handler := StripPomeriumCookie(tt.pomeriumCookie)(testHandler)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package policy // import "github.com/pomerium/pomerium/internal/policy"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Policy contains authorization policy information.
|
||||
// todo(bdd) : add upstream timeout and configuration settings
|
||||
type Policy struct {
|
||||
//
|
||||
From string `mapstructure:"from" yaml:"from"`
|
||||
To string `mapstructure:"to" yaml:"to"`
|
||||
// Identity related policy
|
||||
AllowedEmails []string `mapstructure:"allowed_users" yaml:"allowed_users"`
|
||||
AllowedGroups []string `mapstructure:"allowed_groups" yaml:"allowed_groups"`
|
||||
AllowedDomains []string `mapstructure:"allowed_domains" yaml:"allowed_domains"`
|
||||
|
||||
Source *url.URL
|
||||
Destination *url.URL
|
||||
|
||||
// Allow unauthenticated HTTP OPTIONS requests as per the CORS spec
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests
|
||||
CORSAllowPreflight bool `mapstructure:"cors_allow_preflight" yaml:"cors_allow_preflight"`
|
||||
|
||||
// Allow any public request to access this route. **Bypasses authentication**
|
||||
AllowPublicUnauthenticatedAccess bool `mapstructure:"allow_public_unauthenticated_access" yaml:"allow_public_unauthenticated_access"`
|
||||
|
||||
// UpstreamTimeout is the route specific timeout. Must be less than the global
|
||||
// timeout. If unset, route will fallback to the proxy's DefaultUpstreamTimeout.
|
||||
UpstreamTimeout time.Duration `mapstructure:"timeout" yaml:"timeout"`
|
||||
}
|
||||
|
||||
// Validate parses the source and destination URLs in the Policy
|
||||
func (p *Policy) Validate() (err error) {
|
||||
p.Source, err = urlParse(p.From)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Destination, err = urlParse(p.To)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only allow public access if no other whitelists are in place
|
||||
if p.AllowPublicUnauthenticatedAccess && (p.AllowedDomains != nil || p.AllowedGroups != nil || p.AllowedEmails != nil) {
|
||||
return errors.New("route marked as public but contains whitelists")
|
||||
}
|
||||
|
||||
return 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)
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package policy
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_urlParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
uri string
|
||||
want *url.URL
|
||||
wantErr bool
|
||||
}{
|
||||
{"good url without schema", "accounts.google.com", &url.URL{Scheme: "https", Host: "accounts.google.com"}, false},
|
||||
{"good url with schema", "https://accounts.google.com", &url.URL{Scheme: "https", Host: "accounts.google.com"}, false},
|
||||
{"bad url, malformed", "https://accounts.google.^", nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := urlParse(tt.uri)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("urlParse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("urlParse() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
basePolicy := Policy{From: "httpbin.corp.example", To: "httpbin.corp.notatld"}
|
||||
|
||||
corsPolicy := basePolicy
|
||||
corsPolicy.CORSAllowPreflight = true
|
||||
|
||||
publicPolicy := basePolicy
|
||||
publicPolicy.AllowPublicUnauthenticatedAccess = true
|
||||
|
||||
publicAndWhitelistPolicy := publicPolicy
|
||||
publicAndWhitelistPolicy.AllowedEmails = []string{"test@gmail.com"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
policy Policy
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", basePolicy, false},
|
||||
{"cors policy", corsPolicy, false},
|
||||
{"public policy", publicPolicy, false},
|
||||
{"public and whitelist", publicAndWhitelistPolicy, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.policy.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, want %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -57,7 +57,6 @@ func (s *RestStore) ClearSession(w http.ResponseWriter, r *http.Request) {
|
|||
"error_description": "The token has expired."
|
||||
}`
|
||||
w.Write([]byte(errMsg))
|
||||
return
|
||||
}
|
||||
|
||||
// LoadSession attempts to load a pomerium session from a Bearer Token set
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package urlutil // import "github.com/pomerium/pomerium/internal/urlutil"
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StripPort returns a host, without any port number.
|
||||
//
|
||||
|
@ -17,3 +21,19 @@ func StripPort(hostport string) string {
|
|||
}
|
||||
return hostport[:colon]
|
||||
}
|
||||
|
||||
// ParseAndValidateURL wraps standard library's default url.Parse because
|
||||
// it's much more lenient about what type of urls it accepts than pomerium.
|
||||
func ParseAndValidateURL(rawurl string) (*url.URL, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
return nil, fmt.Errorf("%s url does contain a valid scheme. Did you mean https://%s?", rawurl, rawurl)
|
||||
}
|
||||
if u.Host == "" {
|
||||
return nil, fmt.Errorf("%s url does contain a valid hostname", rawurl)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
package urlutil // import "github.com/pomerium/pomerium/internal/urlutil"
|
||||
package urlutil
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func Test_StripPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -27,3 +32,30 @@ func Test_StripPort(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndValidateURL(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rawurl string
|
||||
want *url.URL
|
||||
wantErr bool
|
||||
}{
|
||||
{"good", "https://some.example", &url.URL{Scheme: "https", Host: "some.example"}, false},
|
||||
{"bad schema", "//some.example", nil, true},
|
||||
{"bad hostname", "https://", nil, true},
|
||||
{"bad parse", "https://^", nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseAndValidateURL(tt.rawurl)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseAndValidateURL() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.want); diff != "" {
|
||||
t.Errorf("TestParseAndValidateURL() = %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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, ":") {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
237
proxy/proxy.go
237
proxy/proxy.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue