mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-10 07:37:33 +02:00
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:
parent
1187be2bf3
commit
c13459bb88
65 changed files with 1683 additions and 879 deletions
26
.codecov.yml
Normal file
26
.codecov.yml
Normal 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
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
.docker-compose.yml
|
||||
.*.yml
|
||||
pem
|
||||
env
|
||||
coverage.txt
|
||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
107
authorize/authorize.go
Normal 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)
|
||||
}
|
93
authorize/authorize_test.go
Normal file
93
authorize/authorize_test.go
Normal 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
18
authorize/gprc.go
Normal 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
39
authorize/gprc_test.go
Normal 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
127
authorize/identity.go
Normal 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 }
|
62
authorize/identity_test.go
Normal file
62
authorize/identity_test.go
Normal 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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -64,6 +64,7 @@ func isValidService(service string) bool {
|
|||
case
|
||||
"all",
|
||||
"proxy",
|
||||
"authorize",
|
||||
"authenticate":
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|||
|
||||

|
||||
|
||||
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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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].
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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].
|
||||
|
||||
|
|
14
env.example
14
env.example
|
@ -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
7
go.mod
|
@ -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
22
go.sum
|
@ -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=
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestPasswordHashing(t *testing.T) {
|
||||
t.Parallel()
|
||||
bcryptTests := []struct {
|
||||
plaintext []byte
|
||||
hash []byte
|
||||
|
|
|
@ -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-----
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestES256Signer(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewES256Signer(t *testing.T) {
|
||||
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
privKey []byte
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
83
internal/policy/policy.go
Normal 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)
|
||||
}
|
92
internal/policy/policy_test.go
Normal file
92
internal/policy/policy_test.go
Normal 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
6
internal/policy/testdata/basic.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"from": "pomerium.io",
|
||||
"to": "httpbin.org"
|
||||
}
|
||||
]
|
2
internal/policy/testdata/basic.yaml
vendored
Normal file
2
internal/policy/testdata/basic.yaml
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
- from: pomerium.io
|
||||
to: httpbin.org
|
|
@ -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
19
policy.example.yaml
Normal 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
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
221
proto/authorize/authorize.pb.go
Normal file
221
proto/authorize/authorize.pb.go
Normal 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,
|
||||
}
|
18
proto/authorize/authorize.proto
Normal file
18
proto/authorize/authorize.proto
Normal 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; }
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
})
|
64
proxy/clients/authorize_client.go
Normal file
64
proxy/clients/authorize_client.go
Normal 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()
|
||||
}
|
47
proxy/clients/authorize_client_test.go
Normal file
47
proxy/clients/authorize_client_test.go
Normal 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
106
proxy/clients/clients.go
Normal 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),
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
170
proxy/proxy.go
170
proxy/proxy.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue