Merge pull request #134 from nareddyt/unauthenticated-routes

proxy: support for public unauthenticated routes
This commit is contained in:
Bobby DeSimone 2019-05-22 20:29:39 -07:00 committed by GitHub
commit 702cc30b77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 60 additions and 3 deletions

View file

@ -185,6 +185,18 @@ Allowed domains is a collection of whitelisted domains to authorize for a given
Allow unauthenticated HTTP OPTIONS requests as [per the CORS spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests).
#### Public Access
- `yaml`/`json` setting: `allow_public_unauthenticated_access`
- Type: `bool`
- Optional
- Default: `false`
**Use with caution:** Allow all requests for a given route, bypassing authentication and authorization.
Suitable for publicly exposed web services.
If this setting is enabled, no whitelists (e.g. Allowed Users) should be provided in this route.
#### Route Timeout
- `yaml`/`json` setting: `timeout`

View file

@ -1,6 +1,7 @@
package policy // import "github.com/pomerium/pomerium/internal/policy"
import (
"errors"
"fmt"
"io/ioutil"
"net/url"
@ -29,6 +30,9 @@ type Policy struct {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests
CORSAllowPreflight bool `yaml:"cors_allow_preflight"`
// Allow any public request to access this route. **Bypasses authentication**
AllowPublicUnauthenticatedAccess bool `yaml:"allow_public_unauthenticated_access"`
// UpstreamTimeout is the route specific timeout. Must be less than the global
// timeout. If unset, route will fallback to the proxy's DefaultUpstreamTimeout.
UpstreamTimeout time.Duration `yaml:"timeout"`
@ -39,10 +43,17 @@ func (p *Policy) validate() (err error) {
if err != nil {
return err
}
p.Destination, err = urlParse(p.To)
if err != nil {
return err
}
// Only allow public access if no other whitelists are in place
if p.AllowPublicUnauthenticatedAccess && (p.AllowedDomains != nil || p.AllowedGroups != nil || p.AllowedEmails != nil) {
return errors.New("route marked as public but contains whitelists")
}
return nil
}
@ -56,7 +67,7 @@ func FromConfig(confBytes []byte) ([]Policy, error) {
// build source and destination urls
for i := range f {
if err := (&f[i]).validate(); err != nil {
return nil, err
return nil, fmt.Errorf("route at index %d: %v", i, err)
}
}
return f, nil

View file

@ -18,4 +18,7 @@
- from: hello.corp.beyondperimeter.com
to: http://hello:8080
allowed_groups:
- admins
- admins
- from: external-search.corp.beyondperimeter.com
to: google.com
allow_public_unauthenticated_access: true

View file

@ -180,11 +180,17 @@ func (p *Proxy) OAuthCallback(w http.ResponseWriter, r *http.Request) {
// Conditions should be few in number and have strong justifications.
func (p *Proxy) shouldSkipAuthentication(r *http.Request) bool {
pol, foundPolicy := p.policy(r)
if isCORSPreflight(r) && foundPolicy && pol.CORSAllowPreflight {
log.FromRequest(r).Debug().Msg("proxy: skipping authentication for valid CORS preflight request")
return true
}
if foundPolicy && pol.AllowPublicUnauthenticatedAccess {
log.FromRequest(r).Debug().Msg("proxy: skipping authentication for public route")
return true
}
return false
}

View file

@ -343,6 +343,8 @@ func TestProxy_Proxy(t *testing.T) {
opts := testOptionsTestServer(ts.URL)
optsCORS := testOptionsWithCORS(ts.URL)
optsPublic := testOptionsWithPublicAccess(ts.URL)
defaultHeaders, goodCORSHeaders, badCORSHeaders := http.Header{}, http.Header{}, http.Header{}
goodCORSHeaders.Set("origin", "anything")
goodCORSHeaders.Set("access-control-request-method", "anything")
@ -360,7 +362,6 @@ func TestProxy_Proxy(t *testing.T) {
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", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK},
{"good cors preflight", optsCORS, http.MethodOptions, goodCORSHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
// same request as above, but with cors_allow_preflight=false in the policy
@ -377,7 +378,10 @@ func TestProxy_Proxy(t *testing.T) {
{"failed refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{RefreshError: errors.New("refresh error")}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden},
{"cannot resave refreshed session", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{SaveError: errors.New("weird"), Session: &sessions.SessionState{RefreshDeadline: time.Now().Add(-10 * time.Second)}}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden},
{"authenticate validation error", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: false}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusForbidden},
{"public access", optsPublic, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusOK},
{"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusForbidden},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := New(tt.options)

View file

@ -120,6 +120,21 @@ func testOptionsWithCORS(uri string) *config.Options {
return opts
}
func testOptionsWithPublicAccess(uri string) *config.Options {
configBlob := fmt.Sprintf(`[{"from":"httpbin.corp.example","to":"%s","allow_public_unauthenticated_access":true}]`, uri)
opts := testOptions()
opts.Policy = base64.URLEncoding.EncodeToString([]byte(configBlob))
return opts
}
func testOptionsWithPublicAccessAndWhitelist(uri string) *config.Options {
configBlob := fmt.Sprintf(`[{"from":"httpbin.corp.example","to":"%s","allow_public_unauthenticated_access":true,"allowed_users":["test@gmail.com"]}]`, uri)
opts := testOptions()
opts.Policy = base64.URLEncoding.EncodeToString([]byte(configBlob))
return opts
}
func TestOptions_Validate(t *testing.T) {
good := testOptions()
badFromRoute := testOptions()
@ -151,6 +166,9 @@ func TestOptions_Validate(t *testing.T) {
badPolicyToURL.Policy = "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbl4KICBhbGxvd2VkX2RvbWFpbnM6CiAgICAtIHBvbWVyaXVtLmlv"
badPolicyFromURL := testOptions()
badPolicyFromURL.Policy = "LSBmcm9tOiBodHRwYmluLmNvcnAuYmV5b25kcGVyaW1ldGVyLmNvbQogIHRvOiBodHRwOi8vaHR0cGJpbl4KICBhbGxvd2VkX2RvbWFpbnM6CiAgICAtIHBvbWVyaXVtLmlv"
corsPolicy := testOptionsWithCORS("example.notatld")
publicPolicy := testOptionsWithPublicAccess("example.notatld")
publicWithWhitelistPolicy := testOptionsWithPublicAccessAndWhitelist("example.notatld")
tests := []struct {
name string
@ -173,6 +191,9 @@ func TestOptions_Validate(t *testing.T) {
{"policy invalid base64", policyBadBase64, true},
{"policy bad to url", badPolicyFromURL, true},
{"policy bad from url", badPolicyFromURL, true},
{"CORS policy good", corsPolicy, false},
{"policy public good", publicPolicy, false},
{"policy public and whitelist bad", publicWithWhitelistPolicy, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {