diff --git a/docs/reference/readme.md b/docs/reference/readme.md index 40d486763..fd90a885d 100644 --- a/docs/reference/readme.md +++ b/docs/reference/readme.md @@ -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` diff --git a/internal/policy/policy.go b/internal/policy/policy.go index 2fc92cb2a..d194e26de 100644 --- a/internal/policy/policy.go +++ b/internal/policy/policy.go @@ -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 diff --git a/policy.example.yaml b/policy.example.yaml index 918b6b699..0367fb0f1 100644 --- a/policy.example.yaml +++ b/policy.example.yaml @@ -18,4 +18,7 @@ - from: hello.corp.beyondperimeter.com to: http://hello:8080 allowed_groups: - - admins \ No newline at end of file + - admins +- from: external-search.corp.beyondperimeter.com + to: google.com + allow_public_unauthenticated_access: true \ No newline at end of file diff --git a/proxy/handlers.go b/proxy/handlers.go index 44c84f1bc..5bda55119 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -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 } diff --git a/proxy/handlers_test.go b/proxy/handlers_test.go index 6b5c0cdc1..4d2d01d66 100644 --- a/proxy/handlers_test.go +++ b/proxy/handlers_test.go @@ -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) diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 81cbb0d94..54d596797 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -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) {