mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-06 21:04:39 +02:00
Refactor to central options struct and parsing
This commit is contained in:
parent
5970d6c766
commit
ebb6df6c3f
12 changed files with 415 additions and 511 deletions
|
@ -6,9 +6,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pomerium/envconfig"
|
"github.com/pomerium/pomerium/internal/config"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||||
"github.com/pomerium/pomerium/internal/identity"
|
"github.com/pomerium/pomerium/internal/identity"
|
||||||
|
@ -16,53 +15,10 @@ import (
|
||||||
"github.com/pomerium/pomerium/internal/templates"
|
"github.com/pomerium/pomerium/internal/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultOptions = &Options{
|
// ValidateOptions checks to see if configuration values are valid for the authenticate service.
|
||||||
CookieName: "_pomerium_authenticate",
|
|
||||||
CookieHTTPOnly: true,
|
|
||||||
CookieSecure: true,
|
|
||||||
CookieExpire: time.Duration(14) * time.Hour,
|
|
||||||
CookieRefresh: time.Duration(30) * time.Minute,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options details the available configuration settings for the authenticate service
|
|
||||||
type Options struct {
|
|
||||||
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
|
|
||||||
|
|
||||||
// SharedKey is used to authenticate requests between services
|
|
||||||
SharedKey string `envconfig:"SHARED_SECRET"`
|
|
||||||
// Session/Cookie management
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
|
||||||
CookieName string
|
|
||||||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
|
||||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
|
||||||
CookieSecure bool `envconfig:"COOKIE_SECURE"`
|
|
||||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
|
||||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
|
||||||
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
|
|
||||||
|
|
||||||
// Identity provider configuration variables as specified by RFC6749
|
|
||||||
// https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
|
|
||||||
ClientID string `envconfig:"IDP_CLIENT_ID"`
|
|
||||||
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"`
|
|
||||||
Provider string `envconfig:"IDP_PROVIDER"`
|
|
||||||
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
|
|
||||||
Scopes []string `envconfig:"IDP_SCOPES"`
|
|
||||||
ServiceAccount string `envconfig:"IDP_SERVICE_ACCOUNT"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptionsFromEnvConfig builds the authenticate service's configuration environmental variables
|
|
||||||
func OptionsFromEnvConfig() (*Options, error) {
|
|
||||||
o := defaultOptions
|
|
||||||
if err := envconfig.Process("", o); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return o, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks to see if configuration values are valid for the authenticate service.
|
|
||||||
// The checks do not modify the internal state of the Option structure. Returns
|
// The checks do not modify the internal state of the Option structure. Returns
|
||||||
// on first error found.
|
// on first error found.
|
||||||
func (o *Options) Validate() error {
|
func ValidateOptions(o *config.Options) error {
|
||||||
if o.AuthenticateURL == nil {
|
if o.AuthenticateURL == nil {
|
||||||
return errors.New("authenticate: 'AUTHENTICATE_SERVICE_URL' missing")
|
return errors.New("authenticate: 'AUTHENTICATE_SERVICE_URL' missing")
|
||||||
}
|
}
|
||||||
|
@ -98,11 +54,11 @@ type Authenticate struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New validates and creates a new authenticate service from a set of Options
|
// New validates and creates a new authenticate service from a set of Options
|
||||||
func New(opts *Options) (*Authenticate, error) {
|
func New(opts *config.Options) (*Authenticate, error) {
|
||||||
if opts == nil {
|
if opts == nil {
|
||||||
return nil, errors.New("authenticate: options cannot be nil")
|
return nil, errors.New("authenticate: options cannot be nil")
|
||||||
}
|
}
|
||||||
if err := opts.Validate(); err != nil {
|
if err := ValidateOptions(opts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
decodedCookieSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
|
decodedCookieSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
|
||||||
|
@ -112,7 +68,7 @@ func New(opts *Options) (*Authenticate, error) {
|
||||||
}
|
}
|
||||||
cookieStore, err := sessions.NewCookieStore(
|
cookieStore, err := sessions.NewCookieStore(
|
||||||
&sessions.CookieStoreOptions{
|
&sessions.CookieStoreOptions{
|
||||||
Name: opts.CookieName,
|
Name: opts.AuthenticateCookieName,
|
||||||
CookieSecure: opts.CookieSecure,
|
CookieSecure: opts.CookieSecure,
|
||||||
CookieHTTPOnly: opts.CookieHTTPOnly,
|
CookieHTTPOnly: opts.CookieHTTPOnly,
|
||||||
CookieExpire: opts.CookieExpire,
|
CookieExpire: opts.CookieExpire,
|
||||||
|
|
|
@ -2,23 +2,23 @@ package authenticate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testOptions() *Options {
|
func testOptions() *config.Options {
|
||||||
redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
|
redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
|
||||||
return &Options{
|
return &config.Options{
|
||||||
AuthenticateURL: redirectURL,
|
AuthenticateURL: redirectURL,
|
||||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
||||||
ClientID: "test-client-id",
|
ClientID: "test-client-id",
|
||||||
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
ClientSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
||||||
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
||||||
CookieRefresh: time.Duration(1) * time.Hour,
|
CookieRefresh: time.Duration(1) * time.Hour,
|
||||||
CookieExpire: time.Duration(168) * time.Hour,
|
CookieExpire: time.Duration(168) * time.Hour,
|
||||||
CookieName: "pomerium",
|
AuthenticateCookieName: "pomerium",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,11 +41,11 @@ func TestOptions_Validate(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
o *Options
|
o *config.Options
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"minimum options", good, false},
|
{"minimum options", good, false},
|
||||||
{"nil options", &Options{}, true},
|
{"nil options", &config.Options{}, true},
|
||||||
{"bad redirect url", badRedirectURL, true},
|
{"bad redirect url", badRedirectURL, true},
|
||||||
{"no cookie secret", emptyCookieSecret, true},
|
{"no cookie secret", emptyCookieSecret, true},
|
||||||
{"invalid cookie secret", invalidCookieSecret, true},
|
{"invalid cookie secret", invalidCookieSecret, true},
|
||||||
|
@ -57,46 +57,13 @@ func TestOptions_Validate(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
o := tt.o
|
o := tt.o
|
||||||
if err := o.Validate(); (err != nil) != tt.wantErr {
|
if err := ValidateOptions(o); (err != nil) != tt.wantErr {
|
||||||
t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOptionsFromEnvConfig(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
want *Options
|
|
||||||
envKey string
|
|
||||||
envValue string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{"good default, no env settings", defaultOptions, "", "", false},
|
|
||||||
{"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.rjlw", true},
|
|
||||||
{"good duration", defaultOptions, "COOKIE_EXPIRE", "1m", false},
|
|
||||||
{"bad duration", nil, "COOKIE_REFRESH", "1sm", true},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.envKey != "" {
|
|
||||||
os.Setenv(tt.envKey, tt.envValue)
|
|
||||||
defer os.Unsetenv(tt.envKey)
|
|
||||||
}
|
|
||||||
got, err := OptionsFromEnvConfig()
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("OptionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("OptionsFromEnvConfig() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
good := testOptions()
|
good := testOptions()
|
||||||
good.Provider = "google"
|
good.Provider = "google"
|
||||||
|
@ -106,7 +73,7 @@ func TestNew(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
opts *Options
|
opts *config.Options
|
||||||
// want *Authenticate
|
// want *Authenticate
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
|
|
|
@ -5,34 +5,14 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/pomerium/envconfig"
|
"github.com/pomerium/pomerium/internal/config"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/policy"
|
"github.com/pomerium/pomerium/internal/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options contains configuration settings for the authorize service.
|
// ValidateOptions checks to see if configuration values are valid for the
|
||||||
type Options struct {
|
|
||||||
// SharedKey is used to validate requests between services
|
|
||||||
SharedKey string `envconfig:"SHARED_SECRET" required:"true"`
|
|
||||||
|
|
||||||
// Policy is a base64 encoded yaml blob which enumerates
|
|
||||||
// per-route access control policies.
|
|
||||||
Policy string `envconfig:"POLICY"`
|
|
||||||
PolicyFile string `envconfig:"POLICY_FILE"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptionsFromEnvConfig creates an authorize service options from environmental
|
|
||||||
// variables.
|
|
||||||
func OptionsFromEnvConfig() (*Options, error) {
|
|
||||||
o := new(Options)
|
|
||||||
if err := envconfig.Process("", o); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return o, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks to see if configuration values are valid for the
|
|
||||||
// authorize service. Returns first error, if found.
|
// authorize service. Returns first error, if found.
|
||||||
func (o *Options) Validate() error {
|
func ValidateOptions(o *config.Options) error {
|
||||||
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
|
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err)
|
return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err)
|
||||||
|
@ -72,11 +52,11 @@ type Authorize struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New validates and creates a new Authorize service from a set of Options
|
// New validates and creates a new Authorize service from a set of Options
|
||||||
func New(opts *Options) (*Authorize, error) {
|
func New(opts *config.Options) (*Authorize, error) {
|
||||||
if opts == nil {
|
if opts == nil {
|
||||||
return nil, errors.New("authorize: options cannot be nil")
|
return nil, errors.New("authorize: options cannot be nil")
|
||||||
}
|
}
|
||||||
if err := opts.Validate(); err != nil {
|
if err := ValidateOptions(opts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// errors handled by validate
|
// errors handled by validate
|
||||||
|
|
|
@ -4,40 +4,10 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
|
||||||
|
|
||||||
func TestOptionsFromEnvConfig(t *testing.T) {
|
"github.com/pomerium/pomerium/internal/config"
|
||||||
t.Parallel()
|
)
|
||||||
os.Clearenv()
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
want *Options
|
|
||||||
envKey string
|
|
||||||
envValue string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{"shared secret missing", nil, "", "", true},
|
|
||||||
{"with secret", &Options{SharedKey: "aGkK"}, "SHARED_SECRET", "aGkK", false},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.envKey != "" {
|
|
||||||
os.Setenv(tt.envKey, tt.envValue)
|
|
||||||
defer os.Unsetenv(tt.envKey)
|
|
||||||
}
|
|
||||||
got, err := OptionsFromEnvConfig()
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("OptionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("OptionsFromEnvConfig() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
@ -76,7 +46,7 @@ func TestNew(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
o := &Options{SharedKey: tt.SharedKey, Policy: tt.Policy, PolicyFile: tt.PolicyFile}
|
o := &config.Options{SharedKey: tt.SharedKey, Policy: tt.Policy, PolicyFile: tt.PolicyFile}
|
||||||
if tt.name == "nil options" {
|
if tt.name == "nil options" {
|
||||||
o = nil
|
o = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/authenticate"
|
"github.com/pomerium/pomerium/authenticate"
|
||||||
"github.com/pomerium/pomerium/authorize"
|
"github.com/pomerium/pomerium/authorize"
|
||||||
|
"github.com/pomerium/pomerium/internal/config"
|
||||||
"github.com/pomerium/pomerium/internal/https"
|
"github.com/pomerium/pomerium/internal/https"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
"github.com/pomerium/pomerium/internal/middleware"
|
"github.com/pomerium/pomerium/internal/middleware"
|
||||||
|
@ -30,7 +31,7 @@ func main() {
|
||||||
fmt.Println(version.FullVersion())
|
fmt.Println(version.FullVersion())
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
opt, err := optionsFromEnvConfig()
|
opt, err := config.OptionsFromEnvConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("cmd/pomerium: options")
|
log.Fatal().Err(err).Msg("cmd/pomerium: options")
|
||||||
}
|
}
|
||||||
|
@ -41,17 +42,17 @@ func main() {
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
_, err = newAuthenticateService(opt.Services, mux, grpcServer)
|
_, err = newAuthenticateService(opt, mux, grpcServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("cmd/pomerium: authenticate")
|
log.Fatal().Err(err).Msg("cmd/pomerium: authenticate")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = newAuthorizeService(opt.Services, grpcServer)
|
_, err = newAuthorizeService(opt, grpcServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("cmd/pomerium: authorize")
|
log.Fatal().Err(err).Msg("cmd/pomerium: authorize")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = newProxyService(opt.Services, mux)
|
_, err = newProxyService(opt, mux)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("cmd/pomerium: proxy")
|
log.Fatal().Err(err).Msg("cmd/pomerium: proxy")
|
||||||
}
|
}
|
||||||
|
@ -102,32 +103,24 @@ func startRedirectServer(addr string) (*http.Server, error) {
|
||||||
return srv, nil
|
return srv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAuthenticateService(s string, mux *http.ServeMux, rpc *grpc.Server) (*authenticate.Authenticate, error) {
|
func newAuthenticateService(opt *config.Options, mux *http.ServeMux, rpc *grpc.Server) (*authenticate.Authenticate, error) {
|
||||||
if !isAuthenticate(s) {
|
if opt == nil || !config.IsAuthenticate(opt.Services) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
opts, err := authenticate.OptionsFromEnvConfig()
|
service, err := authenticate.New(opt)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
service, err := authenticate.New(opts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pbAuthenticate.RegisterAuthenticatorServer(rpc, service)
|
pbAuthenticate.RegisterAuthenticatorServer(rpc, service)
|
||||||
mux.Handle(urlutil.StripPort(opts.AuthenticateURL.Host)+"/", service.Handler())
|
mux.Handle(urlutil.StripPort(opt.AuthenticateURL.Host)+"/", service.Handler())
|
||||||
return service, nil
|
return service, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAuthorizeService(s string, rpc *grpc.Server) (*authorize.Authorize, error) {
|
func newAuthorizeService(opt *config.Options, rpc *grpc.Server) (*authorize.Authorize, error) {
|
||||||
if !isAuthorize(s) {
|
if opt == nil || !config.IsAuthorize(opt.Services) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
opts, err := authorize.OptionsFromEnvConfig()
|
service, err := authorize.New(opt)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
service, err := authorize.New(opts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -135,15 +128,11 @@ func newAuthorizeService(s string, rpc *grpc.Server) (*authorize.Authorize, erro
|
||||||
return service, nil
|
return service, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProxyService(s string, mux *http.ServeMux) (*proxy.Proxy, error) {
|
func newProxyService(opt *config.Options, mux *http.ServeMux) (*proxy.Proxy, error) {
|
||||||
if !isProxy(s) {
|
if opt == nil || !config.IsProxy(opt.Services) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
opts, err := proxy.OptionsFromEnvConfig()
|
service, err := proxy.New(opt)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
service, err := proxy.New(opts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -151,7 +140,7 @@ func newProxyService(s string, mux *http.ServeMux) (*proxy.Proxy, error) {
|
||||||
return service, nil
|
return service, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapMiddleware(o *Options, mux *http.ServeMux) http.Handler {
|
func wrapMiddleware(o *config.Options, mux *http.ServeMux) http.Handler {
|
||||||
c := middleware.NewChain()
|
c := middleware.NewChain()
|
||||||
c = c.Append(middleware.NewHandler(log.Logger))
|
c = c.Append(middleware.NewHandler(log.Logger))
|
||||||
c = c.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
|
c = c.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
|
||||||
|
@ -179,3 +168,17 @@ func wrapMiddleware(o *Options, mux *http.ServeMux) http.Handler {
|
||||||
c = c.Append(middleware.Healthcheck("/ping", version.UserAgent()))
|
c = c.Append(middleware.Healthcheck("/ping", version.UserAgent()))
|
||||||
return c.Then(mux)
|
return c.Then(mux)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOptions() (*config.Options, error) {
|
||||||
|
o, err := config.OptionsFromEnvConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if o.Debug {
|
||||||
|
log.SetDebugMode()
|
||||||
|
}
|
||||||
|
if o.LogLevel != "" {
|
||||||
|
log.SetLevel(o.LogLevel)
|
||||||
|
}
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
|
@ -5,10 +5,13 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/internal/config"
|
||||||
"github.com/pomerium/pomerium/internal/middleware"
|
"github.com/pomerium/pomerium/internal/middleware"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
@ -47,42 +50,41 @@ func Test_startRedirectServer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_newAuthenticateService(t *testing.T) {
|
func Test_newAuthenticateService(t *testing.T) {
|
||||||
os.Clearenv()
|
|
||||||
grpcAuth := middleware.NewSharedSecretCred("test")
|
grpcAuth := middleware.NewSharedSecretCred("test")
|
||||||
grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)}
|
grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)}
|
||||||
grpcServer := grpc.NewServer(grpcOpts...)
|
grpcServer := grpc.NewServer(grpcOpts...)
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
s string
|
s string
|
||||||
envKey string
|
Field string
|
||||||
envValue string
|
Value string
|
||||||
|
|
||||||
wantHostname string
|
wantHostname string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"wrong service", "proxy", "", "", "", false},
|
{"wrong service", "proxy", "", "", "", false},
|
||||||
{"bad", "authenticate", "SHARED_SECRET", "error!", "", true},
|
{"bad", "authenticate", "SharedKey", "error!", "", true},
|
||||||
{"bad emv", "authenticate", "COOKIE_REFRESH", "error!", "", true},
|
{"good", "authenticate", "ClientID", "test", "auth.server.com", false},
|
||||||
{"good", "authenticate", "IDP_CLIENT_ID", "test", "auth.server.com", false},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
os.Setenv("IDP_PROVIDER", "google")
|
authURL, _ := url.Parse("http://auth.server.com")
|
||||||
os.Setenv("IDP_CLIENT_SECRET", "TEST")
|
testOpts := config.NewOptions()
|
||||||
os.Setenv("SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=")
|
testOpts.Provider = "google"
|
||||||
os.Setenv("COOKIE_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=")
|
testOpts.ClientSecret = "TEST"
|
||||||
os.Setenv("AUTHENTICATE_SERVICE_URL", "http://auth.server.com")
|
testOpts.SharedKey = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
|
||||||
defer os.Unsetenv("IDP_CLIENT_ID")
|
testOpts.CookieSecret = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
|
||||||
defer os.Unsetenv("IDP_CLIENT_SECRET")
|
testOpts.AuthenticateURL = authURL
|
||||||
defer os.Unsetenv("SHARED_SECRET")
|
testOpts.Services = tt.s
|
||||||
defer os.Unsetenv("COOKIE_SECRET")
|
|
||||||
|
|
||||||
os.Setenv(tt.envKey, tt.envValue)
|
if tt.Field != "" {
|
||||||
defer os.Unsetenv(tt.envKey)
|
testOptsField := reflect.ValueOf(testOpts).Elem().FieldByName(tt.Field)
|
||||||
|
testOptsField.Set(reflect.ValueOf(tt).FieldByName("Value"))
|
||||||
|
}
|
||||||
|
|
||||||
_, err := newAuthenticateService(tt.s, mux, grpcServer)
|
_, err := newAuthenticateService(testOpts, mux, grpcServer)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("newAuthenticateService() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("newAuthenticateService() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
@ -99,27 +101,31 @@ func Test_newAuthorizeService(t *testing.T) {
|
||||||
grpcServer := grpc.NewServer(grpcOpts...)
|
grpcServer := grpc.NewServer(grpcOpts...)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
s string
|
s string
|
||||||
envKey string
|
Field string
|
||||||
envValue string
|
Value string
|
||||||
|
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"wrong service", "proxy", "", "", false},
|
{"wrong service", "proxy", "", "", false},
|
||||||
{"bad option parsing", "authorize", "SHARED_SECRET", "false", true},
|
{"bad option parsing", "authorize", "SharedKey", "false", true},
|
||||||
{"bad env", "authorize", "POLICY", "error!", true},
|
{"bad env", "authorize", "Policy", "error!", true},
|
||||||
{"good", "authorize", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
|
{"good", "authorize", "SharedKey", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
os.Setenv("POLICY", "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw==")
|
testOpts := config.NewOptions()
|
||||||
os.Setenv("COOKIE_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=")
|
testOpts.Services = tt.s
|
||||||
defer os.Unsetenv("SHARED_SECRET")
|
testOpts.CookieSecret = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
|
||||||
defer os.Unsetenv("COOKIE_SECRET")
|
testOpts.Policy = "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw=="
|
||||||
os.Setenv(tt.envKey, tt.envValue)
|
|
||||||
defer os.Unsetenv(tt.envKey)
|
if tt.Field != "" {
|
||||||
_, err := newAuthorizeService(tt.s, grpcServer)
|
testOptsField := reflect.ValueOf(testOpts).Elem().FieldByName(tt.Field)
|
||||||
|
testOptsField.Set(reflect.ValueOf(tt).FieldByName("Value"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := newAuthorizeService(testOpts, grpcServer)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("newAuthorizeService() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("newAuthorizeService() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
@ -131,34 +137,33 @@ func Test_newAuthorizeService(t *testing.T) {
|
||||||
func Test_newProxyeService(t *testing.T) {
|
func Test_newProxyeService(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
s string
|
s string
|
||||||
envKey string
|
Field string
|
||||||
envValue string
|
Value string
|
||||||
|
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"wrong service", "authenticate", "", "", false},
|
{"wrong service", "authenticate", "", "", false},
|
||||||
{"bad option parsing", "proxy", "SHARED_SECRET", "false", true},
|
{"bad option parsing", "proxy", "SharedKey", "false", true},
|
||||||
{"bad env", "proxy", "POLICY", "error!", true},
|
{"bad env", "proxy", "Policy", "error!", true},
|
||||||
{"bad encoding for envar", "proxy", "COOKIE_REFRESH", "error!", true},
|
{"good", "proxy", "SharedKey", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
|
||||||
{"good", "proxy", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
testOpts := config.NewOptions()
|
||||||
|
testOpts.AuthenticateURL, _ = url.Parse("https://authenticate.example.com")
|
||||||
|
testOpts.AuthorizeURL, _ = url.Parse("https://authorize.example.com")
|
||||||
|
testOpts.Policy = "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw=="
|
||||||
|
testOpts.CookieSecret = "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM="
|
||||||
|
testOpts.Services = tt.s
|
||||||
|
|
||||||
os.Setenv("AUTHENTICATE_SERVICE_URL", "https://authenticate.example.com")
|
if tt.Field != "" {
|
||||||
os.Setenv("AUTHORIZE_SERVICE_URL", "https://authorize.example.com")
|
testOptsField := reflect.ValueOf(testOpts).Elem().FieldByName(tt.Field)
|
||||||
os.Setenv("POLICY", "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbgogIGFsbG93ZWRfZG9tYWluczoKICAgIC0gcG9tZXJpdW0uaW8KICBjb3JzX2FsbG93X3ByZWZsaWdodDogdHJ1ZQogIHRpbWVvdXQ6IDMwcwotIGZyb206IGV4dGVybmFsLWh0dHBiaW4uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHBiaW4ub3JnCiAgYWxsb3dlZF9kb21haW5zOgogICAgLSBnbWFpbC5jb20KLSBmcm9tOiB3ZWlyZGx5c3NsLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vbmV2ZXJzc2wuY29tCiAgYWxsb3dlZF91c2VyczoKICAgIC0gYmRkQHBvbWVyaXVtLmlvCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucwogICAgLSBkZXZlbG9wZXJzCi0gZnJvbTogaGVsbG8uY29ycC5iZXlvbmRwZXJpbWV0ZXIuY29tCiAgdG86IGh0dHA6Ly9oZWxsbzo4MDgwCiAgYWxsb3dlZF9ncm91cHM6CiAgICAtIGFkbWlucw==")
|
testOptsField.Set(reflect.ValueOf(tt).FieldByName("Value"))
|
||||||
os.Setenv("COOKIE_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=")
|
}
|
||||||
defer os.Unsetenv("AUTHENTICATE_SERVICE_URL")
|
_, err := newProxyService(testOpts, mux)
|
||||||
defer os.Unsetenv("AUTHORIZE_SERVICE_URL")
|
|
||||||
defer os.Unsetenv("SHARED_SECRET")
|
|
||||||
defer os.Unsetenv("COOKIE_SECRET")
|
|
||||||
os.Setenv(tt.envKey, tt.envValue)
|
|
||||||
defer os.Unsetenv(tt.envKey)
|
|
||||||
_, err := newProxyService(tt.s, mux)
|
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("newProxyService() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("newProxyService() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
@ -168,7 +173,7 @@ func Test_newProxyeService(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_wrapMiddleware(t *testing.T) {
|
func Test_wrapMiddleware(t *testing.T) {
|
||||||
o := &Options{
|
o := &config.Options{
|
||||||
Services: "all",
|
Services: "all",
|
||||||
Headers: map[string]string{
|
Headers: map[string]string{
|
||||||
"X-Content-Type-Options": "nosniff",
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
@ -197,3 +202,34 @@ func Test_wrapMiddleware(t *testing.T) {
|
||||||
t.Errorf("handler returned unexpected body: got %v want %v", body, expected)
|
t.Errorf("handler returned unexpected body: got %v want %v", body, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func Test_parseOptions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
envKey string
|
||||||
|
envValue string
|
||||||
|
|
||||||
|
wantSharedKey string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"no shared secret", "", "", "", true},
|
||||||
|
{"good", "SHARED_SECRET", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", "YixWi1MYh77NMECGGIJQevoonYtVF+ZPRkQZrrmeRqM=", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
os.Setenv(tt.envKey, tt.envValue)
|
||||||
|
defer os.Unsetenv(tt.envKey)
|
||||||
|
|
||||||
|
got, err := parseOptions()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("parseOptions() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != nil && got.SharedKey != tt.wantSharedKey {
|
||||||
|
t.Errorf("parseOptions()\n")
|
||||||
|
t.Errorf("got: %+v\n", got.SharedKey)
|
||||||
|
t.Errorf("want: %+v\n", tt.wantSharedKey)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,140 +0,0 @@
|
||||||
package main // import "github.com/pomerium/pomerium/cmd/pomerium"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pomerium/envconfig"
|
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DisableHeaderKey is the key used to check whether to disable setting header
|
|
||||||
const DisableHeaderKey = "disable"
|
|
||||||
|
|
||||||
// Options are the global environmental flags used to set up pomerium's services.
|
|
||||||
// If a base64 encoded certificate and key are not provided as environmental variables,
|
|
||||||
// or if a file location is not provided, the server will attempt to find a matching keypair
|
|
||||||
// in the local directory as `./cert.pem` and `./privkey.pem` respectively.
|
|
||||||
type Options struct {
|
|
||||||
// Debug outputs human-readable logs to Stdout.
|
|
||||||
Debug bool `envconfig:"POMERIUM_DEBUG"`
|
|
||||||
|
|
||||||
// LogLevel sets the global override for log level. All Loggers will use at least this value.
|
|
||||||
// Possible options are "info","warn", and "error". Defaults to "debug".
|
|
||||||
LogLevel string `envconfig:"LOG_LEVEL"`
|
|
||||||
|
|
||||||
// SharedKey is the shared secret authorization key used to mutually authenticate
|
|
||||||
// requests between services.
|
|
||||||
SharedKey string `envconfig:"SHARED_SECRET"`
|
|
||||||
|
|
||||||
// Services is a list enabled service mode. If none are selected, "all" is used.
|
|
||||||
// Available options are : "all", "authenticate", "proxy".
|
|
||||||
Services string `envconfig:"SERVICES"`
|
|
||||||
|
|
||||||
// Addr specifies the host and port on which the server should serve
|
|
||||||
// HTTPS requests. If empty, ":https" is used.
|
|
||||||
Addr string `envconfig:"ADDRESS"`
|
|
||||||
|
|
||||||
// Cert and Key specifies the base64 encoded TLS certificates to use.
|
|
||||||
Cert string `envconfig:"CERTIFICATE"`
|
|
||||||
Key string `envconfig:"CERTIFICATE_KEY"`
|
|
||||||
|
|
||||||
// CertFile and KeyFile specifies the TLS certificates to use.
|
|
||||||
CertFile string `envconfig:"CERTIFICATE_FILE"`
|
|
||||||
KeyFile string `envconfig:"CERTIFICATE_KEY_FILE"`
|
|
||||||
|
|
||||||
// HttpRedirectAddr, if set, specifies the host and port to run the HTTP
|
|
||||||
// to HTTPS redirect server on. For example, ":http" would start a server
|
|
||||||
// on port 80. If empty, no redirect server is started.
|
|
||||||
HTTPRedirectAddr string `envconfig:"HTTP_REDIRECT_ADDR"`
|
|
||||||
|
|
||||||
// Headers to set on all proxied requests. Add a 'disable' key map to turn off.
|
|
||||||
Headers map[string]string `envconfig:"HEADERS"`
|
|
||||||
|
|
||||||
// Timeout settings : https://github.com/pomerium/pomerium/issues/40
|
|
||||||
ReadTimeout time.Duration `envconfig:"TIMEOUT_READ"`
|
|
||||||
WriteTimeout time.Duration `envconfig:"TIMEOUT_WRITE"`
|
|
||||||
ReadHeaderTimeout time.Duration `envconfig:"TIMEOUT_READ_HEADER"`
|
|
||||||
IdleTimeout time.Duration `envconfig:"TIMEOUT_IDLE"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultOptions = &Options{
|
|
||||||
Debug: false,
|
|
||||||
LogLevel: "debug",
|
|
||||||
Services: "all",
|
|
||||||
Headers: map[string]string{
|
|
||||||
"X-Content-Type-Options": "nosniff",
|
|
||||||
"X-Frame-Options": "SAMEORIGIN",
|
|
||||||
"X-XSS-Protection": "1; mode=block",
|
|
||||||
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// optionsFromEnvConfig builds the main binary's configuration
|
|
||||||
// options from provided environmental variables
|
|
||||||
func optionsFromEnvConfig() (*Options, error) {
|
|
||||||
o := defaultOptions
|
|
||||||
if err := envconfig.Process("", o); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !isValidService(o.Services) {
|
|
||||||
return nil, fmt.Errorf("%s is an invalid service type", o.Services)
|
|
||||||
}
|
|
||||||
if o.SharedKey == "" {
|
|
||||||
return nil, errors.New("shared-key cannot be empty")
|
|
||||||
}
|
|
||||||
if o.Debug {
|
|
||||||
log.SetDebugMode()
|
|
||||||
}
|
|
||||||
if o.LogLevel != "" {
|
|
||||||
log.SetLevel(o.LogLevel)
|
|
||||||
}
|
|
||||||
if _, disable := o.Headers[DisableHeaderKey]; disable {
|
|
||||||
o.Headers = make(map[string]string)
|
|
||||||
}
|
|
||||||
return o, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidService checks to see if a service is a valid service mode
|
|
||||||
func isValidService(s string) bool {
|
|
||||||
switch s {
|
|
||||||
case
|
|
||||||
"all",
|
|
||||||
"proxy",
|
|
||||||
"authorize",
|
|
||||||
"authenticate":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAuthenticate(s string) bool {
|
|
||||||
switch s {
|
|
||||||
case
|
|
||||||
"all",
|
|
||||||
"authenticate":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAuthorize(s string) bool {
|
|
||||||
switch s {
|
|
||||||
case
|
|
||||||
"all",
|
|
||||||
"authorize":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isProxy(s string) bool {
|
|
||||||
switch s {
|
|
||||||
case
|
|
||||||
"all",
|
|
||||||
"proxy":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
226
internal/config/options.go
Normal file
226
internal/config/options.go
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pomerium/envconfig"
|
||||||
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
|
"github.com/pomerium/pomerium/internal/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DisableHeaderKey is the key used to check whether to disable setting header
|
||||||
|
const DisableHeaderKey = "disable"
|
||||||
|
|
||||||
|
// Options are the global environmental flags used to set up pomerium's services.
|
||||||
|
// If a base64 encoded certificate and key are not provided as environmental variables,
|
||||||
|
// or if a file location is not provided, the server will attempt to find a matching keypair
|
||||||
|
// in the local directory as `./cert.pem` and `./privkey.pem` respectively.
|
||||||
|
type Options struct {
|
||||||
|
// Debug outputs human-readable logs to Stdout.
|
||||||
|
Debug bool `envconfig:"POMERIUM_DEBUG"`
|
||||||
|
|
||||||
|
// LogLevel sets the global override for log level. All Loggers will use at least this value.
|
||||||
|
// Possible options are "info","warn", and "error". Defaults to "debug".
|
||||||
|
LogLevel string `envconfig:"LOG_LEVEL"`
|
||||||
|
|
||||||
|
// SharedKey is the shared secret authorization key used to mutually authenticate
|
||||||
|
// requests between services.
|
||||||
|
SharedKey string `envconfig:"SHARED_SECRET"`
|
||||||
|
|
||||||
|
// Services is a list enabled service mode. If none are selected, "all" is used.
|
||||||
|
// Available options are : "all", "authenticate", "proxy".
|
||||||
|
Services string `envconfig:"SERVICES"`
|
||||||
|
|
||||||
|
// Addr specifies the host and port on which the server should serve
|
||||||
|
// HTTPS requests. If empty, ":https" is used.
|
||||||
|
Addr string `envconfig:"ADDRESS"`
|
||||||
|
|
||||||
|
// Cert and Key specifies the base64 encoded TLS certificates to use.
|
||||||
|
Cert string `envconfig:"CERTIFICATE"`
|
||||||
|
Key string `envconfig:"CERTIFICATE_KEY"`
|
||||||
|
|
||||||
|
// CertFile and KeyFile specifies the TLS certificates to use.
|
||||||
|
CertFile string `envconfig:"CERTIFICATE_FILE"`
|
||||||
|
KeyFile string `envconfig:"CERTIFICATE_KEY_FILE"`
|
||||||
|
|
||||||
|
// HttpRedirectAddr, if set, specifies the host and port to run the HTTP
|
||||||
|
// to HTTPS redirect server on. For example, ":http" would start a server
|
||||||
|
// on port 80. If empty, no redirect server is started.
|
||||||
|
HTTPRedirectAddr string `envconfig:"HTTP_REDIRECT_ADDR"`
|
||||||
|
|
||||||
|
// Timeout settings : https://github.com/pomerium/pomerium/issues/40
|
||||||
|
ReadTimeout time.Duration `envconfig:"TIMEOUT_READ"`
|
||||||
|
WriteTimeout time.Duration `envconfig:"TIMEOUT_WRITE"`
|
||||||
|
ReadHeaderTimeout time.Duration `envconfig:"TIMEOUT_READ_HEADER"`
|
||||||
|
IdleTimeout time.Duration `envconfig:"TIMEOUT_IDLE"`
|
||||||
|
|
||||||
|
// Policy is a base64 encoded yaml blob which enumerates
|
||||||
|
// per-route access control policies.
|
||||||
|
Policy string `envconfig:"POLICY"`
|
||||||
|
PolicyFile string `envconfig:"POLICY_FILE"`
|
||||||
|
|
||||||
|
// AuthenticateURL represents the externally accessible http endpoints
|
||||||
|
// used for authentication requests and callbacks
|
||||||
|
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
|
||||||
|
|
||||||
|
// Session/Cookie management
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||||
|
AuthenticateCookieName string
|
||||||
|
ProxyCookieName string
|
||||||
|
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
||||||
|
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
||||||
|
CookieSecure bool `envconfig:"COOKIE_SECURE"`
|
||||||
|
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
||||||
|
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
||||||
|
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
|
||||||
|
|
||||||
|
// Identity provider configuration variables as specified by RFC6749
|
||||||
|
// https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
|
||||||
|
ClientID string `envconfig:"IDP_CLIENT_ID"`
|
||||||
|
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"`
|
||||||
|
Provider string `envconfig:"IDP_PROVIDER"`
|
||||||
|
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
|
||||||
|
Scopes []string `envconfig:"IDP_SCOPES"`
|
||||||
|
ServiceAccount string `envconfig:"IDP_SERVICE_ACCOUNT"`
|
||||||
|
|
||||||
|
Policies []policy.Policy `envconfig:"POLICY"`
|
||||||
|
|
||||||
|
// AuthenticateInternalAddr is used as an override when using a load balancer
|
||||||
|
// or ingress that does not natively support routing gRPC.
|
||||||
|
AuthenticateInternalAddr string `envconfig:"AUTHENTICATE_INTERNAL_URL"`
|
||||||
|
|
||||||
|
// AuthorizeURL is the routable destination of the authorize service's
|
||||||
|
// gRPC endpoint. NOTE: As above, many load balancers do not support
|
||||||
|
// externally routed gRPC so this may be an internal location.
|
||||||
|
AuthorizeURL *url.URL `envconfig:"AUTHORIZE_SERVICE_URL"`
|
||||||
|
|
||||||
|
// Settings to enable custom behind-the-ingress service communication
|
||||||
|
OverrideCertificateName string `envconfig:"OVERRIDE_CERTIFICATE_NAME"`
|
||||||
|
CA string `envconfig:"CERTIFICATE_AUTHORITY"`
|
||||||
|
CAFile string `envconfig:"CERTIFICATE_AUTHORITY_FILE"`
|
||||||
|
|
||||||
|
// SigningKey is a base64 encoded private key used to add a JWT-signature.
|
||||||
|
// https://www.pomerium.io/docs/signed-headers.html
|
||||||
|
SigningKey string `envconfig:"SIGNING_KEY"`
|
||||||
|
|
||||||
|
// Headers to set on all proxied requests. Add a 'disable' key map to turn off.
|
||||||
|
Headers map[string]string `envconfig:"HEADERS"`
|
||||||
|
|
||||||
|
// Sub-routes
|
||||||
|
Routes map[string]string `envconfig:"ROUTES"`
|
||||||
|
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptions returns a new options struct with default vaules
|
||||||
|
func NewOptions() *Options {
|
||||||
|
o := &Options{
|
||||||
|
Debug: false,
|
||||||
|
LogLevel: "debug",
|
||||||
|
Services: "all",
|
||||||
|
AuthenticateCookieName: "_pomerium_authenticate",
|
||||||
|
CookieHTTPOnly: true,
|
||||||
|
CookieSecure: true,
|
||||||
|
CookieExpire: time.Duration(14) * time.Hour,
|
||||||
|
CookieRefresh: time.Duration(30) * time.Minute,
|
||||||
|
ProxyCookieName: "_pomerium_proxy",
|
||||||
|
DefaultUpstreamTimeout: time.Duration(30) * time.Second,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"X-Frame-Options": "SAMEORIGIN",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
||||||
|
},
|
||||||
|
Addr: ":https",
|
||||||
|
CertFile: filepath.Join(findPwd(), "cert.pem"),
|
||||||
|
KeyFile: filepath.Join(findPwd(), "privkey.pem"),
|
||||||
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
WriteTimeout: 0, // support streaming by default
|
||||||
|
IdleTimeout: 5 * time.Minute,
|
||||||
|
AuthenticateURL: new(url.URL),
|
||||||
|
AuthorizeURL: new(url.URL),
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionsFromEnvConfig builds the main binary's configuration
|
||||||
|
// options from provided environmental variables
|
||||||
|
func OptionsFromEnvConfig() (*Options, error) {
|
||||||
|
o := NewOptions()
|
||||||
|
if err := envconfig.Process("", o); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !IsValidService(o.Services) {
|
||||||
|
return nil, fmt.Errorf("%s is an invalid service type", o.Services)
|
||||||
|
}
|
||||||
|
if o.SharedKey == "" {
|
||||||
|
return nil, errors.New("shared-key cannot be empty")
|
||||||
|
}
|
||||||
|
if o.Debug {
|
||||||
|
log.SetDebugMode()
|
||||||
|
}
|
||||||
|
if o.LogLevel != "" {
|
||||||
|
log.SetLevel(o.LogLevel)
|
||||||
|
}
|
||||||
|
if _, disable := o.Headers[DisableHeaderKey]; disable {
|
||||||
|
o.Headers = make(map[string]string)
|
||||||
|
}
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findPwd returns best guess at current working directory
|
||||||
|
func findPwd() string {
|
||||||
|
p, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidService checks to see if a service is a valid service mode
|
||||||
|
func IsValidService(s string) bool {
|
||||||
|
switch s {
|
||||||
|
case
|
||||||
|
"all",
|
||||||
|
"proxy",
|
||||||
|
"authorize",
|
||||||
|
"authenticate":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsAuthenticate(s string) bool {
|
||||||
|
switch s {
|
||||||
|
case
|
||||||
|
"all",
|
||||||
|
"authenticate":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsAuthorize(s string) bool {
|
||||||
|
switch s {
|
||||||
|
case
|
||||||
|
"all",
|
||||||
|
"authorize":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsProxy(s string) bool {
|
||||||
|
switch s {
|
||||||
|
case
|
||||||
|
"all",
|
||||||
|
"proxy":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main // import "github.com/pomerium/pomerium/cmd/pomerium"
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_optionsFromEnvConfig(t *testing.T) {
|
func Test_optionsFromEnvConfig(t *testing.T) {
|
||||||
good := defaultOptions
|
good := NewOptions()
|
||||||
good.SharedKey = "test"
|
good.SharedKey = "test"
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -31,7 +31,7 @@ func Test_optionsFromEnvConfig(t *testing.T) {
|
||||||
if tt.envKey != "SHARED_SECRET" {
|
if tt.envKey != "SHARED_SECRET" {
|
||||||
os.Setenv("SHARED_SECRET", "test")
|
os.Setenv("SHARED_SECRET", "test")
|
||||||
}
|
}
|
||||||
got, err := optionsFromEnvConfig()
|
got, err := OptionsFromEnvConfig()
|
||||||
os.Unsetenv(tt.envKey)
|
os.Unsetenv(tt.envKey)
|
||||||
|
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
|
@ -39,7 +39,7 @@ func Test_optionsFromEnvConfig(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("optionsFromEnvConfig() = got %v, want %v", got, tt.want)
|
t.Errorf("optionsFromEnvConfig() = got %#v,\n want %#v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ func Test_isValidService(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := isValidService(tt.service); got != tt.want {
|
if got := IsValidService(tt.service); got != tt.want {
|
||||||
t.Errorf("isValidService() = %v, want %v", got, tt.want)
|
t.Errorf("isValidService() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -83,7 +83,7 @@ func Test_isAuthenticate(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := isAuthenticate(tt.service); got != tt.want {
|
if got := IsAuthenticate(tt.service); got != tt.want {
|
||||||
t.Errorf("isAuthenticate() = %v, want %v", got, tt.want)
|
t.Errorf("isAuthenticate() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/internal/config"
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
"github.com/pomerium/pomerium/internal/sessions"
|
||||||
"github.com/pomerium/pomerium/proxy/clients"
|
"github.com/pomerium/pomerium/proxy/clients"
|
||||||
)
|
)
|
||||||
|
@ -361,7 +362,7 @@ func TestProxy_Proxy(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
options *Options
|
options *config.Options
|
||||||
method string
|
method string
|
||||||
header http.Header
|
header http.Header
|
||||||
host string
|
host string
|
||||||
|
@ -371,8 +372,8 @@ func TestProxy_Proxy(t *testing.T) {
|
||||||
wantStatus int
|
wantStatus int
|
||||||
}{
|
}{
|
||||||
// weirdly, we want 503 here because that means proxy is trying to route a domain (example.com) that we dont control. Weird. I know.
|
// weirdly, we want 503 here because that means proxy is trying to route a domain (example.com) that we dont control. Weird. I know.
|
||||||
{"good", opts, http.MethodGet, defaultHeaders, "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusServiceUnavailable},
|
{"good", opts, http.MethodGet, defaultHeaders, "https://corp.example.notatld/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadGateway},
|
||||||
{"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusServiceUnavailable},
|
{"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://corp.example.notatld/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
||||||
// same request as above, but with cors_allow_preflight=false in the policy
|
// same request as above, but with cors_allow_preflight=false in the policy
|
||||||
{"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
{"valid cors, but not allowed", opts, http.MethodOptions, goodCORSHeaders, "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
|
||||||
// cors allowed, but the request is missing proper headers
|
// cors allowed, but the request is missing proper headers
|
||||||
|
|
|
@ -10,9 +10,8 @@ import (
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pomerium/envconfig"
|
"github.com/pomerium/pomerium/internal/config"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
|
@ -33,71 +32,9 @@ const (
|
||||||
HeaderGroups = "x-pomerium-authenticated-user-groups"
|
HeaderGroups = "x-pomerium-authenticated-user-groups"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options represents the configurations available for the proxy service.
|
// ValidateOptions checks that proper configuration settings are set to create
|
||||||
type Options struct {
|
|
||||||
Policy string `envconfig:"POLICY"`
|
|
||||||
PolicyFile string `envconfig:"POLICY_FILE"`
|
|
||||||
|
|
||||||
// AuthenticateURL represents the externally accessible http endpoints
|
|
||||||
// used for authentication requests and callbacks
|
|
||||||
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
|
|
||||||
// AuthenticateInternalAddr is used as an override when using a load balancer
|
|
||||||
// or ingress that does not natively support routing gRPC.
|
|
||||||
AuthenticateInternalAddr string `envconfig:"AUTHENTICATE_INTERNAL_URL"`
|
|
||||||
|
|
||||||
// AuthorizeURL is the routable destination of the authorize service's
|
|
||||||
// gRPC endpoint. NOTE: As above, many load balancers do not support
|
|
||||||
// externally routed gRPC so this may be an internal location.
|
|
||||||
AuthorizeURL *url.URL `envconfig:"AUTHORIZE_SERVICE_URL"`
|
|
||||||
|
|
||||||
// Settings to enable custom behind-the-ingress service communication
|
|
||||||
OverrideCertificateName string `envconfig:"OVERRIDE_CERTIFICATE_NAME"`
|
|
||||||
CA string `envconfig:"CERTIFICATE_AUTHORITY"`
|
|
||||||
CAFile string `envconfig:"CERTIFICATE_AUTHORITY_FILE"`
|
|
||||||
|
|
||||||
// SigningKey is a base64 encoded private key used to add a JWT-signature.
|
|
||||||
// https://www.pomerium.io/docs/signed-headers.html
|
|
||||||
SigningKey string `envconfig:"SIGNING_KEY"`
|
|
||||||
// SharedKey is a 32 byte random key used to authenticate access between services.
|
|
||||||
SharedKey string `envconfig:"SHARED_SECRET"`
|
|
||||||
|
|
||||||
// Session/Cookie management
|
|
||||||
CookieName string
|
|
||||||
CookieSecret string `envconfig:"COOKIE_SECRET"`
|
|
||||||
CookieDomain string `envconfig:"COOKIE_DOMAIN"`
|
|
||||||
CookieSecure bool `envconfig:"COOKIE_SECURE"`
|
|
||||||
CookieHTTPOnly bool `envconfig:"COOKIE_HTTP_ONLY"`
|
|
||||||
CookieExpire time.Duration `envconfig:"COOKIE_EXPIRE"`
|
|
||||||
CookieRefresh time.Duration `envconfig:"COOKIE_REFRESH"`
|
|
||||||
|
|
||||||
// Sub-routes
|
|
||||||
Routes map[string]string `envconfig:"ROUTES"`
|
|
||||||
DefaultUpstreamTimeout time.Duration `envconfig:"DEFAULT_UPSTREAM_TIMEOUT"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOptions returns a new options struct
|
|
||||||
var defaultOptions = &Options{
|
|
||||||
CookieName: "_pomerium_proxy",
|
|
||||||
CookieHTTPOnly: true,
|
|
||||||
CookieSecure: true,
|
|
||||||
CookieExpire: time.Duration(14) * time.Hour,
|
|
||||||
CookieRefresh: time.Duration(30) * time.Minute,
|
|
||||||
DefaultUpstreamTimeout: time.Duration(30) * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptionsFromEnvConfig builds the identity provider service's configuration
|
|
||||||
// options from provided environmental variables
|
|
||||||
func OptionsFromEnvConfig() (*Options, error) {
|
|
||||||
o := defaultOptions
|
|
||||||
if err := envconfig.Process("", o); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return o, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks that proper configuration settings are set to create
|
|
||||||
// a proper Proxy instance
|
// a proper Proxy instance
|
||||||
func (o *Options) Validate() error {
|
func ValidateOptions(o *config.Options) error {
|
||||||
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
|
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err)
|
return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err)
|
||||||
|
@ -187,11 +124,11 @@ type routeConfig struct {
|
||||||
|
|
||||||
// New takes a Proxy service from options and a validation function.
|
// New takes a Proxy service from options and a validation function.
|
||||||
// Function returns an error if options fail to validate.
|
// Function returns an error if options fail to validate.
|
||||||
func New(opts *Options) (*Proxy, error) {
|
func New(opts *config.Options) (*Proxy, error) {
|
||||||
if opts == nil {
|
if opts == nil {
|
||||||
return nil, errors.New("options cannot be nil")
|
return nil, errors.New("options cannot be nil")
|
||||||
}
|
}
|
||||||
if err := opts.Validate(); err != nil {
|
if err := ValidateOptions(opts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// error explicitly handled by validate
|
// error explicitly handled by validate
|
||||||
|
@ -203,7 +140,7 @@ func New(opts *Options) (*Proxy, error) {
|
||||||
|
|
||||||
cookieStore, err := sessions.NewCookieStore(
|
cookieStore, err := sessions.NewCookieStore(
|
||||||
&sessions.CookieStoreOptions{
|
&sessions.CookieStoreOptions{
|
||||||
Name: opts.CookieName,
|
Name: opts.ProxyCookieName,
|
||||||
CookieDomain: opts.CookieDomain,
|
CookieDomain: opts.CookieDomain,
|
||||||
CookieSecure: opts.CookieSecure,
|
CookieSecure: opts.CookieSecure,
|
||||||
CookieHTTPOnly: opts.CookieHTTPOnly,
|
CookieHTTPOnly: opts.CookieHTTPOnly,
|
||||||
|
@ -326,11 +263,11 @@ func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReverseProxyHandler applies handler specific options to a given route.
|
// NewReverseProxyHandler applies handler specific options to a given route.
|
||||||
func NewReverseProxyHandler(o *Options, proxy *httputil.ReverseProxy, route *policy.Policy) (http.Handler, error) {
|
func NewReverseProxyHandler(o *config.Options, proxy *httputil.ReverseProxy, route *policy.Policy) (http.Handler, error) {
|
||||||
up := &UpstreamProxy{
|
up := &UpstreamProxy{
|
||||||
name: route.Destination.Host,
|
name: route.Destination.Host,
|
||||||
handler: proxy,
|
handler: proxy,
|
||||||
cookieName: o.CookieName,
|
cookieName: o.ProxyCookieName,
|
||||||
}
|
}
|
||||||
if len(o.SigningKey) != 0 {
|
if len(o.SigningKey) != 0 {
|
||||||
decodedSigningKey, _ := base64.StdEncoding.DecodeString(o.SigningKey)
|
decodedSigningKey, _ := base64.StdEncoding.DecodeString(o.SigningKey)
|
||||||
|
|
|
@ -7,49 +7,16 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/internal/config"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/policy"
|
"github.com/pomerium/pomerium/internal/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
|
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
|
||||||
|
|
||||||
func TestOptionsFromEnvConfig(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
want *Options
|
|
||||||
envKey string
|
|
||||||
envValue string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{"good default, no env settings", defaultOptions, "", "", false},
|
|
||||||
{"bad url", nil, "AUTHENTICATE_SERVICE_URL", "%.ugly", true},
|
|
||||||
{"good duration", defaultOptions, "COOKIE_REFRESH", "1m", false},
|
|
||||||
{"bad duration", nil, "COOKIE_REFRESH", "1sm", true},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.envKey != "" {
|
|
||||||
os.Setenv(tt.envKey, tt.envValue)
|
|
||||||
defer os.Unsetenv(tt.envKey)
|
|
||||||
}
|
|
||||||
got, err := OptionsFromEnvConfig()
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("OptionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("OptionsFromEnvConfig() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewReverseProxy(t *testing.T) {
|
func TestNewReverseProxy(t *testing.T) {
|
||||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
@ -88,7 +55,7 @@ func TestNewReverseProxyHandler(t *testing.T) {
|
||||||
backendHost := net.JoinHostPort(backendHostname, backendPort)
|
backendHost := net.JoinHostPort(backendHostname, backendPort)
|
||||||
proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
|
proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
|
||||||
proxyHandler := NewReverseProxy(proxyURL)
|
proxyHandler := NewReverseProxy(proxyURL)
|
||||||
opts := defaultOptions
|
opts := config.NewOptions()
|
||||||
opts.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
|
opts.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
|
||||||
route, err := policy.FromConfig([]byte(`[{"from":"corp.example.com","to":"example.com","timeout":"1s"}]`))
|
route, err := policy.FromConfig([]byte(`[{"from":"corp.example.com","to":"example.com","timeout":"1s"}]`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -112,22 +79,23 @@ func TestNewReverseProxyHandler(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOptions() *Options {
|
func testOptions() *config.Options {
|
||||||
authenticateService, _ := url.Parse("https://authenticate.corp.beyondperimeter.com")
|
authenticateService, _ := url.Parse("https://authenticate.corp.beyondperimeter.com")
|
||||||
authorizeService, _ := url.Parse("https://authorize.corp.beyondperimeter.com")
|
authorizeService, _ := url.Parse("https://authorize.corp.beyondperimeter.com")
|
||||||
configBlob := `[{"from":"corp.example.com","to":"example.com"}]` //valid yaml
|
configBlob := `[{"from":"corp.example.notatld","to":"example.notatld"}]` //valid yaml
|
||||||
policy := base64.URLEncoding.EncodeToString([]byte(configBlob))
|
policy := base64.URLEncoding.EncodeToString([]byte(configBlob))
|
||||||
return &Options{
|
|
||||||
Policy: policy,
|
opts := config.NewOptions()
|
||||||
AuthenticateURL: authenticateService,
|
opts.Policy = policy
|
||||||
AuthorizeURL: authorizeService,
|
opts.AuthenticateURL = authenticateService
|
||||||
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
|
opts.AuthorizeURL = authorizeService
|
||||||
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
|
opts.SharedKey = "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="
|
||||||
CookieName: "pomerium",
|
opts.CookieSecret = "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw="
|
||||||
}
|
opts.ProxyCookieName = "pomerium"
|
||||||
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOptionsWithCORS() *Options {
|
func testOptionsWithCORS() *config.Options {
|
||||||
configBlob := `[{"from":"corp.example.com","to":"example.com","cors_allow_preflight":true}]` //valid yaml
|
configBlob := `[{"from":"corp.example.com","to":"example.com","cors_allow_preflight":true}]` //valid yaml
|
||||||
opts := testOptions()
|
opts := testOptions()
|
||||||
opts.Policy = base64.URLEncoding.EncodeToString([]byte(configBlob))
|
opts.Policy = base64.URLEncoding.EncodeToString([]byte(configBlob))
|
||||||
|
@ -168,11 +136,11 @@ func TestOptions_Validate(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
o *Options
|
o *config.Options
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"good - minimum options", good, false},
|
{"good - minimum options", good, false},
|
||||||
{"nil options", &Options{}, true},
|
{"nil options", &config.Options{}, true},
|
||||||
{"from route", badFromRoute, true},
|
{"from route", badFromRoute, true},
|
||||||
{"to route", badToRoute, true},
|
{"to route", badToRoute, true},
|
||||||
{"authenticate service url", badAuthURL, true},
|
{"authenticate service url", badAuthURL, true},
|
||||||
|
@ -191,7 +159,7 @@ func TestOptions_Validate(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
o := tt.o
|
o := tt.o
|
||||||
if err := o.Validate(); (err != nil) != tt.wantErr {
|
if err := ValidateOptions(o); (err != nil) != tt.wantErr {
|
||||||
t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -207,13 +175,13 @@ func TestNew(t *testing.T) {
|
||||||
badRoutedProxy.SigningKey = "YmFkIGtleQo="
|
badRoutedProxy.SigningKey = "YmFkIGtleQo="
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
opts *Options
|
opts *config.Options
|
||||||
wantProxy bool
|
wantProxy bool
|
||||||
numRoutes int
|
numRoutes int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"good", good, true, 1, false},
|
{"good", good, true, 1, false},
|
||||||
{"empty options", &Options{}, false, 0, true},
|
{"empty options", &config.Options{}, false, 0, true},
|
||||||
{"nil options", nil, false, 0, true},
|
{"nil options", nil, false, 0, true},
|
||||||
{"short secret/validate sanity check", shortCookieLength, false, 0, true},
|
{"short secret/validate sanity check", shortCookieLength, false, 0, true},
|
||||||
{"invalid ec key, valid base64 though", badRoutedProxy, false, 0, true},
|
{"invalid ec key, valid base64 though", badRoutedProxy, false, 0, true},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue