mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-28 18:06:34 +02:00
Merge pull request #134 from nareddyt/unauthenticated-routes
proxy: support for public unauthenticated routes
This commit is contained in:
commit
702cc30b77
6 changed files with 60 additions and 3 deletions
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue