authorize: add authorization (#59)

* authorize: authorization module adds support for per-route access policy. In this release we support the most common forms of identity based access policy: `allowed_users`, `allowed_groups`, and `allowed_domains`. In future versions, the authorization module will also support context and device based authorization policy and decisions. See website documentation for more details.
 * docs: updated `env.example` to include a `POLICY` setting example.
 * docs:  added `IDP_SERVICE_ACCOUNT` to  `env.example` .
 * docs: removed `PROXY_ROOT_DOMAIN` settings which has been replaced by `POLICY`.
 * all: removed `ALLOWED_DOMAINS` settings which has been replaced by `POLICY`. Authorization is now handled by the authorization service and is defined in the policy configuration files.
 * proxy: `ROUTES` settings which has been replaced by `POLICY`.
* internal/log: `http.Server` and `httputil.NewSingleHostReverseProxy` now uses pomerium's logging package instead of the standard library's built in one.

Closes #54
Closes #41
Closes #61
Closes #58
This commit is contained in:
Bobby DeSimone 2019-03-07 12:47:07 -08:00 committed by GitHub
parent 1187be2bf3
commit c13459bb88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1683 additions and 879 deletions

26
.codecov.yml Normal file
View file

@ -0,0 +1,26 @@
codecov:
notify:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project: yes
patch: no
changes: no
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "header, diff"
behavior: default
require_changes: no

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
.docker-compose.yml
.*.yml
pem
env
coverage.txt

View file

@ -2,8 +2,9 @@
## Unreleased
FEATURES:
**FEATURES:**
* **Authorization** : The authorization module adds support for per-route access policy. In this release we support the most common forms of identity based access policy: `allowed_users`, `allowed_groups`, and `allowed_domains`. In future versions, the authorization module will also support context and device based authorization policy and decisions. See website documentation for more details.
* **Group Support** : The authenticate service now retrieves a user's group membership information during authentication and refresh. This change may require additional identity provider configuration; all of which are described in the [updated docs](https://www.pomerium.io/docs/identity-providers.html). A brief summary of the requirements for each IdP are as follows:
- Google requires the [Admin SDK](https://developers.google.com/admin-sdk/directory/) to enabled, a service account with properly delegated access, and `IDP_SERVICE_ACCOUNT` to be set to the base64 encoded value of the service account's key file.
- Okta requires a `groups` claim to be added to both the `id_token` and `access_token`. No additional API calls are made.
@ -11,25 +12,22 @@ FEATURES:
- Onelogin requires the [groups](https://developers.onelogin.com/openid-connect/scopes) was supplied during authentication and that groups parameter has been mapped. Group membership is validated on refresh with the [user-info api endpoint](https://developers.onelogin.com/openid-connect/api/user-info).
* **WebSocket Support** : With [Go 1.12](https://golang.org/doc/go1.12#net/http/httputil) pomerium automatically proxies WebSocket requests.
**CHANGED**:
CHANGED:
* Updated `env.example` to include a `POLICY` setting example.
* Added `IDP_SERVICE_ACCOUNT` to `env.example` .
* Removed `PROXY_ROOT_DOMAIN` settings which has been replaced by `POLICY`.
* Removed `ALLOWED_DOMAINS` settings which has been replaced by `POLICY`. Authorization is now handled by the authorization service and is defined in the policy configuration files.
* Removed `ROUTES` settings which has been replaced by `POLICY`.
* Add refresh endpoint `${url}/.pomerium/refresh` which forces a token refresh and responds with the json result.
* Group membership added to proxy headers (`x-pomerium-authenticated-user-groups`) and (`x-pomerium-jwt-assertion`).
* Default Cookie lifetime (`COOKIE_EXPIRE`) changed from 7 days to 14 hours ~ roughly one business day.
* Moved identity (`authenticate/providers`) into it's own internal identity package as third party identity providers are going to authorization details (group membership, user role, etc) in addition to just authentication attributes.
* Removed circuit breaker package. Calls that were previously wrapped with a circuit breaker fall under gRPC timeouts; which are gated by relatively short deadlines.
* Moved identity (`authenticate/providers`) into its own internal identity package as third party identity providers are going to authorization details (group membership, user role, etc) in addition to just authentication attributes.
* Removed circuit breaker package. Calls that were previously wrapped with a circuit breaker fall under gRPC timeouts; which are gated by relatively short timeouts.
* Session expiration times are truncated at the second.
* **Removed gitlab provider**. We can't support groups until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed.
* Request context is now maintained throughout request-flow via the [context package](https://golang.org/pkg/context/) enabling timeouts, request tracing, and cancellation.
IMPROVED:
**FIXED:**
* Request context is now maintained throughout request-flow via the [context package](https://golang.org/pkg/context/) enabling deadlines, request tracing, and cancellation.
FIXED:
*
SECURITY:
*
* `http.Server` and `httputil.NewSingleHostReverseProxy` now uses pomerium's logging package instead of the standard library's built in one. [GH-58]

View file

@ -32,10 +32,6 @@ type Options struct {
// RedirectURL specifies the callback url following third party authentication
RedirectURL *url.URL `envconfig:"REDIRECT_URL"`
// Coarse authorization based on user email domain
// todo(bdd) : to be replaced with authorization module
AllowedDomains []string `envconfig:"ALLOWED_DOMAINS"`
ProxyRootDomains []string `envconfig:"PROXY_ROOT_DOMAIN"`
// Session/Cookie management
@ -84,9 +80,6 @@ func (o *Options) Validate() error {
if o.ClientSecret == "" {
return errors.New("missing setting: client secret")
}
if len(o.AllowedDomains) == 0 {
return errors.New("missing setting email domain")
}
if len(o.ProxyRootDomains) == 0 {
return errors.New("missing setting: proxy root domain")
}
@ -106,13 +99,9 @@ func (o *Options) Validate() error {
// Authenticate validates a user's identity
type Authenticate struct {
SharedKey string
RedirectURL *url.URL
AllowedDomains []string
ProxyRootDomains []string
Validator func(string) bool
templates *template.Template
csrfStore sessions.CSRFStore
sessionStore sessions.SessionStore
@ -122,9 +111,9 @@ type Authenticate struct {
}
// New validates and creates a new authenticate service from a set of Options
func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate, error) {
func New(opts *Options) (*Authenticate, error) {
if opts == nil {
return nil, errors.New("options cannot be nil")
return nil, errors.New("authenticate: options cannot be nil")
}
if err := opts.Validate(); err != nil {
return nil, err
@ -166,7 +155,6 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
p := &Authenticate{
SharedKey: opts.SharedKey,
RedirectURL: opts.RedirectURL,
AllowedDomains: opts.AllowedDomains,
ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains),
templates: templates.New(),
@ -176,14 +164,6 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate
provider: provider,
}
// validation via dependency injected function
for _, optFunc := range optionFuncs {
err := optFunc(p)
if err != nil {
return nil, err
}
}
return p, nil
}

View file

@ -12,7 +12,6 @@ func testOptions() *Options {
redirectURL, _ := url.Parse("https://example.com/oauth2/callback")
return &Options{
ProxyRootDomains: []string{"example.com"},
AllowedDomains: []string{"example.com"},
RedirectURL: redirectURL,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
ClientID: "test-client-id",
@ -35,8 +34,6 @@ func TestOptions_Validate(t *testing.T) {
emptyClientID.ClientID = ""
emptyClientSecret := testOptions()
emptyClientSecret.ClientSecret = ""
allowedDomains := testOptions()
allowedDomains.AllowedDomains = nil
proxyRootDomains := testOptions()
proxyRootDomains.ProxyRootDomains = nil
emptyCookieSecret := testOptions()
@ -63,7 +60,6 @@ func TestOptions_Validate(t *testing.T) {
{"no shared secret", badSharedKey, true},
{"no client id", emptyClientID, true},
{"no client secret", emptyClientSecret, true},
{"empty allowed domains", allowedDomains, true},
{"empty root domains", proxyRootDomains, true},
}
for _, tt := range tests {

View file

@ -1,3 +1,5 @@
//go:generate protoc -I ../proto/authenticate --go_out=plugins=grpc:../proto/authenticate ../proto/authenticate/authenticate.proto
package authenticate // import "github.com/pomerium/pomerium/authenticate"
import (
"context"
@ -20,7 +22,7 @@ func (p *Authenticate) Authenticate(ctx context.Context, in *pb.AuthenticateRequ
return newSessionProto, nil
}
// Validate locally validates a JWT id token; does NOT do nonce or revokation validation.
// Validate locally validates a JWT id_token; does NOT do nonce or revokation validation.
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (p *Authenticate) Validate(ctx context.Context, in *pb.ValidateRequest) (*pb.ValidateReply, error) {
isValid, err := p.provider.Validate(ctx, in.IdToken)

View file

@ -78,9 +78,6 @@ func TestAuthenticate_Refresh(t *testing.T) {
false},
{"test error", &identity.MockProvider{RefreshError: errors.New("hi")}, &pb.Session{RefreshToken: "refresh token", RefreshDeadline: fixedProtoTime, LifetimeDeadline: fixedProtoTime}, nil, true},
{"test catch nil", nil, nil, nil, true},
// {"test error", "error", nil, true},
// {"test bad time", "bad time", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -81,7 +81,6 @@ func (a *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
return nil, err
}
// check if session refresh period is up
if session.RefreshPeriodExpired() {
newSession, err := a.provider.Refresh(r.Context(), session)
if err != nil {
@ -111,12 +110,6 @@ func (a *Authenticate) authenticate(w http.ResponseWriter, r *http.Request) (*se
}
}
// authenticate really should not be in the business of authorization
// todo(bdd) : remove when authorization module added
if !a.Validator(session.Email) {
log.FromRequest(r).Error().Msg("invalid email user")
return nil, httputil.ErrUserNotAuthorized
}
return session, nil
}
@ -238,12 +231,7 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) {
// OAuthStart starts the authenticate process by redirecting to the identity provider.
// https://tools.ietf.org/html/rfc6749#section-4.2.1
func (a *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) {
authRedirectURL, err := url.Parse(r.URL.Query().Get("redirect_uri"))
if err != nil {
httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest)
return
}
authRedirectURL = a.RedirectURL.ResolveReference(r.URL)
authRedirectURL := a.RedirectURL.ResolveReference(r.URL)
nonce := fmt.Sprintf("%x", cryptutil.GenerateKey())
a.csrfStore.SetCSRF(w, r, nonce)
@ -345,11 +333,6 @@ func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "Invalid Redirect URI"}
}
// Set cookie, or deny: validates the session email and group
if !a.Validator(session.Email) {
log.FromRequest(r).Error().Err(err).Str("email", session.Email).Msg("invalid email permissions denied")
return "", httputil.HTTPError{Code: http.StatusForbidden, Message: "You don't have access"}
}
err = a.sessionStore.SaveSession(w, r, session)
if err != nil {
log.Error().Err(err).Msg("internal error")

View file

@ -17,15 +17,10 @@ import (
"github.com/pomerium/pomerium/internal/templates"
)
// mocks for validator func
func trueValidator(s string) bool { return true }
func falseValidator(s string) bool { return false }
func testAuthenticate() *Authenticate {
var auth Authenticate
auth.RedirectURL, _ = url.Parse("https://auth.example.com/oauth/callback")
auth.SharedKey = "IzY7MOZwzfOkmELXgozHDKTxoT3nOYhwkcmUVINsRww="
auth.AllowedDomains = []string{"*"}
auth.ProxyRootDomains = []string{"example.com"}
auth.templates = templates.New()
return &auth
@ -89,63 +84,22 @@ func TestAuthenticate_authenticate(t *testing.T) {
name string
session sessions.SessionStore
provider identity.MockProvider
validator func(string) bool
want *sessions.SessionState
wantErr bool
}{
{"good", goodSession, identity.MockProvider{ValidateResponse: true}, trueValidator, nil, false},
{"good but fails validation", goodSession, identity.MockProvider{ValidateResponse: true}, falseValidator, nil, true},
{"can't load session", &sessions.MockSessionStore{LoadError: errors.New("error")}, identity.MockProvider{ValidateResponse: true}, trueValidator, nil, true},
{"validation fails", goodSession, identity.MockProvider{ValidateResponse: false}, trueValidator, nil, true},
{"session fails after good validation", &sessions.MockSessionStore{
SaveError: errors.New("error"),
Session: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
}}, identity.MockProvider{ValidateResponse: true},
trueValidator, nil, true},
{"refresh expired",
expiredRefresPeriod,
identity.MockProvider{
ValidateResponse: true,
RefreshResponse: &sessions.SessionState{
AccessToken: "new token",
LifetimeDeadline: time.Now(),
},
},
trueValidator, nil, false},
{"refresh expired refresh error",
expiredRefresPeriod,
identity.MockProvider{
ValidateResponse: true,
RefreshError: errors.New("error"),
},
trueValidator, nil, true},
{"refresh expired failed save",
&sessions.MockSessionStore{
SaveError: errors.New("error"),
Session: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * -time.Second),
}},
identity.MockProvider{
ValidateResponse: true,
RefreshResponse: &sessions.SessionState{
AccessToken: "new token",
LifetimeDeadline: time.Now(),
},
},
trueValidator, nil, true},
{"good", goodSession, identity.MockProvider{ValidateResponse: true}, nil, false},
{"can't load session", &sessions.MockSessionStore{LoadError: errors.New("error")}, identity.MockProvider{ValidateResponse: true}, nil, true},
{"validation fails", goodSession, identity.MockProvider{ValidateResponse: false}, nil, true},
{"session fails after good validation", &sessions.MockSessionStore{SaveError: errors.New("error"), Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * time.Second)}}, identity.MockProvider{ValidateResponse: true}, nil, true},
{"refresh expired", expiredRefresPeriod, identity.MockProvider{ValidateResponse: true, RefreshResponse: &sessions.SessionState{AccessToken: "new token", LifetimeDeadline: time.Now()}}, nil, false},
{"refresh expired refresh error", expiredRefresPeriod, identity.MockProvider{ValidateResponse: true, RefreshError: errors.New("error")}, nil, true},
{"refresh expired failed save", &sessions.MockSessionStore{SaveError: errors.New("error"), Session: &sessions.SessionState{AccessToken: "AccessToken", RefreshToken: "RefreshToken", RefreshDeadline: time.Now().Add(10 * -time.Second)}}, identity.MockProvider{ValidateResponse: true, RefreshResponse: &sessions.SessionState{AccessToken: "new token", LifetimeDeadline: time.Now()}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Authenticate{
sessionStore: tt.session,
provider: tt.provider,
Validator: tt.validator,
}
r := httptest.NewRequest("GET", "/auth", nil)
w := httptest.NewRecorder()
@ -164,7 +118,6 @@ func TestAuthenticate_SignIn(t *testing.T) {
name string
session sessions.SessionStore
provider identity.MockProvider
validator func(string) bool
wantCode int
}{
{"good",
@ -175,7 +128,7 @@ func TestAuthenticate_SignIn(t *testing.T) {
RefreshDeadline: time.Now().Add(10 * time.Second),
}},
identity.MockProvider{ValidateResponse: true},
trueValidator,
http.StatusForbidden},
{"session fails after good validation", &sessions.MockSessionStore{
SaveError: errors.New("error"),
@ -184,14 +137,13 @@ func TestAuthenticate_SignIn(t *testing.T) {
RefreshToken: "RefreshToken",
RefreshDeadline: time.Now().Add(10 * time.Second),
}}, identity.MockProvider{ValidateResponse: true},
trueValidator, http.StatusBadRequest},
http.StatusBadRequest},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Authenticate{
sessionStore: tt.session,
provider: tt.provider,
Validator: tt.validator,
RedirectURL: uriParse("http://www.pomerium.io"),
csrfStore: &sessions.MockCSRFStore{},
SharedKey: "secret",
@ -592,8 +544,6 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
code string
state string
validDomains []string
validator func(string) bool
session sessions.SessionStore
provider identity.MockProvider
csrfStore sessions.MockCSRFStore
@ -607,7 +557,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
@ -629,7 +579,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
@ -652,7 +602,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
@ -674,7 +624,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
identity.MockProvider{
AuthenticateError: errors.New("error"),
@ -691,30 +641,8 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{SaveError: errors.New("error")},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
Email: "blah@blah.com",
RefreshDeadline: time.Now().Add(10 * time.Second),
}},
sessions.MockCSRFStore{
ResponseCSRF: "csrf",
Cookie: &http.Cookie{Value: "nonce"}},
"",
true,
},
{"failed email validation",
http.MethodGet,
"",
"code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"},
falseValidator,
&sessions.MockSessionStore{},
&sessions.MockSessionStore{SaveError: errors.New("error")},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
AccessToken: "AccessToken",
@ -736,7 +664,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
@ -758,7 +686,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"",
base64.URLEncoding.EncodeToString([]byte("nonce:https://corp.pomerium.io")),
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
@ -780,7 +708,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code",
"nonce:https://corp.pomerium.io",
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
@ -802,7 +730,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code",
base64.URLEncoding.EncodeToString([]byte("nonce")),
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
@ -824,7 +752,7 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
"code",
base64.URLEncoding.EncodeToString([]byte("nonce:corp.pomerium.io")),
[]string{"pomerium.io"},
trueValidator,
&sessions.MockSessionStore{},
identity.MockProvider{
AuthenticateResponse: sessions.SessionState{
@ -848,7 +776,6 @@ func TestAuthenticate_getOAuthCallback(t *testing.T) {
csrfStore: tt.csrfStore,
provider: tt.provider,
ProxyRootDomains: tt.validDomains,
Validator: tt.validator,
}
u, _ := url.Parse("/oauthGet")
params, _ := url.ParseQuery(u.RawQuery)

107
authorize/authorize.go Normal file
View file

@ -0,0 +1,107 @@
package authorize // import "github.com/pomerium/pomerium/authorize"
import (
"encoding/base64"
"errors"
"fmt"
"github.com/pomerium/envconfig"
"github.com/pomerium/pomerium/internal/policy"
)
// Options contains configuration settings for the authorize service.
type Options struct {
// SharedKey is used to validate requests between services
SharedKey string `envconfig:"SHARED_SECRET" required:"true"`
// Policy is a base64 encoded yaml blob which enumerates
// per-route access control policies.
Policy string `envconfig:"POLICY"`
PolicyFile string `envconfig:"POLICY_FILE"`
}
// OptionsFromEnvConfig creates an authorize service options from environmental
// variables.
func OptionsFromEnvConfig() (*Options, error) {
o := new(Options)
if err := envconfig.Process("", o); err != nil {
return nil, err
}
return o, nil
}
// Validate checks to see if configuration values are valid for the
// authorize service. Returns first error, if found.
func (o *Options) Validate() error {
decoded, err := base64.StdEncoding.DecodeString(o.SharedKey)
if err != nil {
return fmt.Errorf("authorize: `SHARED_SECRET` setting is invalid base64: %v", err)
}
if len(decoded) != 32 {
return fmt.Errorf("authorize: `SHARED_SECRET` want 32 but got %d bytes", len(decoded))
}
if o.Policy == "" && o.PolicyFile == "" {
return errors.New("authorize: either `POLICY` or `POLICY_FILE` must be non-nil")
}
if o.Policy != "" {
confBytes, err := base64.StdEncoding.DecodeString(o.Policy)
if err != nil {
return fmt.Errorf("authorize: `POLICY` is invalid base64 %v", err)
}
_, err = policy.FromConfig(confBytes)
if err != nil {
return fmt.Errorf("authorize: `POLICY` %v", err)
}
}
if o.PolicyFile != "" {
_, err = policy.FromConfigFile(o.PolicyFile)
if err != nil {
return fmt.Errorf("authorize: `POLICY_FILE` %v", err)
}
}
return nil
}
// Authorize struct holds
type Authorize struct {
SharedKey string
identityAccess IdentityValidator
// contextValidator
// deviceValidator
}
// New validates and creates a new Authorize service from a set of Options
func New(opts *Options) (*Authorize, error) {
if opts == nil {
return nil, errors.New("authorize: options cannot be nil")
}
if err := opts.Validate(); err != nil {
return nil, err
}
// errors handled by validate
sharedKey, _ := base64.StdEncoding.DecodeString(opts.SharedKey)
var policies []policy.Policy
if opts.Policy != "" {
confBytes, _ := base64.StdEncoding.DecodeString(opts.Policy)
policies, _ = policy.FromConfig(confBytes)
} else {
policies, _ = policy.FromConfigFile(opts.PolicyFile)
}
return &Authorize{
SharedKey: string(sharedKey),
identityAccess: NewIdentityWhitelist(policies),
}, nil
}
// ValidIdentity returns if an identity is authorized to access a route resource.
func (a *Authorize) ValidIdentity(route string, identity *Identity) bool {
return a.identityAccess.Valid(route, identity)
}
// NewIdentityWhitelist returns an indentity validator.
// todo(bdd) : a radix-tree implementation is probably more efficient
func NewIdentityWhitelist(policies []policy.Policy) IdentityValidator {
return newIdentityWhitelistMap(policies)
}

View file

@ -0,0 +1,93 @@
package authorize
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
)
func TestOptionsFromEnvConfig(t *testing.T) {
t.Parallel()
os.Clearenv()
tests := []struct {
name string
want *Options
envKey string
envValue string
wantErr bool
}{
{"shared secret missing", nil, "", "", true},
{"with secret", &Options{SharedKey: "aGkK"}, "SHARED_SECRET", "aGkK", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envKey != "" {
os.Setenv(tt.envKey, tt.envValue)
defer os.Unsetenv(tt.envKey)
}
got, err := OptionsFromEnvConfig()
if (err != nil) != tt.wantErr {
t.Errorf("OptionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("OptionsFromEnvConfig() = %v, want %v", got, tt.want)
}
})
}
}
func TestNew(t *testing.T) {
t.Parallel()
content := []byte(`[{"from": "pomerium.io","to":"httpbin.org"}]`)
tmpfile, err := ioutil.TempFile("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
log.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
tests := []struct {
name string
SharedKey string
Policy string
PolicyFile string
wantErr bool
}{
{"good", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ==", "", false},
{"bad shared secret", "AZA85podM73CjLCjViDNz1EUvvejKpWp7Hysr0knXA==", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ==", "", true},
{"really bad shared secret", "sup", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ==", "", true},
{"bad base64 policy", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ^=", "", true},
{"bad json", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "e30=", "", true},
{"no policies", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "", "", true},
{"good policy file", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "", "./testdata/basic.json", true},
{"bad policy file, directory", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "", "./testdata/", true},
{"good policy", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "WwogIHsKICAgICJyb3V0ZXMiOiAiaHR0cDovL3BvbWVyaXVtLmlvIgogIH0KXQ==", "", false},
{"good file", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", "", tmpfile.Name(), false},
{"validation error, short secret", "AZA85podM73CjLCjViDNz1EUvvejKpWp7Hysr0knXA==", "", "", true},
{"nil options", "", "", "", true}, // special case
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &Options{SharedKey: tt.SharedKey, Policy: tt.Policy, PolicyFile: tt.PolicyFile}
if tt.name == "nil options" {
o = nil
}
_, err := New(o)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
// if !reflect.DeepEqual(got, tt.want) {
// t.Errorf("New() = %v, want %v", got, tt.want)
// }
})
}
}

18
authorize/gprc.go Normal file
View file

@ -0,0 +1,18 @@
//go:generate protoc -I ../proto/authorize --go_out=plugins=grpc:../proto/authorize ../proto/authorize/authorize.proto
package authorize // import "github.com/pomerium/pomerium/authorize"
import (
"context"
pb "github.com/pomerium/pomerium/proto/authorize"
"github.com/pomerium/pomerium/internal/log"
)
// Authorize validates the user identity, device, and context of a request for
// a given route. Currently only checks identity.
func (a *Authorize) Authorize(ctx context.Context, in *pb.AuthorizeRequest) (*pb.AuthorizeReply, error) {
ok := a.ValidIdentity(in.Route, &Identity{in.User, in.Email, in.Groups})
log.Debug().Str("route", in.Route).Strs("groups", in.Groups).Str("email", in.Email).Bool("Valid?", ok).Msg("authorize/grpc")
return &pb.AuthorizeReply{IsValid: ok}, nil
}

39
authorize/gprc_test.go Normal file
View file

@ -0,0 +1,39 @@
//go:generate protoc -I ../proto/authorize --go_out=plugins=grpc:../proto/authorize ../proto/authorize/authorize.proto
package authorize
import (
"context"
"reflect"
"testing"
pb "github.com/pomerium/pomerium/proto/authorize"
)
func TestAuthorize_Authorize(t *testing.T) {
t.Parallel()
tests := []struct {
name string
SharedKey string
identityAccess IdentityValidator
in *pb.AuthorizeRequest
want *pb.AuthorizeReply
wantErr bool
}{
{"valid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: true}, &pb.AuthorizeRequest{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: true}, false},
{"invalid authorization request", "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=", &MockIdentityValidator{ValidResponse: false}, &pb.AuthorizeRequest{Route: "http://pomerium.io", User: "user@pomerium.io"}, &pb.AuthorizeReply{IsValid: false}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Authorize{SharedKey: tt.SharedKey, identityAccess: tt.identityAccess}
got, err := a.Authorize(context.Background(), tt.in)
if (err != nil) != tt.wantErr {
t.Errorf("Authorize.Authorize() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Authorize.Authorize() = %v, want %v", got, tt.want)
}
})
}
}

127
authorize/identity.go Normal file
View file

@ -0,0 +1,127 @@
package authorize // import "github.com/pomerium/pomerium/authorize"
import (
"fmt"
"strings"
"sync"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/policy"
)
// Identity contains a user's identity information.
type Identity struct {
User string
Email string
Groups []string
}
// EmailDomain returns the domain of the identity's email.
func (i *Identity) EmailDomain() string {
if i.Email == "" {
return ""
}
comp := strings.Split(i.Email, "@")
if len(comp) != 2 || comp[0] == "" {
return ""
}
return comp[1]
}
// IdentityValidator provides an interface to check whether a user has access
// to a given route.
type IdentityValidator interface {
Valid(string, *Identity) bool
}
type identityWhitelist struct {
sync.RWMutex
m map[string]bool
}
// 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) *identityWhitelist {
var im identityWhitelist
im.m = make(map[string]bool, len(policies)*3)
for _, p := range policies {
for _, group := range p.AllowedGroups {
log.Debug().Str("route", p.From).Str("group", group).Msg("add group")
im.PutGroup(p.From, group)
}
for _, domain := range p.AllowedDomains {
im.PutDomain(p.From, domain)
log.Debug().Str("route", p.From).Str("group", domain).Msg("add domain")
}
for _, email := range p.AllowedEmails {
im.PutEmail(p.From, email)
log.Debug().Str("route", p.From).Str("group", email).Msg("add email")
}
}
return &im
}
// Valid reports whether an identity has valid access for a given route.
func (m *identityWhitelist) Valid(route string, i *Identity) bool {
if ok := m.Domain(route, i.EmailDomain()); ok {
return ok
}
if ok := m.Email(route, i.Email); ok {
return ok
}
for _, group := range i.Groups {
if ok := m.Group(route, group); ok {
return ok
}
}
return false
}
// Group retrieves per-route access given a group name.
func (m *identityWhitelist) Group(route, group string) bool {
m.RLock()
defer m.RUnlock()
return m.m[fmt.Sprintf("%s|group:%s", route, group)]
}
// PutGroup adds an access entry for a route given a group name.
func (m *identityWhitelist) PutGroup(route, group string) {
m.Lock()
m.m[fmt.Sprintf("%s|group:%s", route, group)] = true
m.Unlock()
}
// Domain retrieves per-route access given a domain name.
func (m *identityWhitelist) Domain(route, domain string) bool {
m.RLock()
defer m.RUnlock()
return m.m[fmt.Sprintf("%s|domain:%s", route, domain)]
}
// PutDomain adds an access entry for a route given a domain name.
func (m *identityWhitelist) PutDomain(route, domain string) {
m.Lock()
m.m[fmt.Sprintf("%s|domain:%s", route, domain)] = true
m.Unlock()
}
// Email retrieves per-route access given a user's email.
func (m *identityWhitelist) Email(route, email string) bool {
m.RLock()
defer m.RUnlock()
return m.m[fmt.Sprintf("%s|email:%s", route, email)]
}
// PutEmail adds an access entry for a route given a user's email.
func (m *identityWhitelist) PutEmail(route, email string) {
m.Lock()
m.m[fmt.Sprintf("%s|email:%s", route, email)] = true
m.Unlock()
}
// MockIdentityValidator is a mock implementation of IdentityValidator
type MockIdentityValidator struct{ ValidResponse bool }
// Valid is a mock implementation IdentityValidator's Valid method
func (mv *MockIdentityValidator) Valid(u string, i *Identity) bool { return mv.ValidResponse }

View file

@ -0,0 +1,62 @@
package authorize
import (
"testing"
"github.com/pomerium/pomerium/internal/policy"
)
func TestIdentity_EmailDomain(t *testing.T) {
t.Parallel()
tests := []struct {
name string
Email string
want string
}{
{"simple", "user@pomerium.io", "pomerium.io"},
{"period malformed", "user@.io", ".io"},
{"empty", "", ""},
{"empty first part", "@uhoh.com", ""},
{"empty second part", "uhoh@", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
i := &Identity{Email: tt.Email}
if got := i.EmailDomain(); got != tt.want {
t.Errorf("Identity.EmailDomain() = %v, want %v", got, tt.want)
}
})
}
}
func Test_IdentityWhitelistMap(t *testing.T) {
t.Parallel()
tests := []struct {
name string
policies []policy.Policy
route string
Identity *Identity
want bool
}{
{"valid domain", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, true},
{"invalid domain prepend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "a@1example.com"}, false},
{"invalid domain postpend", []policy.Policy{{From: "example.com", AllowedDomains: []string{"example.com"}}}, "example.com", &Identity{Email: "user@example.com2"}, false},
{"valid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"admin"}}, true},
{"invalid group", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone"}}, false},
{"invalid empty", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{""}}, false},
{"valid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyone", "admin"}}, true},
{"invalid group multiple", []policy.Policy{{From: "example.com", AllowedGroups: []string{"admin"}}}, "example.com", &Identity{Email: "user@example.com", Groups: []string{"everyones", "sadmin"}}, false},
{"valid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user@example.com"}, true},
{"invalid user email", []policy.Policy{{From: "example.com", AllowedEmails: []string{"user@example.com"}}}, "example.com", &Identity{Email: "user2@example.com"}, false},
{"empty everything", []policy.Policy{{From: "example.com"}}, "example.com", &Identity{Email: "user2@example.com"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wl := NewIdentityWhitelist(tt.policies)
if got := wl.Valid(tt.route, tt.Identity); got != tt.want {
t.Errorf("IdentityACLMap.Allowed() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -9,12 +9,13 @@ import (
"google.golang.org/grpc"
"github.com/pomerium/pomerium/authenticate"
"github.com/pomerium/pomerium/authorize"
"github.com/pomerium/pomerium/internal/https"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/options"
"github.com/pomerium/pomerium/internal/version"
pb "github.com/pomerium/pomerium/proto/authenticate"
pbAuthenticate "github.com/pomerium/pomerium/proto/authenticate"
pbAuthorize "github.com/pomerium/pomerium/proto/authorize"
"github.com/pomerium/pomerium/proxy"
)
@ -36,7 +37,7 @@ func main() {
fmt.Printf("%s", version.FullVersion())
os.Exit(0)
}
log.Info().Str("version", version.FullVersion()).Str("service-mode", mainOpts.Services).Msg("cmd/pomerium")
log.Info().Str("version", version.FullVersion()).Str("service", mainOpts.Services).Msg("cmd/pomerium")
grpcAuth := middleware.NewSharedSecretCred(mainOpts.SharedKey)
grpcOpts := []grpc.ServerOption{grpc.UnaryInterceptor(grpcAuth.ValidateRequest)}
@ -45,22 +46,29 @@ func main() {
var authenticateService *authenticate.Authenticate
var authHost string
if mainOpts.Services == "all" || mainOpts.Services == "authenticate" {
authOpts, err := authenticate.OptionsFromEnvConfig()
opts, err := authenticate.OptionsFromEnvConfig()
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: authenticate settings")
}
emailValidator := func(p *authenticate.Authenticate) error {
p.Validator = options.NewEmailValidator(authOpts.AllowedDomains)
return nil
}
authenticateService, err = authenticate.New(authOpts, emailValidator)
authenticateService, err = authenticate.New(opts)
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate")
}
authHost = authOpts.RedirectURL.Host
pb.RegisterAuthenticatorServer(grpcServer, authenticateService)
authHost = opts.RedirectURL.Host
pbAuthenticate.RegisterAuthenticatorServer(grpcServer, authenticateService)
}
var authorizeService *authorize.Authorize
if mainOpts.Services == "all" || mainOpts.Services == "authorize" {
opts, err := authorize.OptionsFromEnvConfig()
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: authorize settings")
}
authorizeService, err = authorize.New(opts)
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: new authorize")
}
pbAuthorize.RegisterAuthorizerServer(grpcServer, authorizeService)
}
var proxyService *proxy.Proxy
@ -74,7 +82,10 @@ func main() {
if err != nil {
log.Fatal().Err(err).Msg("cmd/pomerium: new proxy")
}
// cleanup our RPC services
defer proxyService.AuthenticateClient.Close()
defer proxyService.AuthorizeClient.Close()
}
topMux := http.NewServeMux()

View file

@ -64,6 +64,7 @@ func isValidService(service string) bool {
case
"all",
"proxy",
"authorize",
"authenticate":
return true
}

View file

@ -55,7 +55,7 @@ func Test_isValidService(t *testing.T) {
{"all", "all", true},
{"authenticate", "authenticate", true},
{"authenticate bad case", "AuThenticate", false},
{"authorize not yet implemented", "authorize", false},
{"authorize implemented", "authorize", true},
{"jiberish", "xd23", false},
}
for _, tt := range tests {

View file

@ -19,7 +19,7 @@ Global settings are configuration variables that are shared by all services.
- Environmental Variable: `SERVICES`
- Type: `string`
- Default: `all`
- Options: `all` `authenticate` or `proxy`
- Options: `all` `authenticate` `authorize` or `proxy`
Service mode sets the pomerium service(s) to run. If testing, you may want to set to `all` and run pomerium in "all-in-one mode." In production, you'll likely want to spin of several instances of each service mode for high availability.
@ -43,6 +43,17 @@ Shared Secret is the base64 encoded 256-bit key used to mutually authenticate re
head -c32 /dev/urandom | base64
```
### Policy
- Environmental Variable: either `POLICY` or `POLICY_FILE`
- Type: [base64 encoded] `string` or relative file location
- Filetype: `json` or `yaml`
- Required
Policy contains the routes, and their access policies. For example,
<<< @/policy.example.yaml
### Debug
- Environmental Variable: `POMERIUM_DEBUG`
@ -51,7 +62,7 @@ head -c32 /dev/urandom | base64
By default, JSON encoded logs are produced. Debug enables colored, human-readable, and more verbose logs to be streamed to [standard out](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). In production, it's recommended to be set to `false`.
For example, if true.
For example, if `true`.
```
10:37AM INF cmd/pomerium version=v0.0.1-dirty+ede4124
@ -60,7 +71,7 @@ For example, if true.
10:37AM INF proxy/authenticator: grpc connection OverrideCertificateName= addr=auth.corp.beyondperimeter.com:443
```
If false:
If `false`:
```
{"level":"info","version":"v0.0.1-dirty+ede4124","time":"2019-02-18T10:41:03-08:00","message":"cmd/pomerium"}
@ -96,21 +107,6 @@ Certificate key is the x509 _private-key_ used to establish secure HTTP and gRPC
Redirect URL is the url the user will be redirected to following authentication with the third-party identity provider (IdP). Note the URL ends with `/oauth2/callback`. This setting will mirror the URL set when configuring your [identity provider]. Typically, on the provider side, this is called an _authorized callback url_.
### Allowed Email Domains
::: warning
This setting will be deprecated with the upcoming release of the authorization service.
:::
- Environmental Variable: `ALLOWED_DOMAINS`
- Type: `[]string` (e.g. comma separated list of strings)
- Required
- Example: `engineering.corp-b.com,devops.corp-a.com`
The allowed email domains settings dictates which email domains are valid for access. If an authenticated user has a email ending in a non-whitelisted domain, the request will be denied as unauthorized.
### Proxy Root Domains
- Environmental Variable: `PROXY_ROOT_DOMAIN`
@ -161,19 +157,18 @@ Provider URL is the base path to an identity provider's [OpenID connect discover
- Default: `oidc`,`profile`, `email`, `offline_access` (typically)
- Optional for built-in identity providers.
Identity provider scopes correspond to access privilege scopes as defined in Section 3.3 of OAuth 2.0 RFC6749. The scopes associated with Access Tokens determine what resources will be available when they are used to access OAuth 2.0 protected endpoints. If you are using a built-in provider, you probably don't want to set customized scopes.
Identity provider scopes correspond to access privilege scopes as defined in Section 3.3 of OAuth 2.0 RFC6749\. The scopes associated with Access Tokens determine what resources will be available when they are used to access OAuth 2.0 protected endpoints. If you are using a built-in provider, you probably don't want to set customized scopes.
### Identity Provider Service Account
- Environmental Variable: `IDP_SERVICE_ACCOUNT`
- Type: `string`
- Required, depending on provider
Identity Provider Service Account is field used to configure any additional user account or access-token that may be required for querying additional user information during authentication. For a concrete example, Google an additional service account and to make a follow-up request to query a user's group membership. For more information, refer to the [identity provider] docs to see if your provider requires this setting.
## Proxy Service
### Routes
- Environmental Variable: `ROUTES`
- Type: `map[string]string` comma separated mapping of managed entities.
- Required
- Example: `https://httpbin.corp.example.com=http://httpbin,https://hello.corp.example.com=http://hello:8080/`
The routes setting contains a mapping of routes to be managed by pomerium.
### Signing Key
- Environmental Variable: `SIGNING_KEY`
@ -198,17 +193,25 @@ Authenticate Service URL is the externally accessible URL for the authenticate s
- Optional
- Example: `pomerium-authenticate-service.pomerium.svc.cluster.local`
Authenticate Internal Service URL is the internal location of the authenticate service. This setting is used to override the authenticate service url for when you need to do "behind-the-ingress" inter-service communication. This is typically required for ingresses and load balancers that do not support HTTP/2 or gRPC termination.
Authenticate Internal Service URL is the internally routed dns name of the authenticate service. This setting is used to override the authenticate service url for when you need to do "behind-the-ingress" inter-service communication. This is typically required for ingresses and load balancers that do not support HTTP/2 or gRPC termination.
### Authenticate Service Port
### Authorize Service URL
- Environmental Variable: `AUTHENTICATE_SERVICE_PORT`
- Type: `int`
- Environmental Variable: `AUTHORIZE_SERVICE_URL`
- Type: `URL`
- Required
- Example: `https://access.corp.example.com`
Authorize Service URL is the externally accessible URL for the authorize service.
### Authorize Internal Service URL
- Environmental Variable: `AUTHORIZE_INTERNAL_URL`
- Type: `string`
- Optional
- Default: `443`
- Example: `8443`
- Example: `pomerium-authorize-service.pomerium.svc.cluster.local`
Authenticate Service Port is used to set the port value for authenticate service communication.
Authorize Internal Service URL is the internally routed dns name of the authorize service. This setting is used to override the authorize service url for when you need to do "behind-the-ingress" inter-service communication. This is typically required for ingresses and load balancers that do not support HTTP/2 or gRPC termination.
### Override Certificate Name

View file

@ -56,20 +56,6 @@ Customize for your identity provider run `docker-compose up -f nginx.docker-comp
<<< @/docs/docs/examples/docker/nginx.docker-compose.yml
### Gitlab On-Prem
- Docker and Docker-Compose based.
- Uses pre-configured built-in nginx load balancer
- Runs separate containers for each service
- Comes with a pre-configured instance of on-prem Gitlab-CE
- Routes default to on-prem [helloworld], [httpbin], and [gitlab].
Customize for your identity provider run `docker-compose up -f gitlab.docker-compose.yml`
#### gitlab.docker-compose.yml
<<< @/docs/docs/examples/docker/gitlab.docker-compose.yml
## Kubernetes
### Google Kubernetes Engine
@ -103,7 +89,6 @@ Customize for your identity provider run `docker-compose up -f gitlab.docker-com
<<< @/docs/docs/examples/kubernetes/ingress.yml
[gitlab]: https://docs.gitlab.com/ee/user/project/container_registry.html
[helloworld]: https://hub.docker.com/r/tutum/hello-world
[httpbin]: https://httpbin.org/
[https load balancing]: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress

View file

@ -1,43 +1,35 @@
# Example Pomerium configuration.
#
# NOTE! Change IDP_* settings to match your identity provider settings!
# NOTE! Generate new SHARED_SECRET and COOKIE_SECRET keys!
# NOTE! Generate new SHARED_SECRET and COOKIE_SECRET keys! e.g. `head -c32 /dev/urandom | base64`
# NOTE! Replace `corp.beyondperimeter.com` with whatever your domain is
# NOTE! Make sure certificate files (cert.pem/privkey.pem) are in the same directory as this file
# NOTE! Wrap URLs in quotes to avoid parse errors
# NOTE! Make sure your policy file (policy.example.yaml) is in the same directory as this file
version: "3"
services:
pomerium-all:
pomerium:
image: pomerium/pomerium:latest # or `build: .` to build from source
environment:
- POMERIUM_DEBUG=true
- SERVICES=all
# auth settings
- REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback
# Identity Provider Settings (Must be changed!)
- IDP_PROVIDER=google
- IDP_PROVIDER_URL=https://accounts.google.com
- IDP_CLIENT_ID=REPLACE_ME.apps.googleusercontent.com
- IDP_CLIENT_SECRET=REPLACE_ME
# - SCOPE="openid email"
- PROXY_ROOT_DOMAIN=beyondperimeter.com
- ALLOWED_DOMAINS=*
# shared service settings
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# proxy settings
- CERTIFICATE_FILE=cert.pem
- CERTIFICATE_KEY_FILE=privkey.pem
- AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com
- ROUTES=https://httpbin.corp.beyondperimeter.com=http://httpbin,https://helloworld.corp.beyondperimeter.com=http://helloworld:8080/
# - SIGNING_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
# if passing certs as files
# - CERTIFICATE_KEY=corp.beyondperimeter.com.crt
# - CERTIFICATE_KEY_FILE=corp.beyondperimeter.com.key
# Or, you can pass certifcates as bas64 encoded values. e.g. `base64 -i cert.pem`
# - CERTIFICATE=
# - CERTIFICATE_KEY=
volumes: # volumes is optional; used if passing certificates as files
- AUTHORIZE_SERVICE_URL=https://access.corp.beyondperimeter.com
- POLICY_FILE=./policy.yaml
volumes:
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
- ./policy.example.yaml:/pomerium/policy.yaml:ro
ports:
- 443:443
@ -46,9 +38,8 @@ services:
image: kennethreitz/httpbin:latest
expose:
- 80
# https://helloworld.corp.beyondperimeter.com
helloworld:
# https://hello.corp.beyondperimeter.com
hello:
image: gcr.io/google-samples/hello-app:1.0
expose:
- 8080

View file

@ -1,94 +0,0 @@
version: "3"
services:
nginx:
image: pomerium/nginx-proxy:latest
ports:
- "443:443"
volumes:
# NOTE!!! : nginx must be supplied with your wildcard certificates. And it expects
# it in the format of whatever your wildcard domain name is in.
# see : https://github.com/jwilder/nginx-proxy#wildcard-certificates
# So, if your subdomain is corp.beyondperimeter.com, you'd have the following :
- ./cert.pem:/etc/nginx/certs/corp.beyondperimeter.com.crt:ro
- ./privkey.pem:/etc/nginx/certs/corp.beyondperimeter.com.key:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
pomerium-authenticate:
build: .
restart: always
environment:
- POMERIUM_DEBUG=true
- SERVICES=authenticate
# auth settings
- REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback
# Identity Provider Settings (Must be changed!)
- IDP_PROVIDER=google
- IDP_PROVIDER_URL=https://accounts.google.com
- IDP_CLIENT_ID=REPLACEME
- IDP_CLIENT_SECRET=REPLACE_ME
- PROXY_ROOT_DOMAIN=corp.beyondperimeter.com
- ALLOWED_DOMAINS=*
- SKIP_PROVIDER_BUTTON=false
# shared service settings
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=auth.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
pomerium-proxy:
build: .
restart: always
environment:
- POMERIUM_DEBUG=true
- SERVICES=proxy
# proxy settings
- AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com
# IMPORTANT! If you are running pomerium behind another ingress (loadbalancer/firewall/etc)
# you must tell pomerium proxy how to communicate using an internal hostname for RPC
- AUTHENTICATE_INTERNAL_URL=pomerium-authenticate:443
# When communicating internally, rPC is going to get a name conflict expecting an external
# facing certificate name (i.e. authenticate-service.local vs *.corp.example.com).
- OVERRIDE_CERTIFICATE_NAME=*.corp.beyondperimeter.com
- ROUTES=https://gitlab.corp.beyondperimeter.com=https://gitlab
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=*.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
gitlab:
hostname: gitlab.corp.beyondperimeter.com
image: gitlab/gitlab-ce:latest
restart: always
expose:
- 443
- 80
- 22
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'https://gitlab.corp.beyondperimeter.com'
nginx['ssl_certificate'] = '/etc/gitlab/trusted-certs/corp.beyondperimeter.com.crt'
nginx['ssl_certificate_key'] = '/etc/gitlab/trusted-certs/corp.beyondperimeter.com.key'
VIRTUAL_PROTO: https
VIRTUAL_HOST: gitlab.corp.beyondperimeter.com
VIRTUAL_PORT: 443
volumes:
- ./cert.pem:/etc/gitlab/trusted-certs/corp.beyondperimeter.com.crt
- ./privkey.pem:/etc/gitlab/trusted-certs/corp.beyondperimeter.com.key
- $HOME/gitlab/config:/etc/gitlab
- $HOME/gitlab/logs:/var/log/gitlab
- $HOME/gitlab/data:/var/opt/gitlab

View file

@ -15,59 +15,75 @@ services:
- /var/run/docker.sock:/tmp/docker.sock:ro
pomerium-authenticate:
build: .
image: pomerium/pomerium:latest # or `build: .` to build from source
restart: always
environment:
- POMERIUM_DEBUG=true
- SERVICES=authenticate
# auth settings
- REDIRECT_URL=https://auth.corp.beyondperimeter.com/oauth2/callback
# Identity Provider Settings (Must be changed!)
- IDP_PROVIDER=google
- IDP_PROVIDER_URL=https://accounts.google.com
- IDP_CLIENT_ID=REPLACEME
- IDP_CLIENT_ID=REPLACE_ME.apps.googleusercontent.com
- IDP_CLIENT_SECRET=REPLACE_ME
- PROXY_ROOT_DOMAIN=corp.beyondperimeter.com
- ALLOWED_DOMAINS=*
- SKIP_PROVIDER_BUTTON=false
# shared service settings
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=auth.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
volumes:
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
pomerium-proxy:
build: .
restart: always
pomerium-proxy:
image: pomerium/pomerium:latest # or `build: .` to build from source
restart: always
environment:
- POMERIUM_DEBUG=true
- SERVICES=proxy
# proxy settings
- POLICY_FILE=policy.yaml
- AUTHENTICATE_SERVICE_URL=https://auth.corp.beyondperimeter.com
- AUTHORIZE_SERVICE_URL=https://access.corp.beyondperimeter.com
# IMPORTANT! If you are running pomerium behind another ingress (loadbalancer/firewall/etc)
# you must tell pomerium proxy how to communicate using an internal hostname for RPC
- AUTHENTICATE_INTERNAL_URL=pomerium-authenticate:443
- AUTHORIZE_INTERNAL_URL=pomerium-authorize:443
# When communicating internally, rPC is going to get a name conflict expecting an external
# facing certificate name (i.e. authenticate-service.local vs *.corp.example.com).
- OVERRIDE_CERTIFICATE_NAME=*.corp.beyondperimeter.com
- ROUTES=https://httpbin.corp.beyondperimeter.com=http://httpbin,https://hello.corp.beyondperimeter.com=http://hello:8080/
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=*.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
volumes:
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
- ./policy.example.yaml:/pomerium/policy.yaml:ro
expose:
- 443
pomerium-authorize:
image: pomerium/pomerium:latest # or `build: .` to build from source
restart: always
environment:
- POMERIUM_DEBUG=true
- SERVICES=authorize
- POLICY_FILE=policy.yaml
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=access.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes:
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
- ./policy.example.yaml:/pomerium/policy.yaml:ro
expose:
- 443

View file

@ -8,6 +8,7 @@ metadata:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
# to avoid ingress routing, enable
# nginx.ingress.kubernetes.io/ssl-passthrough: "true"
spec:
@ -16,6 +17,8 @@ spec:
hosts:
- "*.corp.beyondperimeter.com"
- "auth.corp.beyondperimeter.com"
- "access.corp.beyondperimeter.com"
rules:
- host: "*.corp.beyondperimeter.com"
http:
@ -32,3 +35,10 @@ spec:
backend:
serviceName: pomerium-authenticate-service
servicePort: https
- host: "access.corp.beyondperimeter.com"
http:
paths:
- paths:
backend:
serviceName: pomerium-authorize-service
servicePort: https

View file

@ -13,6 +13,8 @@ spec:
hosts:
- "*.corp.beyondperimeter.com"
- "auth.corp.beyondperimeter.com"
- "access.corp.beyondperimeter.com"
rules:
- host: "*.corp.beyondperimeter.com"
http:
@ -29,3 +31,10 @@ spec:
backend:
serviceName: pomerium-authenticate-service
servicePort: https
- host: "access.corp.beyondperimeter.com"
http:
paths:
- paths:
backend:
serviceName: pomerium-authorize-service
servicePort: https

View file

@ -23,10 +23,12 @@ spec:
name: https
protocol: TCP
env:
- name: ROUTES
value: https://httpbin.corp.beyondperimeter.com=https://httpbin.org,https://hi.corp.beyondperimeter.com=http://hello-app.pomerium.svc.cluster.local:8080
- name: SERVICES
value: proxy
- name: AUTHORIZE_SERVICE_URL
value: https://access.corp.beyondperimeter.com
- name: AUTHORIZE_INTERNAL_URL
value: "pomerium-authorize-service.pomerium.svc.cluster.local"
- name: AUTHENTICATE_SERVICE_URL
value: https://auth.corp.beyondperimeter.com
- name: AUTHENTICATE_INTERNAL_URL

View file

@ -120,7 +120,7 @@ IDP_CLIENT_SECRET="REPLACE-ME"
:::warning
Support was removed in v0.0.3 because Gitlab does not provide callers with a user email, under any scope, to a caller unless that user has selected her email to be public. Pomerium until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed.
Support was removed in v0.0.3 because Gitlab does not provide callers with a user email, under any scope, to a caller unless that user has selected her email to be public. Pomerium support is blocked until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed.
:::
@ -192,14 +192,16 @@ In order to have Pomerium validate group membership, we'll also need to configur
![Google create service account](./google/google-create-sa.png)
Then, you'll need to manually open an editor add an `impersonate_user` key to the downloaded public/private key file. In this case, we'd be impersonating the admin account `user@pomerium.io`.
Then, you'll need to manually open an editor and add an `impersonate_user` field to the downloaded public/private key file. In this case, we'd be impersonating the admin account `user@pomerium.io`.
::: warning
You MUST add the `impersonate_user` field to your json key file. [Google requires](https://stackoverflow.com/questions/48585700/is-it-possible-to-call-apis-from-service-account-without-acting-on-behalf-of-a-u/48601364#48601364) that service accounts act on behalf of another user.
[Google requires](https://stackoverflow.com/questions/48585700/is-it-possible-to-call-apis-from-service-account-without-acting-on-behalf-of-a-u/48601364#48601364) that service accounts act on behalf of another user. You MUST add the `impersonate_user` field to your json key file.
:::
```json
{
"type": "service_account",

View file

@ -6,7 +6,7 @@ Pomerium is an open-source, identity-aware access proxy.
## Why
Traditional [perimeter](https://www.redbooks.ibm.com/redpapers/pdfs/redp4397.pdf) [security](https://en.wikipedia.org/wiki/Perimeter_Security)has some shortcomings, namely:
Traditional [perimeter](https://www.redbooks.ibm.com/redpapers/pdfs/redp4397.pdf) [security](https://en.wikipedia.org/wiki/Perimeter_Security) has some shortcomings, namely:
- Insider threat is not well addressed and 28% of breaches are [by internal actors](http://www.documentwereld.nl/files/2018/Verizon-DBIR_2018-Main_report.pdf).
- Impenetrable fortress in theory falls in practice; multiple entry points (like VPNs), lots of firewall rules, network segmentation creep.

View file

@ -27,17 +27,19 @@ The command will run all the tests, some code linters, then build the binary. If
## Configure
Make a copy of the [env.example] and name it something like `env`.
### Environmental Configuration Variables
```bash
cp env.example env
```
Create a environmental configuration file modify its configuration to to match your [identity provider] settings. For example, `env`:
Modify your `env` configuration to to match your [identity provider] settings.
<<< @/env.example
```bash
vim env
```
### policy.yaml
Next, create a policy configuration file which will contain the routes you want to proxy, and their desired access-controls. For example, `policy.example.yaml`:
<<< @/policy.example.yaml
### Certificates
Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt].

View file

@ -35,6 +35,8 @@ git clone https://github.com/pomerium/pomerium.git $HOME/pomerium
Edit the the [example kubernetes files][./scripts/kubernetes_gke.sh] to match your [identity provider] settings:
- `./docs/docs/examples/authorize.deploy.yml`
- `./docs/docs/examples/authorize.service.yml`
- `./docs/docs/examples/authenticate.deploy.yml`
- `./docs/docs/examples/authenticate.service.yml`
- `./docs/docs/examples/proxy.deploy.yml`
@ -50,8 +52,8 @@ Edit [./scripts/kubernetes_gke.sh] making sure to change the identity provider s
Run [./scripts/kubernetes_gke.sh] which will:
1. Provision a new cluster
2. Create authenticate and proxy [deployments](https://cloud.google.com/kubernetes-engine/docs/concepts/deployment).
3. Provision and apply authenticate and proxy [services](https://cloud.google.com/kubernetes-engine/docs/concepts/service).
2. Create authenticate, authorize, and proxy [deployments](https://cloud.google.com/kubernetes-engine/docs/concepts/deployment).
3. Provision and apply authenticate, authorize, and proxy [services](https://cloud.google.com/kubernetes-engine/docs/concepts/service).
4. Configure an ingress load balancer.
```bash

View file

@ -10,11 +10,23 @@ Docker and docker-compose are tools for defining and running multi-container Doc
## Download
Copy and paste the contents of the provided example [basic.docker-compose.yml] and save it locally as `docker-compose.yml`.
Copy and paste the contents of the provided example [basic.docker-compose.yml].
## Configure
Edit the `docker-compose.yml` to match your [identity provider] settings.
### Docker-compose
Edit the `docker-compose.yml` to match your specific [identity provider]'s settings. For example, `basic.docker-compose.yml`:
<<< @/docs/docs/examples/docker/basic.docker-compose.yml
### Policy configuration
Next, create a policy configuration file which will contain the routes you want to proxy, and their desired access-controls. For example, `policy.example.yaml`:
<<< @/policy.example.yaml
### Certificates
Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt].

View file

@ -42,6 +42,11 @@ export IDP_PROVIDER_URL="https://accounts.google.com" # optional for google
export IDP_CLIENT_ID="REPLACE-ME.googleusercontent.com"
export IDP_CLIENT_SECRET="REPLACEME"
# IF GSUITE and you want to get user groups you will need to set a service account
# see identity provider docs for gooogle for more info :
# GSUITE_JSON_SERVICE_ACCOUNT='{"impersonate_user": "bdd@pomerium.io"}'
# export IDP_SERVICE_ACCOUNT=$(echo $GSUITE_JSON_SERVICE_ACCOUNT | base64)
# OKTA
# export IDP_PROVIDER="okta"
# export IDP_CLIENT_ID="REPLACEME"
@ -56,8 +61,7 @@ export IDP_CLIENT_SECRET="REPLACEME"
# export SCOPE="openid email" # generally, you want the default OIDC scopes
# k/v seperated list of simple routes. If no scheme is set, HTTPS will be used.
# Currently set to httpbin which is a handy utility letting you inspect requests recieved by
# a client application
export ROUTES="httpbin.corp.example.com=httpbin.org"
# export ROUTES="https://weirdlyssl.corp.example.com=http://neverssl.com" #https to http!
# Proxied routes and per-route policies are defined in a policy provided either
# directly as a base64 encoded yaml/json file, or as a path pointing to a
# policy file (`POLICY_FILE`)
export POLICY_FILE="./policy.example.yml"

7
go.mod
View file

@ -5,14 +5,19 @@ go 1.12
require (
github.com/golang/mock v1.2.0
github.com/golang/protobuf v1.3.0
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31
github.com/google/pprof v0.0.0-20190228041337-2ef8d84b2e3c // indirect
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect
github.com/pomerium/envconfig v1.4.0
github.com/pomerium/go-oidc v2.0.0+incompatible
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/rs/zerolog v1.12.0
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/arch v0.0.0-20190226203302-36aee92af9e8 // indirect
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
google.golang.org/api v0.1.0
google.golang.org/grpc v1.19.0
gopkg.in/square/go-jose.v2 v2.3.0
gopkg.in/yaml.v2 v2.2.2
)

22
go.sum
View file

@ -1,10 +1,14 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -14,14 +18,22 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/pprof v0.0.0-20190228041337-2ef8d84b2e3c h1:hqIMb/MbwYamune8FA5YtFAVzfTE8OXRtg9Nf0rzmqo=
github.com/google/pprof v0.0.0-20190228041337-2ef8d84b2e3c/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pomerium/envconfig v1.3.0 h1:/qJ+JOrWKkd/MgSrBDQ6xYJ7sxzqxiIAB/3qgHwdrHY=
github.com/pomerium/envconfig v1.3.0/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 h1:bNqUesLWa+RUxQvSaV3//dEFviXdCSvMF9GKDOopFLU=
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/envconfig v1.4.0 h1:o+WY/E/9M4fh0nDX7oJodU7N9p1hcHPsTnNLYjlbQA8=
github.com/pomerium/envconfig v1.4.0/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF44mDNx5nayBhC0o=
github.com/pomerium/go-oidc v2.0.0+incompatible/go.mod h1:DRsGVw6MOgxbfq4Y57jKOE8lbEfayxeiY0A8/4vxjBM=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
@ -32,7 +44,12 @@ github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7q
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c=
github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
golang.org/x/arch v0.0.0-20190226203302-36aee92af9e8 h1:G3kY3WDPiChidkYzLqbniw7jg23paUtzceZorG6YAJw=
golang.org/x/arch v0.0.0-20190226203302-36aee92af9e8/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -50,6 +67,7 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFM
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -64,6 +82,7 @@ google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -74,9 +93,12 @@ google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9M
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -11,6 +11,7 @@ import (
)
func TestPasswordHashing(t *testing.T) {
t.Parallel()
bcryptTests := []struct {
plaintext []byte
hash []byte

View file

@ -25,27 +25,6 @@ nFUSTwqQFo5gbfIlP+gvEYba+Rxj2hhqjfzqxIleRK40IRyEi3fJM/8Qhg==
-----END PUBLIC KEY-----
`
// A keypair for NIST P-384 / secp384r1
// Generated using:
// openssl ecparam -genkey -name secp384r1 -outform PEM
var pemECPrivateKeyP384 = `-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDAhA0YPVL1kimIy+FAqzUAtmR3It2Yjv2I++YpcC4oX7wGuEWcWKBYE
oOjj7wG/memgBwYFK4EEACKhZANiAAQub8xaaCTTW5rCHJCqUddIXpvq/TxdwViH
+tPEQQlJAJciXStM/aNLYA7Q1K1zMjYyzKSWz5kAh/+x4rXQ9Hlm3VAwCQDVVSjP
bfiNOXKOWfmyrGyQ7fQfs+ro1lmjLjs=
-----END EC PRIVATE KEY-----
`
var pemECPublicKeyP384 = `-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAELm/MWmgk01uawhyQqlHXSF6b6v08XcFY
h/rTxEEJSQCXIl0rTP2jS2AO0NStczI2Msykls+ZAIf/seK10PR5Zt1QMAkA1VUo
z234jTlyjln5sqxskO30H7Pq6NZZoy47
-----END PUBLIC KEY-----
`
var garbagePEM = `-----BEGIN GARBAGE-----
TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ=
-----END GARBAGE-----

View file

@ -22,7 +22,7 @@ func TestES256Signer(t *testing.T) {
}
func TestNewES256Signer(t *testing.T) {
t.Parallel()
tests := []struct {
name string
privKey []byte

View file

@ -4,6 +4,7 @@ import (
"crypto/tls"
"encoding/base64"
"fmt"
stdlog "log"
"net"
"net/http"
"os"
@ -12,6 +13,7 @@ import (
"time"
"github.com/pomerium/pomerium/internal/fileutil"
"github.com/pomerium/pomerium/internal/log"
"google.golang.org/grpc"
)
@ -90,15 +92,18 @@ func ListenAndServeTLS(opt *Options, httpHandler http.Handler, grpcHandler *grpc
} else {
h = grpcHandlerFunc(grpcHandler, httpHandler)
}
sublogger := log.With().Str("addr", opt.Addr).Logger()
// Set up the main server.
server := &http.Server{
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
// WriteTimeout is set to 0 for streaming replies
WriteTimeout: 0,
IdleTimeout: 60 * time.Second,
IdleTimeout: 5 * time.Minute,
TLSConfig: config,
Handler: h,
ErrorLog: stdlog.New(&log.StdLogWrapper{Logger: &sublogger}, "", 0),
}
return server.Serve(ln)

View file

@ -44,8 +44,10 @@ func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
if err != nil {
return nil, err
}
// Google rejects the offline scope favoring "access_type=offline"
// as part of the authorization request instead.
if len(p.Scopes) == 0 {
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access"}
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email"}
}
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
@ -86,7 +88,7 @@ func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
return nil, fmt.Errorf("identity/google: failed creating admin service %v", err)
}
} else {
log.Warn().Msg("identity/google: no service account found, cannot retrieve user groups")
log.Warn().Msg("identity/google: no service account, cannot retrieve groups")
}
gp.RevokeURL, err = url.Parse(claims.RevokeURL)
@ -114,6 +116,11 @@ func (p *GoogleProvider) Revoke(accessToken string) error {
// the required scopes explicitly.
// Google requires an additional access scope for offline access which is a requirement for any
// application that needs to access a Google API when the user is not present.
// Support for this scope differs between OpenID Connect providers. For instance
// Google rejects it, favoring appending "access_type=offline" as part of the
// authorization request instead.
//
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
// https://developers.google.com/identity/protocols/OAuth2WebServer#offline
func (p *GoogleProvider) GetSignInURL(state string) string {
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
@ -167,7 +174,7 @@ func (p *GoogleProvider) Authenticate(code string) (*sessions.SessionState, erro
}, nil
}
// Refresh renews a user's session using an oid refresh token without reprompting the user.
// Refresh renews a user's session using an oid refresh token withoutreprompting the user.
// Group membership is also refreshed.
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
func (p *GoogleProvider) Refresh(ctx context.Context, s *sessions.SessionState) (*sessions.SessionState, error) {
@ -211,7 +218,6 @@ func (p *GoogleProvider) UserGroups(ctx context.Context, user string) ([]string,
return nil, fmt.Errorf("identity/google: group api request failed %v", err)
}
for _, group := range resp.Groups {
log.Info().Str("group.Name", group.Name).Msg("sanity check3")
groups = append(groups, group.Name)
}
}

View file

@ -1,5 +1,3 @@
//go:generate protoc -I ../../proto/authenticate --go_out=plugins=grpc:../../proto/authenticate ../../proto/authenticate/authenticate.proto
// Package identity provides support for making OpenID Connect and OAuth2 authorized and
// authenticated HTTP requests with third party identity providers.
package identity // import "github.com/pomerium/pomerium/internal/identity"

View file

@ -110,3 +110,19 @@ func Ctx(ctx context.Context) *zerolog.Logger {
func FromRequest(r *http.Request) *zerolog.Logger {
return Ctx(r.Context())
}
// StdLogWrapper can be used to wrap logs originating from the from std
// library's ErrorFunction argument in http.Serve and httputil.ReverseProxy.
type StdLogWrapper struct {
*zerolog.Logger
}
func (l *StdLogWrapper) Write(p []byte) (n int, err error) {
n = len(p)
if n > 0 && p[n-1] == '\n' {
// Trim CR added by stdlog.
p = p[0 : n-1]
}
l.Error().Msg(string(p))
return len(p), nil
}

View file

@ -70,7 +70,7 @@ func TestThenFuncTreatsNilAsDefaultServeMux(t *testing.T) {
func TestThenFuncConstructsHandlerFunc(t *testing.T) {
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.WriteHeader(http.StatusOK)
})
chained := NewChain().ThenFunc(fn)
rec := httptest.NewRecorder()

83
internal/policy/policy.go Normal file
View file

@ -0,0 +1,83 @@
package policy // import "github.com/pomerium/pomerium/internal/policy"
import (
"fmt"
"io/ioutil"
"net/url"
"strings"
"time"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/fileutil"
yaml "gopkg.in/yaml.v2"
)
// Policy contains authorization policy information.
// todo(bdd) : add upstream timeout and configuration settings
type Policy struct {
// proxy related
From string `yaml:"from"`
To string `yaml:"to"`
// upstream transport settings
UpstreamTimeout time.Duration `yaml:"timeout"`
// Identity related policy
AllowedEmails []string `yaml:"allowed_users"`
AllowedGroups []string `yaml:"allowed_groups"`
AllowedDomains []string `yaml:"allowed_domains"`
Source *url.URL
Destination *url.URL
}
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
}
return nil
}
// FromConfig parses configuration file as bytes and returns authorization
// policies. Supports yaml, json.
func FromConfig(confBytes []byte) ([]Policy, error) {
var f []Policy
if err := yaml.Unmarshal(confBytes, &f); err != nil {
return nil, err
}
// build source and destination urls
for i := range f {
if err := (&f[i]).validate(); err != nil {
return nil, err
}
}
log.Info().Msgf("from config %+v", f)
return f, nil
}
// FromConfigFile parses configuration file from a path and returns
// authorization policies. Supports yaml, json.
func FromConfigFile(f string) ([]Policy, error) {
exists, err := fileutil.IsReadableFile(f)
if err != nil || !exists {
return nil, fmt.Errorf("policy file %v: %v exists? %v", f, err, exists)
}
confBytes, err := ioutil.ReadFile(f)
if err != nil {
return nil, err
}
return FromConfig(confBytes)
}
// 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)
}

View file

@ -0,0 +1,92 @@
package policy
import (
"net/url"
"reflect"
"testing"
)
func TestFromConfig(t *testing.T) {
t.Parallel()
source, _ := urlParse("pomerium.io")
dest, _ := urlParse("httpbin.org")
tests := []struct {
name string
yamlBytes []byte
want []Policy
wantErr bool
}{
{"simple json", []byte(`[{"from": "pomerium.io","to":"httpbin.org"}]`), []Policy{{From: "pomerium.io", To: "httpbin.org", Source: source, Destination: dest}}, false},
{"bad from", []byte(`[{"from": "%","to":"httpbin.org"}]`), nil, true},
{"bad to", []byte(`[{"from": "pomerium.io","to":"%"}]`), nil, true},
{"simple error", []byte(`{}`), nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := FromConfig(tt.yamlBytes)
if (err != nil) != tt.wantErr {
t.Errorf("FromConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("FromConfig() = \n%v, want \n%v", got, tt.want)
}
})
}
}
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 TestFromConfigFile(t *testing.T) {
t.Parallel()
source, _ := urlParse("pomerium.io")
dest, _ := urlParse("httpbin.org")
tests := []struct {
name string
f string
want []Policy
wantErr bool
}{
{"simple json", "./testdata/basic.json", []Policy{{From: "pomerium.io", To: "httpbin.org", Source: source, Destination: dest}}, false},
{"simple yaml", "./testdata/basic.yaml", []Policy{{From: "pomerium.io", To: "httpbin.org", Source: source, Destination: dest}}, false},
{"failed dir", "./testdata/", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := FromConfigFile(tt.f)
if (err != nil) != tt.wantErr {
t.Errorf("FromConfigFile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("FromConfigFile() = %v, want %v", got, tt.want)
}
})
}
}

6
internal/policy/testdata/basic.json vendored Normal file
View file

@ -0,0 +1,6 @@
[
{
"from": "pomerium.io",
"to": "httpbin.org"
}
]

2
internal/policy/testdata/basic.yaml vendored Normal file
View file

@ -0,0 +1,2 @@
- from: pomerium.io
to: httpbin.org

View file

@ -106,17 +106,6 @@ func TestCookieStore_makeCookie(t *testing.T) {
t.Fatal(err)
}
type fields struct {
Name string
CSRFCookieName string
CookieCipher cryptutil.Cipher
CookieExpire time.Duration
CookieRefresh time.Duration
CookieSecure bool
CookieHTTPOnly bool
CookieDomain string
}
now := time.Now()
tests := []struct {
name string

19
policy.example.yaml Normal file
View file

@ -0,0 +1,19 @@
- from: httpbin.corp.beyondperimeter.com
to: http://httpbin
allowed_domains:
- pomerium.io
- from: external-httpbin.corp.beyondperimeter.com
to: httpbin.org
allowed_domains:
- gmail.com
- from: weirdlyssl.corp.beyondperimeter.com
to: http://neverssl.com
allowed_users:
- bdd@pomerium.io
allowed_groups:
- admins
- developers
- from: hello.corp.beyondperimeter.com
to: http://hello:8080
allowed_groups:
- admins

View file

@ -1,142 +0,0 @@
package mock_authenticate_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
pb "github.com/pomerium/pomerium/proto/authenticate"
mock "github.com/pomerium/pomerium/proto/authenticate/mock_authenticate"
)
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
// rpcMsg implements the gomock.Matcher interface
type rpcMsg struct {
msg proto.Message
}
func (r *rpcMsg) Matches(msg interface{}) bool {
m, ok := msg.(proto.Message)
if !ok {
return false
}
return proto.Equal(m, r.msg)
}
func (r *rpcMsg) String() string {
return fmt.Sprintf("is %s", r.msg)
}
func TestValidate(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockAuthenticateClient := mock.NewMockAuthenticatorClient(ctrl)
req := &pb.ValidateRequest{IdToken: "unit_test"}
mockAuthenticateClient.EXPECT().Validate(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.ValidateReply{IsValid: false}, nil)
testValidate(t, mockAuthenticateClient)
}
func testValidate(t *testing.T, client pb.AuthenticatorClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := client.Validate(ctx, &pb.ValidateRequest{IdToken: "unit_test"})
if err != nil || r.IsValid != false {
t.Errorf("mocking failed")
}
t.Log("Reply : ", r.IsValid)
}
func TestAuthenticate(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockAuthenticateClient := mock.NewMockAuthenticatorClient(ctrl)
mockExpire, err := ptypes.TimestampProto(fixedDate)
if err != nil {
t.Fatalf("%v failed converting timestamp", err)
}
req := &pb.AuthenticateRequest{Code: "unit_test"}
mockAuthenticateClient.EXPECT().Authenticate(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.Session{
AccessToken: "mocked access token",
RefreshToken: "mocked refresh token",
IdToken: "mocked id token",
User: "user1",
Email: "test@email.com",
LifetimeDeadline: mockExpire,
}, nil)
testAuthenticate(t, mockAuthenticateClient)
}
func testAuthenticate(t *testing.T, client pb.AuthenticatorClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := client.Authenticate(ctx, &pb.AuthenticateRequest{Code: "unit_test"})
if err != nil {
t.Errorf("mocking failed %v", err)
}
if r.AccessToken != "mocked access token" {
t.Errorf("authenticate: invalid access token")
}
if r.RefreshToken != "mocked refresh token" {
t.Errorf("authenticate: invalid refresh token")
}
if r.IdToken != "mocked id token" {
t.Errorf("authenticate: invalid id token")
}
if r.User != "user1" {
t.Errorf("authenticate: invalid user")
}
if r.Email != "test@email.com" {
t.Errorf("authenticate: invalid email")
}
}
func TestRefresh(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRefreshClient := mock.NewMockAuthenticatorClient(ctrl)
mockExpire, err := ptypes.TimestampProto(fixedDate)
if err != nil {
t.Fatalf("%v failed converting timestamp", err)
}
req := &pb.Session{RefreshToken: "unit_test"}
mockRefreshClient.EXPECT().Refresh(
gomock.Any(),
&rpcMsg{msg: req},
).Return(&pb.Session{
AccessToken: "mocked access token",
LifetimeDeadline: mockExpire,
}, nil)
testRefresh(t, mockRefreshClient)
}
func testRefresh(t *testing.T, client pb.AuthenticatorClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := client.Refresh(ctx, &pb.Session{RefreshToken: "unit_test"})
if err != nil {
t.Errorf("mocking failed %v", err)
}
if r.AccessToken != "mocked access token" {
t.Errorf("Refresh: invalid access token")
}
respExpire, err := ptypes.Timestamp(r.LifetimeDeadline)
if err != nil {
t.Fatalf("%v failed converting timestamp", err)
}
if respExpire != fixedDate {
t.Errorf("Refresh: bad expiry got:%v want:%v", respExpire, fixedDate)
}
}

View file

@ -0,0 +1,221 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: authorize.proto
package authorize
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type AuthorizeRequest struct {
// request context
Route string `protobuf:"bytes,1,opt,name=route,proto3" json:"route,omitempty"`
// user context
User string `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"`
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
Groups []string `protobuf:"bytes,4,rep,name=groups,proto3" json:"groups,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AuthorizeRequest) Reset() { *m = AuthorizeRequest{} }
func (m *AuthorizeRequest) String() string { return proto.CompactTextString(m) }
func (*AuthorizeRequest) ProtoMessage() {}
func (*AuthorizeRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_authorize_dad4e29706fc340b, []int{0}
}
func (m *AuthorizeRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthorizeRequest.Unmarshal(m, b)
}
func (m *AuthorizeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_AuthorizeRequest.Marshal(b, m, deterministic)
}
func (dst *AuthorizeRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_AuthorizeRequest.Merge(dst, src)
}
func (m *AuthorizeRequest) XXX_Size() int {
return xxx_messageInfo_AuthorizeRequest.Size(m)
}
func (m *AuthorizeRequest) XXX_DiscardUnknown() {
xxx_messageInfo_AuthorizeRequest.DiscardUnknown(m)
}
var xxx_messageInfo_AuthorizeRequest proto.InternalMessageInfo
func (m *AuthorizeRequest) GetRoute() string {
if m != nil {
return m.Route
}
return ""
}
func (m *AuthorizeRequest) GetUser() string {
if m != nil {
return m.User
}
return ""
}
func (m *AuthorizeRequest) GetEmail() string {
if m != nil {
return m.Email
}
return ""
}
func (m *AuthorizeRequest) GetGroups() []string {
if m != nil {
return m.Groups
}
return nil
}
type AuthorizeReply struct {
IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AuthorizeReply) Reset() { *m = AuthorizeReply{} }
func (m *AuthorizeReply) String() string { return proto.CompactTextString(m) }
func (*AuthorizeReply) ProtoMessage() {}
func (*AuthorizeReply) Descriptor() ([]byte, []int) {
return fileDescriptor_authorize_dad4e29706fc340b, []int{1}
}
func (m *AuthorizeReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AuthorizeReply.Unmarshal(m, b)
}
func (m *AuthorizeReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_AuthorizeReply.Marshal(b, m, deterministic)
}
func (dst *AuthorizeReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_AuthorizeReply.Merge(dst, src)
}
func (m *AuthorizeReply) XXX_Size() int {
return xxx_messageInfo_AuthorizeReply.Size(m)
}
func (m *AuthorizeReply) XXX_DiscardUnknown() {
xxx_messageInfo_AuthorizeReply.DiscardUnknown(m)
}
var xxx_messageInfo_AuthorizeReply proto.InternalMessageInfo
func (m *AuthorizeReply) GetIsValid() bool {
if m != nil {
return m.IsValid
}
return false
}
func init() {
proto.RegisterType((*AuthorizeRequest)(nil), "authorize.AuthorizeRequest")
proto.RegisterType((*AuthorizeReply)(nil), "authorize.AuthorizeReply")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// AuthorizerClient is the client API for Authorizer service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type AuthorizerClient interface {
Authorize(ctx context.Context, in *AuthorizeRequest, opts ...grpc.CallOption) (*AuthorizeReply, error)
}
type authorizerClient struct {
cc *grpc.ClientConn
}
func NewAuthorizerClient(cc *grpc.ClientConn) AuthorizerClient {
return &authorizerClient{cc}
}
func (c *authorizerClient) Authorize(ctx context.Context, in *AuthorizeRequest, opts ...grpc.CallOption) (*AuthorizeReply, error) {
out := new(AuthorizeReply)
err := c.cc.Invoke(ctx, "/authorize.Authorizer/Authorize", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthorizerServer is the server API for Authorizer service.
type AuthorizerServer interface {
Authorize(context.Context, *AuthorizeRequest) (*AuthorizeReply, error)
}
func RegisterAuthorizerServer(s *grpc.Server, srv AuthorizerServer) {
s.RegisterService(&_Authorizer_serviceDesc, srv)
}
func _Authorizer_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AuthorizeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthorizerServer).Authorize(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/authorize.Authorizer/Authorize",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthorizerServer).Authorize(ctx, req.(*AuthorizeRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Authorizer_serviceDesc = grpc.ServiceDesc{
ServiceName: "authorize.Authorizer",
HandlerType: (*AuthorizerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Authorize",
Handler: _Authorizer_Authorize_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "authorize.proto",
}
func init() { proto.RegisterFile("authorize.proto", fileDescriptor_authorize_dad4e29706fc340b) }
var fileDescriptor_authorize_dad4e29706fc340b = []byte{
// 187 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4f, 0x2c, 0x2d, 0xc9,
0xc8, 0x2f, 0xca, 0xac, 0x4a, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x84, 0x0b, 0x28,
0x65, 0x71, 0x09, 0x38, 0xc2, 0x38, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x22, 0x5c,
0xac, 0x45, 0xf9, 0xa5, 0x25, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x10, 0x8e, 0x90,
0x10, 0x17, 0x4b, 0x69, 0x71, 0x6a, 0x91, 0x04, 0x13, 0x58, 0x10, 0xcc, 0x06, 0xa9, 0x4c, 0xcd,
0x4d, 0xcc, 0xcc, 0x91, 0x60, 0x86, 0xa8, 0x04, 0x73, 0x84, 0xc4, 0xb8, 0xd8, 0xd2, 0x8b, 0xf2,
0x4b, 0x0b, 0x8a, 0x25, 0x58, 0x14, 0x98, 0x35, 0x38, 0x83, 0xa0, 0x3c, 0x25, 0x6d, 0x2e, 0x3e,
0x24, 0xbb, 0x0a, 0x72, 0x2a, 0x85, 0x24, 0xb9, 0x38, 0x32, 0x8b, 0xe3, 0xcb, 0x12, 0x73, 0x32,
0x53, 0xc0, 0x96, 0x71, 0x04, 0xb1, 0x67, 0x16, 0x87, 0x81, 0xb8, 0x46, 0xc1, 0x5c, 0x5c, 0x70,
0xc5, 0x45, 0x42, 0xae, 0x5c, 0x9c, 0x70, 0x9e, 0x90, 0xb4, 0x1e, 0xc2, 0x43, 0xe8, 0x8e, 0x97,
0x92, 0xc4, 0x2e, 0x59, 0x90, 0x53, 0xa9, 0xc4, 0x90, 0xc4, 0x06, 0xf6, 0xbf, 0x31, 0x20, 0x00,
0x00, 0xff, 0xff, 0x28, 0xac, 0x76, 0x2d, 0x12, 0x01, 0x00, 0x00,
}

View file

@ -0,0 +1,18 @@
syntax = "proto3";
package authorize;
service Authorizer {
rpc Authorize(AuthorizeRequest) returns (AuthorizeReply) {}
}
message AuthorizeRequest {
// request context
string route = 1;
// user context
string user = 2;
string email = 3;
repeated string groups = 4;
}
message AuthorizeReply { bool is_valid = 1; }

View file

@ -1,45 +0,0 @@
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator"
import (
"context"
"github.com/pomerium/pomerium/internal/sessions"
)
// Authenticator provides the authenticate service interface
type Authenticator interface {
// Redeem takes a code and returns a validated session or an error
Redeem(context.Context, string) (*sessions.SessionState, error)
// Refresh attempts to refresh a valid session with a refresh token. Returns a refreshed session.
Refresh(context.Context, *sessions.SessionState) (*sessions.SessionState, error)
// Validate evaluates a given oidc id_token for validity. Returns validity and any error.
Validate(context.Context, string) (bool, error)
// Close closes the authenticator connection if any.
Close() error
}
// Options contains options for connecting to an authenticate service .
type Options struct {
// Addr is the location of the authenticate service. Used if InternalAddr is not set.
Addr string
Port int
// InternalAddr is the internal (behind the ingress) address to use when making an
// authentication connection. If empty, Addr is used.
InternalAddr string
// 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.
OverrideCertificateName string
// Shared secret is used to authenticate a authenticate-client with a authenticate-server.
SharedSecret string
// CA specifies the base64 encoded TLS certificate authority to use.
CA string
// CAFile specifies the TLS certificate authority file to use.
CAFile string
}
// New returns a new authenticate service client. Takes a client implementation name as an argument.
// Currently only gRPC is supported and is always returned.
func New(name string, opts *Options) (a Authenticator, err error) {
return NewGRPC(opts)
}

View file

@ -1,90 +1,37 @@
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator"
package clients // import "github.com/pomerium/pomerium/proxy/clients"
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/sessions"
pb "github.com/pomerium/pomerium/proto/authenticate"
)
// NewGRPC returns a new authenticate service client.
func NewGRPC(opts *Options) (p *AuthenticateGRPC, err error) {
// gRPC uses a pre-shared secret middleware to establish authentication b/w server and client
if opts.SharedSecret == "" {
return nil, errors.New("proxy/authenticator: grpc client requires shared secret")
}
grpcAuth := middleware.NewSharedSecretCred(opts.SharedSecret)
// Authenticator provides the authenticate service interface
type Authenticator interface {
// Redeem takes a code and returns a validated session or an error
Redeem(context.Context, string) (*sessions.SessionState, error)
// Refresh attempts to refresh a valid session with a refresh token. Returns a refreshed session.
Refresh(context.Context, *sessions.SessionState) (*sessions.SessionState, error)
// Validate evaluates a given oidc id_token for validity. Returns validity and any error.
Validate(context.Context, string) (bool, error)
// Close closes the authenticator connection if any.
Close() error
}
var connAddr string
if opts.InternalAddr != "" {
connAddr = opts.InternalAddr
} else {
connAddr = opts.Addr
}
if connAddr == "" {
return nil, errors.New("proxy/authenticator: connection address required")
}
// no colon exists in the connection string, assume one must be added manually
if !strings.Contains(connAddr, ":") {
connAddr = fmt.Sprintf("%s:%d", connAddr, opts.Port)
}
// NewAuthenticateClient returns a new authenticate service client.
func NewAuthenticateClient(name string, opts *Options) (a Authenticator, err error) {
// Only gRPC is supported and is always returned so name is ignored
return NewGRPCAuthenticateClient(opts)
}
var cp *x509.CertPool
if opts.CA != "" || opts.CAFile != "" {
cp = x509.NewCertPool()
var ca []byte
var err error
if opts.CA != "" {
ca, err = base64.StdEncoding.DecodeString(opts.CA)
if err != nil {
return nil, fmt.Errorf("failed to decode certificate authority: %v", err)
}
} else {
ca, err = ioutil.ReadFile(opts.CAFile)
if err != nil {
return nil, fmt.Errorf("certificate authority file %v not readable: %v", opts.CAFile, err)
}
}
if ok := cp.AppendCertsFromPEM(ca); !ok {
return nil, fmt.Errorf("failed to append CA cert to certPool")
}
} else {
newCp, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
cp = newCp
}
log.Info().
Str("OverrideCertificateName", opts.OverrideCertificateName).
Str("addr", connAddr).Msgf("proxy/authenticator: grpc connection")
cert := credentials.NewTLS(&tls.Config{RootCAs: cp})
// override allowed certificate name string, typically used when doing behind ingress connection
if opts.OverrideCertificateName != "" {
err = cert.OverrideServerName(opts.OverrideCertificateName)
if err != nil {
return nil, err
}
}
conn, err := grpc.Dial(
connAddr,
grpc.WithTransportCredentials(cert),
grpc.WithPerRPCCredentials(grpcAuth),
)
// NewGRPCAuthenticateClient returns a new authenticate service client.
func NewGRPCAuthenticateClient(opts *Options) (p *AuthenticateGRPC, err error) {
conn, err := NewGRPCClientConn(opts)
if err != nil {
return nil, err
}

View file

@ -1,4 +1,4 @@
package authenticator
package clients // import "github.com/pomerium/pomerium/proxy/clients"
import (
"context"
@ -16,6 +16,27 @@ import (
mock "github.com/pomerium/pomerium/proto/authenticate/mock_authenticate"
)
func TestNew(t *testing.T) {
tests := []struct {
name string
serviceName string
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},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewAuthenticateClient(tt.serviceName, tt.opts)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
// rpcMsg implements the gomock.Matcher interface
@ -194,27 +215,27 @@ func TestNewGRPC(t *testing.T) {
wantTarget string
}{
{"no shared secret", &Options{}, true, "proxy/authenticator: grpc client requires shared secret", ""},
{"empty connection", &Options{Addr: "", Port: 443, SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
{"both internal and addr empty", &Options{Addr: "", Port: 443, InternalAddr: "", SharedSecret: "shh"}, true, "proxy/authenticator: connection address required", ""},
{"internal addr with port", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local:8443", SharedSecret: "shh"}, false, "", "intranet.local:8443"},
{"internal addr without port", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", SharedSecret: "shh"}, false, "", "intranet.local:443"},
{"cert override", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh"}, false, "", "intranet.local:443"},
{"custom ca", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZrQ0ZBWHhneFg5K0hjWlBVVVBEK0laV0NGNUEvVTdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1FVXgKQ3pBSkJnTlZCQVlUQWtGVk1STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbApjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTk1Ua3dNakk0TVRnMU1EQTNXaGNOTWprd01qSTFNVGcxCk1EQTNXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNBd0tVMjl0WlMxVGRHRjBaVEVoTUI4R0ExVUUKQ2d3WVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBOVRFMEFiaTdnMHhYeURkVUtEbDViNTBCT05ZVVVSc3F2THQrSWkwdlpjMzRRTHhOClJrT0hrOFZEVUgzcUt1N2UrNGVubUdLVVNUdzRPNFlkQktiSWRJTFpnb3o0YitNL3FVOG5adVpiN2pBVTdOYWkKajMzVDVrbXB3L2d4WHNNUzNzdUpXUE1EUDB3Z1BUZUVRK2J1bUxVWmpLdUVIaWNTL0l5dmtaVlBzRlE4NWlaUwpkNXE2a0ZGUUdjWnFXeFg0dlhDV25Sd3E3cHY3TThJd1RYc1pYSVRuNXB5Z3VTczNKb29GQkg5U3ZNTjRKU25GCmJMK0t6ekduMy9ScXFrTXpMN3FUdkMrNWxVT3UxUmNES21mZXBuVGVaN1IyVnJUQm42NndWMjVHRnBkSDIzN00KOXhJVkJrWEd1U2NvWHVPN1lDcWFrZkt6aXdoRTV4UmRaa3gweXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFCaHRWUEI0OCs4eFZyVmRxM1BIY3k5QkxtVEtrRFl6N2Q0ODJzTG1HczBuVUdGSTFZUDdmaFJPV3ZxCktCTlpkNEI5MUpwU1NoRGUrMHpoNno4WG5Ha01mYnRSYWx0NHEwZ3lKdk9hUWhqQ3ZCcSswTFk5d2NLbXpFdnMKcTRiNUZ5NXNpRUZSekJLTmZtTGwxTTF2cW1hNmFCVnNYUUhPREdzYS83dE5MalZ2ay9PYm52cFg3UFhLa0E3cQpLMTQvV0tBRFBJWm9mb00xMzB4Q1RTYXVpeXROajlnWkx1WU9leEZhblVwNCt2MHBYWS81OFFSNTk2U0ROVTlKClJaeDhwTzBTaUYvZXkxVUZXbmpzdHBjbTQzTFVQKzFwU1hFeVhZOFJrRTI2QzNvdjNaTFNKc2pMbC90aXVqUlgKZUJPOWorWDdzS0R4amdtajBPbWdpVkpIM0YrUAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}, false, "", "intranet.local:443"},
{"bad ca encoding", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CA: "^"}, true, "", "intranet.local:443"},
{"custom ca file", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt"}, false, "", "intranet.local:443"},
{"bad custom ca file", &Options{Addr: "", Port: 443, InternalAddr: "intranet.local", OverrideCertificateName: "*.local", SharedSecret: "shh", CAFile: "testdata/example.crt2"}, true, "", "intranet.local:443"},
{"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"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewGRPC(tt.opts)
got, err := NewGRPCAuthenticateClient(tt.opts)
if (err != nil) != tt.wantErr {
t.Errorf("NewGRPC() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("NewGRPCAuthenticateClient() error = %v, wantErr %v", err, tt.wantErr)
if !strings.EqualFold(err.Error(), tt.wantErrStr) {
t.Errorf("NewGRPC() error = %v did not contain wantErr %v", err, tt.wantErrStr)
t.Errorf("NewGRPCAuthenticateClient() error = %v did not contain wantErr %v", err, tt.wantErrStr)
}
}
if got != nil && got.Conn.Target() != tt.wantTarget {
t.Errorf("NewGRPC() target = %v expected %v", got.Conn.Target(), tt.wantTarget)
t.Errorf("NewGRPCAuthenticateClient() target = %v expected %v", got.Conn.Target(), tt.wantTarget)
}
})

View file

@ -0,0 +1,64 @@
package clients // import "github.com/pomerium/pomerium/proxy/clients"
import (
"context"
"errors"
"time"
pb "github.com/pomerium/pomerium/proto/authorize"
"google.golang.org/grpc"
"github.com/pomerium/pomerium/internal/sessions"
)
// Authorizer provides the authorize service interface
type Authorizer interface {
// Authorize takes a code and returns a validated session or an error
Authorize(context.Context, string, *sessions.SessionState) (bool, error)
// Close closes the auth connection if any.
Close() error
}
// NewAuthorizeClient returns a new authorize service client.
func NewAuthorizeClient(name string, opts *Options) (a Authorizer, err error) {
// Only gRPC is supported and is always returned so name is ignored
return NewGRPCAuthorizeClient(opts)
}
// NewGRPCAuthorizeClient returns a new authorize service client.
func NewGRPCAuthorizeClient(opts *Options) (p *AuthorizeGRPC, err error) {
conn, err := NewGRPCClientConn(opts)
if err != nil {
return nil, err
}
client := pb.NewAuthorizerClient(conn)
return &AuthorizeGRPC{Conn: conn, client: client}, nil
}
// AuthorizeGRPC is a gRPC implementation of an authenticator (authenticate client)
type AuthorizeGRPC struct {
Conn *grpc.ClientConn
client pb.AuthorizerClient
}
// Authorize makes an RPC call to the authorize service to creates a session state
// from an encrypted code provided as a result of an oauth2 callback process.
func (a *AuthorizeGRPC) Authorize(ctx context.Context, route string, s *sessions.SessionState) (bool, error) {
if s == nil {
return false, errors.New("session cannot be nil")
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
response, err := a.client.Authorize(ctx, &pb.AuthorizeRequest{
Route: route,
User: s.User,
Email: s.Email,
Groups: s.Groups,
})
return response.GetIsValid(), err
}
// Close tears down the ClientConn and all underlying connections.
func (a *AuthorizeGRPC) Close() error {
return a.Conn.Close()
}

View file

@ -0,0 +1,47 @@
package clients
import (
"context"
"testing"
"github.com/pomerium/pomerium/internal/sessions"
pb "github.com/pomerium/pomerium/proto/authorize"
"google.golang.org/grpc"
)
func TestAuthorizeGRPC_Authorize(t *testing.T) {
type fields struct {
Conn *grpc.ClientConn
client pb.AuthorizerClient
}
type args struct {
ctx context.Context
route string
s *sessions.SessionState
}
tests := []struct {
name string
fields fields
args args
want bool
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &AuthorizeGRPC{
Conn: tt.fields.Conn,
client: tt.fields.client,
}
got, err := a.Authorize(tt.args.ctx, tt.args.route, tt.args.s)
if (err != nil) != tt.wantErr {
t.Errorf("AuthorizeGRPC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("AuthorizeGRPC.Authorize() = %v, want %v", got, tt.want)
}
})
}
}

106
proxy/clients/clients.go Normal file
View file

@ -0,0 +1,106 @@
package clients // import "github.com/pomerium/pomerium/proxy/clients"
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/middleware"
)
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
// InternalAddr is the internal (behind the ingress) address to use when
// making a connection. If empty, Addr is used.
InternalAddr string
// 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.
OverrideCertificateName string
// Shared secret is used to authenticate a authenticate-client with a authenticate-server.
SharedSecret string
// CA specifies the base64 encoded TLS certificate authority to use.
CA string
// CAFile specifies the TLS certificate authority file to use.
CAFile string
}
// NewGRPCClientConn returns a new gRPC pomerium service client connection.
func NewGRPCClientConn(opts *Options) (*grpc.ClientConn, error) {
// gRPC uses a pre-shared secret middleware to establish authentication b/w server and client
if opts.SharedSecret == "" {
return nil, errors.New("proxy/clients: grpc client requires shared secret")
}
grpcAuth := middleware.NewSharedSecretCred(opts.SharedSecret)
var connAddr string
if opts.InternalAddr != "" {
connAddr = opts.InternalAddr
} else {
connAddr = opts.Addr
}
if connAddr == "" {
return nil, errors.New("proxy/clients: connection address required")
}
// no colon exists in the connection string, assume one must be added manually
if !strings.Contains(connAddr, ":") {
connAddr = fmt.Sprintf("%s:%d", connAddr, defaultGRPCPort)
}
var cp *x509.CertPool
if opts.CA != "" || opts.CAFile != "" {
cp = x509.NewCertPool()
var ca []byte
var err error
if opts.CA != "" {
ca, err = base64.StdEncoding.DecodeString(opts.CA)
if err != nil {
return nil, fmt.Errorf("failed to decode certificate authority: %v", err)
}
} else {
ca, err = ioutil.ReadFile(opts.CAFile)
if err != nil {
return nil, fmt.Errorf("certificate authority file %v not readable: %v", opts.CAFile, err)
}
}
if ok := cp.AppendCertsFromPEM(ca); !ok {
return nil, fmt.Errorf("failed to append CA cert to certPool")
}
} else {
newCp, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
cp = newCp
}
log.Info().
Str("OverrideCertificateName", opts.OverrideCertificateName).
Str("addr", connAddr).Msgf("proxy/clients: grpc connection")
cert := credentials.NewTLS(&tls.Config{RootCAs: cp})
// override allowed certificate name string, typically used when doing behind ingress connection
if opts.OverrideCertificateName != "" {
err := cert.OverrideServerName(opts.OverrideCertificateName)
if err != nil {
return nil, err
}
}
return grpc.Dial(
connAddr,
grpc.WithTransportCredentials(cert),
grpc.WithPerRPCCredentials(grpcAuth),
)
}

View file

@ -1,4 +1,4 @@
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator"
package clients // import "github.com/pomerium/pomerium/proxy/clients"
import (
"context"
@ -34,3 +34,18 @@ func (a MockAuthenticate) Validate(ctx context.Context, idToken string) (bool, e
// Close is a mocked authenticator client function.
func (a MockAuthenticate) Close() error { return a.CloseError }
// MockAuthorize provides a mocked implementation of the authorizer interface.
type MockAuthorize struct {
AuthorizeResponse bool
AuthorizeError error
CloseError error
}
// Close is a mocked authorizer client function.
func (a MockAuthorize) Close() error { return a.CloseError }
// Authorize is a mocked authorizer client function.
func (a MockAuthorize) Authorize(ctx context.Context, route string, s *sessions.SessionState) (bool, error) {
return a.AuthorizeResponse, a.AuthorizeError
}

View file

@ -1,4 +1,4 @@
package authenticator
package clients
import (
"context"
@ -59,24 +59,3 @@ func TestMockAuthenticate(t *testing.T) {
}
}
func TestNew(t *testing.T) {
tests := []struct {
name string
serviceName string
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},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := New(tt.serviceName, tt.opts)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}

View file

@ -19,7 +19,7 @@ import (
)
var (
// ErrUserNotAuthorized is set when user is not authorized to access a resource
// ErrUserNotAuthorized is set when user is not authorized to access a resource.
ErrUserNotAuthorized = errors.New("user not authorized")
)
@ -242,9 +242,18 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
return
}
}
// todo(bdd): add authorization service validation
// remove dupe session call
session, err := p.sessionStore.LoadSession(r)
if err != nil {
p.sessionStore.ClearSession(w, r)
return
}
authorized, err := p.AuthorizeClient.Authorize(r.Context(), r.Host, session)
if !authorized || err != nil {
log.FromRequest(r).Warn().Err(err).Msg("proxy: user unauthorized")
httputil.ErrorResponse(w, r, "Access unauthorized", http.StatusForbidden)
return
}
// We have validated the users request and now proxy their request to the provided upstream.
route, ok := p.router(r)
if !ok {
@ -279,7 +288,7 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) {
// httputil.ErrorResponse(w, r, err.Error(), http.StatusInternalServerError)
// return
// }
// fmt.Fprintf(w, string(jsonSession))
// fmt.Fprint(w, string(jsonSession))
// }
// Authenticate authenticates a request by checking for a session cookie, and validating its expiration,

View file

@ -1,6 +1,7 @@
package proxy
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
@ -13,7 +14,7 @@ import (
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/version"
"github.com/pomerium/pomerium/proxy/authenticator"
"github.com/pomerium/pomerium/proxy/clients"
)
type mockCipher struct{}
@ -152,13 +153,11 @@ func TestProxy_Favicon(t *testing.T) {
t.Fatal(err)
}
req := httptest.NewRequest("GET", "/favicon.ico", nil)
rr := httptest.NewRecorder()
proxy.Favicon(rr, req)
if status := rr.Code; status != http.StatusNotFound {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound)
}
// todo(bdd) : good way of mocking auth then serving a simple favicon?
}
func TestProxy_Signout(t *testing.T) {
@ -190,7 +189,7 @@ 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://sso-auth.corp.beyondperimeter.com/sign_in`
expected := `<a href="https://authenticate.corp.beyondperimeter.com/sign_in`
body := rr.Body.String()
if !strings.HasPrefix(body, expected) {
t.Errorf("handler returned unexpected body: got %v want %v", body, expected)
@ -219,8 +218,6 @@ func TestProxy_Handler(t *testing.T) {
}
func TestProxy_OAuthCallback(t *testing.T) {
//todo(bdd): test malformed requests
// https://github.com/golang/go/blob/master/src/net/http/request_test.go#L110
normalSession := sessions.MockSessionStore{
Session: &sessions.SessionState{
AccessToken: "AccessToken",
@ -229,7 +226,7 @@ func TestProxy_OAuthCallback(t *testing.T) {
RefreshDeadline: time.Now().Add(-10 * time.Second),
},
}
normalAuth := authenticator.MockAuthenticate{
normalAuth := clients.MockAuthenticate{
RedeemResponse: &sessions.SessionState{
AccessToken: "AccessToken",
RefreshToken: "RefreshToken",
@ -247,13 +244,13 @@ func TestProxy_OAuthCallback(t *testing.T) {
name string
csrf sessions.MockCSRFStore
session sessions.MockSessionStore
authenticator authenticator.MockAuthenticate
authenticator clients.MockAuthenticate
params map[string]string
wantCode int
}{
{"good", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusFound},
{"error", normalCsrf, normalSession, normalAuth, map[string]string{"error": "some error"}, http.StatusForbidden},
{"code err", normalCsrf, normalSession, authenticator.MockAuthenticate{RedeemError: errors.New("error")}, map[string]string{"code": "error"}, http.StatusInternalServerError},
{"code err", normalCsrf, normalSession, clients.MockAuthenticate{RedeemError: errors.New("error")}, map[string]string{"code": "error"}, http.StatusInternalServerError},
{"state err", normalCsrf, normalSession, normalAuth, map[string]string{"code": "code", "state": "error"}, http.StatusInternalServerError},
{"csrf err", sessions.MockCSRFStore{GetError: errors.New("error")}, normalSession, normalAuth, map[string]string{"code": "code", "state": "state"}, http.StatusBadRequest},
{"unmarshal err", sessions.MockCSRFStore{
@ -311,30 +308,31 @@ func Test_extendDeadline(t *testing.T) {
}
func TestProxy_router(t *testing.T) {
configBlob := `[{"from":"corp.example.com","to":"example.com"}]` //valid yaml
policy := base64.URLEncoding.EncodeToString([]byte(configBlob))
tests := []struct {
name string
host string
mux map[string]string
mux string
route http.Handler
wantOk bool
}{
{"good corp", "https://corp.example.com", map[string]string{"corp.example.com": "example.com"}, nil, true},
{"good with slash", "https://corp.example.com/", map[string]string{"corp.example.com": "example.com"}, nil, true},
{"good with path", "https://corp.example.com/123", map[string]string{"corp.example.com": "example.com"}, nil, true},
{"multiple", "https://corp.example.com/", map[string]string{"corp.unrelated.com": "unrelated.com", "corp.example.com": "example.com"}, nil, true},
{"bad corp", "https://notcorp.example.com/123", map[string]string{"corp.example.com": "example.com"}, nil, false},
{"bad sub-sub", "https://notcorp.corp.example.com/123", map[string]string{"corp.example.com": "example.com"}, nil, false},
{"good corp", "https://corp.example.com", policy, nil, true},
{"good with slash", "https://corp.example.com/", policy, nil, true},
{"good with path", "https://corp.example.com/123", policy, nil, true},
// {"multiple", "https://corp.example.com/", map[string]string{"corp.unrelated.com": "unrelated.com", "corp.example.com": "example.com"}, nil, true},
{"bad corp", "https://notcorp.example.com/123", policy, nil, false},
{"bad sub-sub", "https://notcorp.corp.example.com/123", policy, nil, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := testOptions()
opts.Routes = tt.mux
opts.Policy = tt.mux
p, err := New(opts)
if err != nil {
t.Fatal(err)
}
p.AuthenticateClient = authenticator.MockAuthenticate{}
p.AuthenticateClient = clients.MockAuthenticate{}
p.cipher = mockCipher{}
req := httptest.NewRequest("GET", tt.host, nil)
@ -358,14 +356,16 @@ func TestProxy_Proxy(t *testing.T) {
name string
host string
session sessions.SessionStore
authenticator authenticator.Authenticator
authenticator clients.Authenticator
authorizer clients.Authorizer
wantStatus int
}{
// weirdly, we want 503 here because that means proxy is trying to route a domain (example.com) that we dont control. Weird. I know.
{"good", "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, authenticator.MockAuthenticate{}, http.StatusServiceUnavailable},
{"unexpected error", "https://corp.example.com/test", &sessions.MockSessionStore{LoadError: errors.New("ok")}, authenticator.MockAuthenticate{}, http.StatusInternalServerError},
{"good", "https://corp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusServiceUnavailable},
{"unexpected error", "https://corp.example.com/test", &sessions.MockSessionStore{LoadError: errors.New("ok")}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusInternalServerError},
// redirect to start auth process
{"unknown host", "https://notcorp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, authenticator.MockAuthenticate{}, http.StatusNotFound},
{"unknown host", "https://notcorp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusNotFound},
{"user forbidden", "https://notcorp.example.com/test", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -377,6 +377,7 @@ func TestProxy_Proxy(t *testing.T) {
p.cipher = mockCipher{}
p.sessionStore = tt.session
p.AuthenticateClient = tt.authenticator
p.AuthorizeClient = tt.authorizer
r := httptest.NewRequest("GET", tt.host, nil)
w := httptest.NewRecorder()
@ -390,6 +391,8 @@ func TestProxy_Proxy(t *testing.T) {
}
func TestProxy_Authenticate(t *testing.T) {
configBlob := `[{"from":"corp.example.com","to":"example.com"}]` //valid yaml
policy := base64.URLEncoding.EncodeToString([]byte(configBlob))
goodSession := &sessions.SessionState{
User: "user",
@ -400,21 +403,21 @@ func TestProxy_Authenticate(t *testing.T) {
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(10 * time.Second),
}
testAuth := authenticator.MockAuthenticate{
testAuth := clients.MockAuthenticate{
RedeemResponse: goodSession,
}
tests := []struct {
name string
host string
mux map[string]string
mux string
session sessions.SessionStore
authenticator authenticator.Authenticator
authenticator clients.Authenticator
wantErr bool
}{
{"cannot save session",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
policy,
&sessions.MockSessionStore{Session: &sessions.SessionState{
User: "user",
Email: "email@email.com",
@ -428,11 +431,11 @@ func TestProxy_Authenticate(t *testing.T) {
{"cannot load session",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
policy,
&sessions.MockSessionStore{LoadError: errors.New("error")}, testAuth, true},
{"expired session",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
policy,
&sessions.MockSessionStore{
Session: &sessions.SessionState{
User: "user",
@ -443,7 +446,7 @@ func TestProxy_Authenticate(t *testing.T) {
LifetimeDeadline: time.Now().Add(10 * time.Second),
RefreshDeadline: time.Now().Add(-10 * time.Second),
}},
authenticator.MockAuthenticate{
clients.MockAuthenticate{
RefreshError: errors.New("error"),
RefreshResponse: &sessions.SessionState{
User: "user",
@ -456,7 +459,7 @@ func TestProxy_Authenticate(t *testing.T) {
}}, true},
{"bad refresh authenticator",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
policy,
&sessions.MockSessionStore{
Session: &sessions.SessionState{
User: "user",
@ -468,7 +471,7 @@ func TestProxy_Authenticate(t *testing.T) {
RefreshDeadline: time.Now().Add(-10 * time.Second),
},
},
authenticator.MockAuthenticate{
clients.MockAuthenticate{
RefreshError: errors.New("error"),
RefreshResponse: &sessions.SessionState{
User: "user",
@ -483,7 +486,7 @@ func TestProxy_Authenticate(t *testing.T) {
{"good",
"https://corp.example.com/",
map[string]string{"corp.example.com": "example.com"},
policy,
&sessions.MockSessionStore{Session: &sessions.SessionState{
User: "user",
Email: "email@email.com",
@ -497,7 +500,7 @@ func TestProxy_Authenticate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := testOptions()
opts.Routes = tt.mux
opts.Policy = tt.mux
p, err := New(opts)
if err != nil {
t.Fatal(err)

View file

@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"html/template"
"net"
stdlog "log"
"net/http"
"net/http/httputil"
"net/url"
@ -16,34 +16,41 @@ import (
"github.com/pomerium/pomerium/internal/cryptutil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/policy"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/templates"
"github.com/pomerium/pomerium/proxy/authenticator"
"github.com/pomerium/pomerium/proxy/clients"
)
const (
// HeaderJWT is the header key for pomerium proxy's JWT signature.
// HeaderJWT is the header key containing JWT signed user details.
HeaderJWT = "x-pomerium-jwt-assertion"
// HeaderUserID represents the header key for the user that is passed to the client.
// HeaderUserID is the header key containing the user's id.
HeaderUserID = "x-pomerium-authenticated-user-id"
// HeaderEmail represents the header key for the email that is passed to the client.
// HeaderEmail is the header key containing the user's email.
HeaderEmail = "x-pomerium-authenticated-user-email"
// HeaderGroups represents the header key for the groups that is passed to the client.
// HeaderGroups is the header key containing the user's groups.
HeaderGroups = "x-pomerium-authenticated-user-groups"
)
// Options represents the configurations available for the proxy service.
type Options struct {
Policy string `envconfig:"POLICY"`
PolicyFile string `envconfig:"POLICY_FILE"`
// Authenticate service settings
AuthenticateURL *url.URL `envconfig:"AUTHENTICATE_SERVICE_URL"`
AuthenticateInternalAddr string `envconfig:"AUTHENTICATE_INTERNAL_URL"`
// Authorize service settings
AuthorizeURL *url.URL `envconfig:"AUTHORIZE_SERVICE_URL"`
AuthorizeInternalAddr string `envconfig:"AUTHORIZE_INTERNAL_URL"`
// Settings to enable custom behind-the-ingress service communication
OverrideCertificateName string `envconfig:"OVERRIDE_CERTIFICATE_NAME"`
AuthenticatePort int `envconfig:"AUTHENTICATE_SERVICE_PORT"`
CA string `envconfig:"CERTIFICATE_AUTHORITY"`
CAFile string `envconfig:"CERTIFICATE_AUTHORITY_FILE"`
// SigningKey is a base64 encoded private key used to add a JWT-signature to proxied requests.
// See : https://www.pomerium.io/guide/signed-headers.html
// 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"`
@ -69,9 +76,7 @@ var defaultOptions = &Options{
CookieSecure: true,
CookieExpire: time.Duration(14) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
DefaultUpstreamTimeout: time.Duration(10) * time.Second,
// services
AuthenticatePort: 443,
DefaultUpstreamTimeout: time.Duration(30) * time.Second,
}
// OptionsFromEnvConfig builds the IdentityProvider service's configuration
@ -87,15 +92,36 @@ func OptionsFromEnvConfig() (*Options, error) {
// Validate checks that proper configuration settings are set to create
// a proper Proxy instance
func (o *Options) Validate() error {
if len(o.Routes) == 0 {
return errors.New("missing setting: routes")
if len(o.Routes) != 0 {
return errors.New("routes setting is deprecated, use policy instead")
}
for to, from := range o.Routes {
if _, err := urlParse(to); err != nil {
return fmt.Errorf("could not parse origin %s as url : %q", to, err)
if o.Policy == "" && o.PolicyFile == "" {
return errors.New("proxy: either `POLICY` or `POLICY_FILE` must be non-nil")
}
if _, err := urlParse(from); err != nil {
return fmt.Errorf("could not parse destination %s as url : %q", to, err)
var policies []policy.Policy
var err error
if o.Policy != "" {
confBytes, err := base64.StdEncoding.DecodeString(o.Policy)
if err != nil {
return fmt.Errorf("proxy: `POLICY` is invalid base64 %v", err)
}
policies, err = policy.FromConfig(confBytes)
if err != nil {
return fmt.Errorf("proxy: `POLICY` %v", err)
}
}
if o.PolicyFile != "" {
policies, err = policy.FromConfigFile(o.PolicyFile)
if err != nil {
return fmt.Errorf("proxy: `POLICY_FILE` %v", err)
}
}
for _, p := range policies {
if _, err := urlParse(p.To); err != nil {
return fmt.Errorf("could not parse source %s url: %v", p.To, err)
}
if _, err := urlParse(p.From); err != nil {
return fmt.Errorf("could not parse destination %s url: %v", p.From, err)
}
}
if o.AuthenticateURL == nil {
@ -104,6 +130,12 @@ func (o *Options) Validate() error {
if o.AuthenticateURL.Scheme != "https" {
return errors.New("authenticate-service-url must be a valid https url")
}
if o.AuthorizeURL == nil {
return errors.New("missing setting: authorize-service-url")
}
if o.AuthorizeURL.Scheme != "https" {
return errors.New("authorize-service-url must be a valid https url")
}
if o.CookieSecret == "" {
return errors.New("missing setting: cookie-secret")
}
@ -130,9 +162,12 @@ func (o *Options) Validate() error {
type Proxy struct {
SharedKey string
// services
// authenticate service
AuthenticateURL *url.URL
AuthenticateClient authenticator.Authenticator
AuthenticateClient clients.Authenticator
// authorize service
AuthorizeClient clients.Authorizer
// session
cipher cryptutil.Cipher
@ -146,8 +181,6 @@ type Proxy struct {
// New takes a Proxy service from options and a validation function.
// Function returns an error if options fail to validate.
//
// Caller responsible for closing AuthenticateConn.
func New(opts *Options) (*Proxy, error) {
if opts == nil {
return nil, errors.New("options cannot be nil")
@ -188,34 +221,48 @@ func New(opts *Options) (*Proxy, error) {
redirectURL: &url.URL{Path: "/.pomerium/callback"},
templates: templates.New(),
}
for from, to := range opts.Routes {
fromURL, _ := urlParse(from)
toURL, _ := urlParse(to)
reverseProxy := NewReverseProxy(toURL)
handler, err := NewReverseProxyHandler(opts, reverseProxy, fromURL.Host, toURL.Host)
var policies []policy.Policy
if opts.Policy != "" {
confBytes, _ := base64.StdEncoding.DecodeString(opts.Policy)
policies, _ = policy.FromConfig(confBytes)
} else {
policies, _ = policy.FromConfigFile(opts.PolicyFile)
}
for _, route := range policies {
proxy := NewReverseProxy(route.Destination)
handler, err := NewReverseProxyHandler(opts, proxy, &route)
if err != nil {
return nil, err
}
p.Handle(fromURL.Host, handler)
log.Info().Str("from", fromURL.Host).Str("to", toURL.String()).Msg("proxy: new route")
p.Handle(route.Source.Host, handler)
log.Info().Str("src", route.Source.Host).Str("dst", route.Destination.Host).Msg("proxy: new route")
}
p.AuthenticateClient, err = authenticator.New(
"grpc",
&authenticator.Options{
p.AuthenticateClient, err = clients.NewAuthenticateClient("grpc",
&clients.Options{
Addr: opts.AuthenticateURL.Host,
InternalAddr: opts.AuthenticateInternalAddr,
OverrideCertificateName: opts.OverrideCertificateName,
SharedSecret: opts.SharedKey,
Port: opts.AuthenticatePort,
CA: opts.CA,
CAFile: opts.CAFile,
})
return p, nil
if err != nil {
return nil, err
}
p.AuthorizeClient, err = clients.NewAuthorizeClient("grpc",
&clients.Options{
Addr: opts.AuthorizeURL.Host,
InternalAddr: opts.AuthorizeInternalAddr,
OverrideCertificateName: opts.OverrideCertificateName,
SharedSecret: opts.SharedKey,
CA: opts.CA,
CAFile: opts.CAFile,
})
return p, err
}
// UpstreamProxy stores information necessary for proxying the request back to the upstream.
// UpstreamProxy stores information for proxying the request to the upstream.
type UpstreamProxy struct {
name string
cookieName string
@ -223,19 +270,6 @@ type UpstreamProxy struct {
signer cryptutil.JWTSigner
}
var defaultUpstreamTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 30 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// deleteUpstreamCookies deletes the session cookie from the request header string.
func deleteUpstreamCookies(req *http.Request, cookieName string) {
headers := []string{}
@ -271,8 +305,10 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// base path provided in target. NewReverseProxy rewrites the Host header.
func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(to)
proxy.Transport = defaultUpstreamTransport
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
@ -285,36 +321,36 @@ func NewReverseProxy(to *url.URL) *httputil.ReverseProxy {
}
// NewReverseProxyHandler applies handler specific options to a given route.
func NewReverseProxyHandler(opts *Options, reverseProxy *httputil.ReverseProxy, from, to string) (http.Handler, error) {
func NewReverseProxyHandler(o *Options, proxy *httputil.ReverseProxy, route *policy.Policy) (http.Handler, error) {
up := &UpstreamProxy{
name: to,
handler: reverseProxy,
cookieName: opts.CookieName,
name: route.Destination.Host,
handler: proxy,
cookieName: o.CookieName,
}
if len(opts.SigningKey) != 0 {
decodedSigningKey, err := base64.StdEncoding.DecodeString(opts.SigningKey)
if len(o.SigningKey) != 0 {
decodedSigningKey, err := base64.StdEncoding.DecodeString(o.SigningKey)
if err != nil {
return nil, err
}
signer, err := cryptutil.NewES256Signer(decodedSigningKey, from)
signer, err := cryptutil.NewES256Signer(decodedSigningKey, route.Source.Host)
if err != nil {
return nil, err
}
up.signer = signer
}
timeout := opts.DefaultUpstreamTimeout
timeoutMsg := fmt.Sprintf("%s failed to respond within the %s timeout period", to, timeout)
timeout := o.DefaultUpstreamTimeout
if route.UpstreamTimeout != 0 {
timeout = route.UpstreamTimeout
}
timeoutMsg := fmt.Sprintf("%s failed to respond within the %s timeout period", route.Destination.Host, timeout)
return http.TimeoutHandler(up, timeout, timeoutMsg), nil
}
// urlParse adds a scheme if none-exists, addressesing a quirk in how
// one may expect url.Parse to function when given scheme-less domain is provided.
//
// see: https://github.com/golang/go/issues/12585
// see: https://golang.org/pkg/net/url/#Parse
// 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.Parse(uri)
return url.ParseRequestURI(uri)
}

View file

@ -1,6 +1,7 @@
package proxy // import "github.com/pomerium/pomerium/proxy"
import (
"encoding/base64"
"io/ioutil"
"net"
"net/http"
@ -10,6 +11,8 @@ import (
"reflect"
"testing"
"time"
"github.com/pomerium/pomerium/internal/policy"
)
var fixedDate = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
@ -47,36 +50,9 @@ func TestOptionsFromEnvConfig(t *testing.T) {
}
}
func Test_urlParse(t *testing.T) {
os.Clearenv()
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 TestNewReverseProxy(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.WriteHeader(http.StatusOK)
hostname, _, _ := net.SplitHostPort(r.Host)
w.Write([]byte(hostname))
}))
@ -101,7 +77,7 @@ func TestNewReverseProxy(t *testing.T) {
func TestNewReverseProxyHandler(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.WriteHeader(http.StatusOK)
hostname, _, _ := net.SplitHostPort(r.Host)
w.Write([]byte(hostname))
}))
@ -111,10 +87,14 @@ func TestNewReverseProxyHandler(t *testing.T) {
backendHostname, backendPort, _ := net.SplitHostPort(backendURL.Host)
backendHost := net.JoinHostPort(backendHostname, backendPort)
proxyURL, _ := url.Parse(backendURL.Scheme + "://" + backendHost + "/")
proxyHandler := NewReverseProxy(proxyURL)
opts := defaultOptions
handle, err := NewReverseProxyHandler(opts, proxyHandler, "from", "to")
opts.SigningKey = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="
route, err := policy.FromConfig([]byte(`[{"from":"corp.example.com","to":"example.com","timeout":"1s"}]`))
if err != nil {
t.Fatal(err)
}
handle, err := NewReverseProxyHandler(opts, proxyHandler, &route[0])
if err != nil {
t.Errorf("got %q", err)
}
@ -133,10 +113,14 @@ func TestNewReverseProxyHandler(t *testing.T) {
}
func testOptions() *Options {
authurl, _ := url.Parse("https://sso-auth.corp.beyondperimeter.com")
authenticateService, _ := url.Parse("https://authenticate.corp.beyondperimeter.com")
authorizeService, _ := url.Parse("https://authorize.corp.beyondperimeter.com")
configBlob := `[{"from":"corp.example.com","to":"example.com"}]` //valid yaml
policy := base64.URLEncoding.EncodeToString([]byte(configBlob))
return &Options{
Routes: map[string]string{"corp.example.com": "example.com"},
AuthenticateURL: authurl,
Policy: policy,
AuthenticateURL: authenticateService,
AuthorizeURL: authorizeService,
SharedKey: "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ=",
CookieSecret: "OromP1gurwGWjQPYb1nNgSxtbVB5NnLzX6z5WOKr0Yw=",
CookieName: "pomerium",
@ -151,7 +135,7 @@ func TestOptions_Validate(t *testing.T) {
badToRoute.Routes = map[string]string{"^": "example.com"}
badAuthURL := testOptions()
badAuthURL.AuthenticateURL = nil
authurl, _ := url.Parse("http://sso-auth.corp.beyondperimeter.com")
authurl, _ := url.Parse("http://authenticate.corp.beyondperimeter.com")
httpAuthURL := testOptions()
httpAuthURL.AuthenticateURL = authurl
emptyCookieSecret := testOptions()
@ -193,6 +177,7 @@ func TestOptions_Validate(t *testing.T) {
}
func TestNew(t *testing.T) {
good := testOptions()
shortCookieLength := testOptions()
shortCookieLength.CookieSecret = "gN3xnvfsAwfCXxnJorGLKUG4l2wC8sS8nfLMhcStPg=="
@ -207,7 +192,7 @@ func TestNew(t *testing.T) {
numMuxes int
wantErr bool
}{
{"good - minimum options", good, nil, true, 1, false},
{"good", good, nil, true, 1, false},
{"empty options", &Options{}, nil, false, 0, true},
{"nil options", nil, nil, false, 0, true},
{"short secret/validate sanity check", shortCookieLength, nil, false, 0, true},
@ -224,7 +209,7 @@ func TestNew(t *testing.T) {
t.Errorf("New() expected valid proxy struct")
}
if got != nil && len(got.mux) != tt.numMuxes {
t.Errorf("New() = num muxes %v, want %v", got, tt.numMuxes)
t.Errorf("New() = num muxes \n%+v, want \n%+v", got, tt.numMuxes)
}
})
}

View file

@ -30,6 +30,9 @@ kubectl create ns pomerium
# kubectl apply -f docs/docs/examples/kubernetes/issuer.le.stage.yml
# kubectl get certificate
# add our policy
kubectl create configmap -n pomerium policy --from-literal=POLICY=$(cat policy.example.yaml | base64)
# create our cryptographically random keys
kubectl create secret generic -n pomerium shared-secret --from-literal=shared-secret=$(head -c32 /dev/urandom | base64)
kubectl create secret generic -n pomerium cookie-secret --from-literal=cookie-secret=$(head -c32 /dev/urandom | base64)
@ -47,11 +50,14 @@ kubectl create secret tls -n pomerium pomerium-tls --key privkey.pem --cert cert
# kubectl create secret generic -n pomerium idp-client-secret --from-literal=REPLACE_ME
# Create the proxy & authenticate deployment
kubectl apply -f docs/docs/examples/kubernetes/authorize.deploy.yml
kubectl apply -f docs/docs/examples/kubernetes/authenticate.deploy.yml
kubectl apply -f docs/docs/examples/kubernetes/proxy.deploy.yml
# Create the proxy & authenticate services
kubectl apply -f docs/docs/examples/kubernetes/proxy.service.yml
kubectl apply -f docs/docs/examples/kubernetes/authenticate.service.yml
kubectl apply -f docs/docs/examples/kubernetes/authorize.service.yml
# Create and apply the Ingress; this is GKE specific
kubectl apply -f docs/docs/examples/kubernetes/ingress.yml