Merge pull request #615 from pomerium/cdoxsey/policy-path

implement path-based route matching
This commit is contained in:
Caleb Doxsey 2020-04-21 08:27:50 -06:00 committed by GitHub
commit 45c706666c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 549 additions and 32 deletions

View file

@ -33,8 +33,12 @@ jobs:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Build dependencies
run: make build-deps
- name: Test
run: make test
run: |
export PATH=$PATH:$(go env GOPATH)/bin
make test
cover:
strategy:

View file

@ -27,8 +27,10 @@ CTIMEVAR=-X $(PKG)/internal/version.GitCommit=$(GITCOMMIT) \
-X $(PKG)/internal/version.ProjectURL=$(PKG)
GO_LDFLAGS=-ldflags "-s -w $(CTIMEVAR)"
GOOSARCHES = linux/amd64 darwin/amd64 windows/amd64
GOLANGCI_VERSION = v1.21.0
MISSPELL_VERSION = v0.3.4
GOLANGCI_VERSION = v1.21.0
OPA_VERSION = v0.19.1
.PHONY: all
all: clean build-deps test lint spellcheck build ## Runs a clean, build, fmt, lint, test, and vet.
@ -36,8 +38,9 @@ all: clean build-deps test lint spellcheck build ## Runs a clean, build, fmt, li
.PHONY: build-deps
build-deps: ## Install build dependencies
@echo "==> $@"
@cd /tmp; GO111MODULE=on go get -u github.com/client9/misspell/cmd/misspell
@cd /tmp; GO111MODULE=on go get github.com/client9/misspell/cmd/misspell@${MISSPELL_VERSION}
@cd /tmp; GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_VERSION}
@cd /tmp; GO111MODULE=on go get github.com/open-policy-agent/opa@${OPA_VERSION}
.PHONY: docs
docs: ## Start the vuepress docs development server
@ -68,6 +71,7 @@ lint: ## Verifies `golint` passes.
test: ## Runs the go tests.
@echo "==> $@"
@go test -tags "$(BUILDTAGS)" $(shell go list ./... | grep -v vendor)
@opa test ./authorize/evaluator/opa/policy
.PHONY: spellcheck
spellcheck: # Spellcheck docs

View file

@ -91,9 +91,11 @@ func Test_Eval(t *testing.T) {
}
req := struct {
Host string `json:"host,omitempty"`
URL string `json:"url,omitempty"`
User string `json:"user,omitempty"`
}{
Host: tt.route,
URL: "https://" + tt.route,
User: rawJWT,
}
got, err := pe.IsAuthorized(context.TODO(), req)

View file

@ -8,7 +8,7 @@ default allow = false
# allow by email
allow {
some route
input.host = route_policies[route].source
allowed_route(input.url, route_policies[route])
token.payload.email = route_policies[route].allowed_users[_]
token.valid
count(deny)==0
@ -17,8 +17,9 @@ allow {
# allow group
allow {
some route
input.host = route_policies[route].source
token.payload.groups[_] = route_policies[route].allowed_groups[_]
allowed_route(input.url, route_policies[route])
some group
token.payload.groups[group] == route_policies[route].allowed_groups[_]
token.valid
count(deny)==0
}
@ -26,7 +27,7 @@ allow {
# allow by impersonate email
allow {
some route
input.host = route_policies[route].source
allowed_route(input.url, route_policies[route])
token.payload.impersonate_email = route_policies[route].allowed_users[_]
token.valid
count(deny)==0
@ -35,8 +36,9 @@ allow {
# allow by impersonate group
allow {
some route
input.host = route_policies[route].source
token.payload.impersonate_groups[_] = route_policies[route].allowed_groups[_]
allowed_route(input.url, route_policies[route])
some group
token.payload.impersonate_groups[group] == route_policies[route].allowed_groups[_]
token.valid
count(deny)==0
}
@ -44,7 +46,7 @@ allow {
# allow by domain
allow {
some route
input.host = route_policies[route].source
allowed_route(input.url, route_policies[route])
some domain
email_in_domain(token.payload.email, route_policies[route].allowed_domains[domain])
token.valid
@ -54,13 +56,68 @@ allow {
# allow by impersonate domain
allow {
some route
input.host = route_policies[route].source
allowed_route(input.url, route_policies[route])
some domain
email_in_domain(token.payload.impersonate_email, route_policies[route].allowed_domains[domain])
token.valid
count(deny)==0
}
allowed_route(input_url, policy){
input_url_obj := parse_url(input_url)
allowed_route_source(input_url_obj, policy)
allowed_route_prefix(input_url_obj, policy)
allowed_route_path(input_url_obj, policy)
allowed_route_regex(input_url_obj, policy)
}
allowed_route_source(input_url_obj, policy) {
object.get(policy, "source", "") == ""
}
allowed_route_source(input_url_obj, policy) {
object.get(policy, "source", "") != ""
source_url_obj := parse_url(policy.source)
input_url_obj.host == source_url_obj.host
}
allowed_route_prefix(input_url_obj, policy) {
object.get(policy, "prefix", "") == ""
}
allowed_route_prefix(input_url_obj, policy) {
object.get(policy, "prefix", "") != ""
startswith(input_url_obj.path, policy.prefix)
}
allowed_route_path(input_url_obj, policy) {
object.get(policy, "path", "") == ""
}
allowed_route_path(input_url_obj, policy) {
object.get(policy, "path", "") != ""
policy.path == input_url_obj.path
}
allowed_route_regex(input_url_obj, policy) {
object.get(policy, "regex", "") == ""
}
allowed_route_regex(input_url_obj, policy) {
object.get(policy, "regex", "") != ""
re_match(policy.regex, input_url_obj.path)
}
parse_url(str) = { "scheme": scheme, "host": host, "path": path } {
[_, scheme, host, rawpath] = regex.find_all_string_submatch_n(
`(?:(http[s]?)://)?([^/]+)([^?#]*)`,
str, 1)[0]
path = normalize_url_path(rawpath)
}
normalize_url_path(str) = "/" {
str == ""
}
normalize_url_path(str) = str {
str != ""
}
email_in_domain(email, domain) {
x := split(email, "@")
count(x) == 2

View file

@ -0,0 +1,102 @@
package pomerium.authz
jwt_header := {
"typ": "JWT",
"alg": "HS256"
}
signing_key := {
"kty": "oct",
"k": "OkFmqMK9U0dmPhMCW0VYy6D_raJKwEJsMdxqdnukThzko3D_XrsihwYE0pxrUSpm0JTrW2QpIz4rT1vdEvZw67WP4xrqjiwyd7PgpPTD5xvQBM7TIKiSW0X2R0pfq_OItszPQRtb7VirrSbGJiLNS-NJMMrYVKWWtUbVSTXEjL7VcFqML5PiSe7XDmyCZjpgEpfE5Q82zIeXM2sLrz6HW2A9IwGk7mWS0c57R_2JGyFO2tCA4zEIYhWvLE62Os2tZ6YrrwdB8n35jlPpgUE6poEvIU20lPLaocozXYMqAku-KJnloJlAzKg2Xa_0iSiSgSAumx44B3n7DQjg3jPhRg"
}
shared_key := base64url.decode(signing_key.k)
test_email_allowed {
user := io.jwt.encode_sign(jwt_header, {
"aud": ["example.com"],
"email": "joe@example.com"
}, signing_key)
allow with data.route_policies as [{
"source": "example.com",
"allowed_users": ["joe@example.com"]
}] with data.signing_key as signing_key with data.shared_key as shared_key with input as {
"url": "http://example.com",
"host": "example.com",
"user": user
}
}
test_example {
user := io.jwt.encode_sign(jwt_header, {
"aud": ["example.com"],
"email": "joe@example.com"
}, signing_key)
not allow with data.route_policies as [
{
"source": "http://example.com",
"path": "/a",
"allowed_domains": ["example.com"]
},
{
"source": "http://example.com",
"path": "/b",
"allowed_users": ["noone@pomerium.com"]
},
] with data.signing_key as signing_key with data.shared_key as shared_key with input as {
"url": "http://example.com/b",
"host": "example.com",
"user": user
}
}
test_email_denied {
user := io.jwt.encode_sign(jwt_header, {
"aud": ["example.com"],
"email": "joe@example.com"
}, signing_key)
not allow with data.route_policies as [{
"source": "example.com",
"allowed_users": ["bob@example.com"]
}] with data.signing_key as signing_key with data.shared_key as shared_key with input as {
"url": "http://example.com",
"host": "example.com",
"user": user
}
}
test_parse_url {
url := parse_url("http://example.com/some/path?qs")
url.scheme == "http"
url.host == "example.com"
url.path == "/some/path"
}
test_allowed_route_source {
allowed_route("http://example.com", {"source": "example.com"})
allowed_route("http://example.com", {"source": "http://example.com"})
allowed_route("http://example.com", {"source": "https://example.com"})
allowed_route("http://example.com/", {"source": "https://example.com"})
allowed_route("http://example.com", {"source": "https://example.com/"})
allowed_route("http://example.com/", {"source": "https://example.com/"})
not allowed_route("http://example.org", {"source": "example.com"})
}
test_allowed_route_prefix {
allowed_route("http://example.com", {"prefix": "/"})
allowed_route("http://example.com/admin/somepath", {"prefix": "/admin"})
not allowed_route("http://example.com", {"prefix": "/admin"})
}
test_allowed_route_path {
allowed_route("http://example.com", {"path": "/"})
allowed_route("http://example.com/", {"path": "/"})
not allowed_route("http://example.com/admin/somepath", {"path": "/admin"})
not allowed_route("http://example.com", {"path": "/admin"})
}
test_allowed_route_regex {
allowed_route("http://example.com", {"regex": ".*"})
allowed_route("http://example.com/admin/somepath", {"regex": "/admin/.*"})
not allowed_route("http://example.com", {"regex": "[xyz]"})
}

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,7 @@ package authorize
import (
"context"
"net/url"
"github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/internal/grpc/authorize"
@ -22,7 +23,7 @@ func (a *Authorize) IsAuthorized(ctx context.Context, in *authorize.IsAuthorized
Method: in.GetRequestMethod(),
RequestURI: in.GetRequestRequestUri(),
RemoteAddr: in.GetRequestRemoteAddr(),
URL: in.GetRequestUrl(),
URL: getFullURL(in.GetRequestUrl(), in.GetRequestHost()),
}
return a.pe.IsAuthorized(ctx, req)
}
@ -38,3 +39,17 @@ func cloneHeaders(in protoHeader) map[string][]string {
}
return out
}
func getFullURL(rawurl, host string) string {
u, err := url.Parse(rawurl)
if err != nil {
u = &url.URL{Path: rawurl}
}
if u.Host == "" {
u.Host = host
}
if u.Scheme == "" {
u.Scheme = "http"
}
return u.String()
}

View file

@ -47,3 +47,19 @@ func TestAuthorize_IsAuthorized(t *testing.T) {
})
}
}
func Test_getFullURL(t *testing.T) {
tests := []struct {
rawurl, host, expect string
}{
{"https://www.example.com/admin", "", "https://www.example.com/admin"},
{"https://www.example.com/admin", "example.com", "https://www.example.com/admin"},
{"/admin", "example.com", "http://example.com/admin"},
}
for _, tt := range tests {
actual := getFullURL(tt.rawurl, tt.host)
if actual != tt.expect {
t.Errorf("expected getFullURL(%s, %s) to be %s, but got %s", tt.rawurl, tt.host, tt.expect, actual)
}
}
}

View file

@ -152,7 +152,7 @@ func Test_parsePolicyFile(t *testing.T) {
want []Policy
wantErr bool
}{
{"simple json", []byte(fmt.Sprintf(`{"policy":[{"from": "%s","to":"%s"}]}`, source, dest)), []Policy{{From: source, To: dest, Source: &HostnameURL{sourceURL}, Destination: destURL}}, false},
{"simple json", []byte(fmt.Sprintf(`{"policy":[{"from": "%s","to":"%s"}]}`, source, dest)), []Policy{{From: source, To: dest, Source: &StringURL{sourceURL}, Destination: destURL}}, false},
{"bad from", []byte(`{"policy":[{"from": "%","to":"httpbin.org"}]}`), nil, true},
{"bad to", []byte(`{"policy":[{"from": "pomerium.io","to":"%"}]}`), nil, true},
}

View file

@ -21,8 +21,13 @@ type Policy struct {
AllowedGroups []string `mapstructure:"allowed_groups" yaml:"allowed_groups,omitempty" json:"allowed_groups,omitempty"`
AllowedDomains []string `mapstructure:"allowed_domains" yaml:"allowed_domains,omitempty" json:"allowed_domains,omitempty"`
Source *HostnameURL `yaml:",omitempty" json:"source,omitempty"`
Destination *url.URL `yaml:",omitempty" json:"destination,omitempty"`
Source *StringURL `yaml:",omitempty" json:"source,omitempty"`
Destination *url.URL `yaml:",omitempty" json:"destination,omitempty"`
// Additional route matching options
Prefix string `mapstructure:"prefix" yaml:"prefix,omitempty" json:"prefix,omitempty"`
Path string `mapstructure:"path" yaml:"path,omitempty" json:"path,omitempty"`
Regex string `mapstructure:"regex" yaml:"regex,omitempty" json:"regex,omitempty"`
// Allow unauthenticated HTTP OPTIONS requests as per the CORS spec
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests
@ -85,7 +90,14 @@ func (p *Policy) Validate() error {
if err != nil {
return fmt.Errorf("config: policy bad source url %w", err)
}
p.Source = &HostnameURL{source}
// Make sure there's no path set on the from url
if !(source.Path == "" || source.Path == "/") {
return fmt.Errorf("config: policy source url (%s) contains a path, but it should be set using the path field instead",
source.String())
}
p.Source = &StringURL{source}
p.Destination, err = urlutil.ParseAndValidateURL(p.To)
if err != nil {
@ -135,13 +147,12 @@ func (p *Policy) String() string {
return fmt.Sprintf("%s → %s", p.Source.String(), p.Destination.String())
}
// HostnameURL wraps url but marshals only the host representation of that
// url struct.
type HostnameURL struct {
// StringURL stores a URL as a string in json.
type StringURL struct {
*url.URL
}
// MarshalJSON returns the URLs host as json.
func (j *HostnameURL) MarshalJSON() ([]byte, error) {
return json.Marshal(j.Host)
func (u *StringURL) MarshalJSON() ([]byte, error) {
return json.Marshal(u.String())
}

View file

@ -20,6 +20,7 @@ func Test_PolicyValidate(t *testing.T) {
{"empty from host", Policy{From: "https://", To: "https://httpbin.corp.example"}, true},
{"empty from scheme", Policy{From: "httpbin.corp.example", To: "https://httpbin.corp.example"}, true},
{"empty to scheme", Policy{From: "https://httpbin.corp.example", To: "//httpbin.corp.example"}, true},
{"path in from", Policy{From: "https://httpbin.corp.example/some/path", To: "https://httpbin.corp.example"}, true},
{"cors policy", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", CORSAllowPreflight: true}, false},
{"public policy", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", AllowPublicUnauthenticatedAccess: true}, false},
{"public and whitelist", Policy{From: "https://httpbin.corp.example", To: "https://httpbin.corp.notatld", AllowPublicUnauthenticatedAccess: true, AllowedUsers: []string{"test@domain.example"}}, true},
@ -57,8 +58,8 @@ func TestPolicy_String(t *testing.T) {
want string
wantFrom string
}{
{"good", "https://pomerium.io", "https://localhost", "https://pomerium.io → https://localhost", `"pomerium.io"`},
{"failed to validate", "https://pomerium.io", "localhost", "https://pomerium.io → localhost", `"pomerium.io"`},
{"good", "https://pomerium.io", "https://localhost", "https://pomerium.io → https://localhost", `"https://pomerium.io"`},
{"failed to validate", "https://pomerium.io", "localhost", "https://pomerium.io → localhost", `"https://pomerium.io"`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -753,12 +753,27 @@ Policy contains route specific settings, and access control details. If you are
<<< @/docs/configuration/examples/config/policy.example.yaml
Policy routes are checked in the order they appear in the policy, so more specific routes should appear before less specific routes. For example:
```yaml
policies:
- from: http://from.example.com
to: http://to.example.com
prefix: /admin
allowed_groups: ["superuser"]
- from: http://from.example.com
to: http://to.example.com
allow_public_unauthenticated_access: true
```
In this example an incoming request with a path prefix of `/admin` would be handled by the first route (which is restricted to superusers). All other requests for `from.example.com` would be handled by the second route (which is open to the public).
A list of policy configuration variables follows.
### From
- `yaml`/`json` setting: `from`
- Type: `URL` (must contain a scheme and hostname)
- Type: `URL` (must contain a scheme and hostname, must not contain a path)
- Required
- Example: `https://httpbin.corp.example.com`
@ -773,6 +788,33 @@ A list of policy configuration variables follows.
`To` is the destination of a proxied request. It can be an internal resource, or an external resource.
### Prefix
- `yaml`/`json` setting: `prefix`
- Type: `string`
- Optional
- Example: `/admin`
If set, the route will only match incoming requests with a path that begins with the specified prefix.
### Path
- `yaml`/`json` setting: `path`
- Type: `string`
- Optional
- Example: `/admin/some/exact/path`
If set, the route will only match incoming requests with a path that is an exact match for the specified path.
### Regex
- `yaml`/`json` setting: `regex`
- Type: `string` (containing a regular expression)
- Optional
- Example: `^/(admin|superuser)/.*$`
If set, the route will only match incoming requests with a path that matches the specified regular expression. The supported syntax is the same as the Go [regexp package](https://golang.org/pkg/regexp/) which is based on [re2](https://github.com/google/re2/wiki/Syntax).
### Allowed Users
- `yaml`/`json` setting: `allowed_users`

View file

@ -5,6 +5,45 @@ description: >-
for Pomerium. Please read it carefully.
---
# Since 0.8.0
## Breaking
### Using paths in from URLs
Although it's unlikely anyone ever used it, prior to 0.8.0 the policy configuration allowed you to specify a `from` field with a path component:
```yaml
policy:
- from: "https://example.com/some/path"
```
The proxy and authorization server would simply ignore the path and route/authorize based on the host name.
With the introduction of `prefix`, `path` and `regex` fields to the policy route configuration, we decided not to support using a path in the `from` url, since the behavior was somewhat ambiguous and better handled by the explicit fields.
To avoid future confusion, the application will now declare any configuration which contains a `from` field with a path as invalid, with this error message:
```
config: policy source url (%s) contains a path, but it should be set using the path field instead
```
If you see this error you can fix it by simply removing the path from the `from` field and moving it to a `prefix` field.
In other words, this configuration:
```yaml
policy:
- from: "http://example.com/some/path"
```
Should be written like this:
```yaml
policy:
- from: "http://example.com"
prefix: "/some/path"
```
# Since 0.6.0
## Breaking

View file

@ -115,6 +115,7 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler {
if err != nil {
return httputil.NewError(http.StatusBadRequest, err)
}
originalRequest := p.getOriginalRequest(r, uri)
if _, err := sessions.FromContext(r.Context()); err != nil {
if verifyOnly {
@ -130,10 +131,7 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler {
return nil
}
r.Host = uri.Host
r.URL = uri
r.RequestURI = uri.String()
if err := p.authorize(w, r); err != nil {
if err := p.authorize(w, originalRequest); err != nil {
return err
}
@ -143,3 +141,10 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler {
return nil
})
}
func (p *Proxy) getOriginalRequest(r *http.Request, originalURL *url.URL) *http.Request {
originalRequest := r.Clone(r.Context())
originalRequest.Host = originalURL.Host
originalRequest.URL = originalURL
return originalRequest
}

View file

@ -14,6 +14,8 @@ import (
"net/http"
stdhttputil "net/http/httputil"
"net/url"
"regexp"
"strings"
"time"
"github.com/gorilla/mux"
@ -230,8 +232,8 @@ func (p *Proxy) reverseProxyHandler(r *mux.Router, policy config.Policy) *mux.Ro
// 4. Override any custom transport settings (e.g. TLS settings, etc)
proxy.Transport = p.roundTripperFromPolicy(&policy)
// 5. Create a sub-router for a given route's hostname (`httpbin.corp.example.com`)
rp := r.Host(policy.Source.Host).Subrouter()
// 5. Create a sub-router with a matcher derived from the policy (host, path, etc...)
rp := r.MatcherFunc(routeMatcherFuncFromPolicy(policy)).Subrouter()
rp.PathPrefix("/").Handler(proxy)
// Optional: If websockets are enabled, do not set a handler request timeout
@ -323,3 +325,42 @@ func (p *Proxy) roundTripperFromPolicy(policy *config.Policy) http.RoundTripper
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.Handler.ServeHTTP(w, r)
}
// routeMatcherFuncFromPolicy returns a mux matcher function which compares an http request with a policy.
//
// Routes can be filtered by the `source`, `prefix`, `path` and `regex` fields in the policy config.
func routeMatcherFuncFromPolicy(policy config.Policy) mux.MatcherFunc {
// match by source
sourceMatches := func(r *http.Request) bool {
return r.Host == policy.Source.Host
}
// match by prefix
prefixMatches := func(r *http.Request) bool {
return policy.Prefix == "" ||
strings.HasPrefix(r.URL.Path, policy.Prefix)
}
// match by path
pathMatches := func(r *http.Request) bool {
return policy.Path == "" ||
r.URL.Path == policy.Path
}
// match by path regex
var regexMatches func(*http.Request) bool
if policy.Regex == "" {
regexMatches = func(r *http.Request) bool { return true }
} else if re, err := regexp.Compile(policy.Regex); err == nil {
regexMatches = func(r *http.Request) bool {
return re.MatchString(r.URL.Path)
}
} else {
log.Error().Err(err).Str("regex", policy.Regex).Msg("proxy: invalid regex in policy, ignoring route")
regexMatches = func(r *http.Request) bool { return false }
}
return func(r *http.Request, rm *mux.RouteMatch) bool {
return sourceMatches(r) && prefixMatches(r) && pathMatches(r) && regexMatches(r)
}
}

View file

@ -1,6 +1,7 @@
package proxy
import (
"io"
"io/ioutil"
"net"
"net/http"
@ -276,3 +277,138 @@ func TestNewReverseProxy(t *testing.T) {
t.Errorf("got body %q; expected %q", g, e)
}
}
func TestRouteMatcherFuncFromPolicy(t *testing.T) {
tests := []struct {
source, prefix, path, regex string
incomingURL string
expect bool
msg string
}{
// host in source
{"https://www.example.com", "", "", "",
"https://www.example.com", true,
"should match when host is the same as source host"},
{"https://www.example.com/", "", "", "",
"https://www.example.com", true,
"should match when host is the same as source host with trailing slash"},
{"https://www.example.com", "", "", "",
"https://www.google.com", false,
"should not match when host is different from source host"},
// path prefix
{"https://www.example.com", "/admin", "", "",
"https://www.example.com/admin/someaction", true,
"should match when path begins with prefix"},
{"https://www.example.com", "/admin", "", "",
"https://www.example.com/notadmin", false,
"should not match when path does not begin with prefix"},
// path
{"https://www.example.com", "", "/admin", "",
"https://www.example.com/admin", true,
"should match when path is the same as the policy path"},
{"https://www.example.com", "", "/admin", "",
"https://www.example.com/admin/someaction", false,
"should not match when path merely begins with the policy path"},
{"https://www.example.com", "", "/admin", "",
"https://www.example.com/notadmin", false,
"should not match when path is different from the policy path"},
// path regex
{"https://www.example.com", "", "", "^/admin/[a-z]+$",
"https://www.example.com/admin/someaction", true,
"should match when path matches policy path regex"},
{"https://www.example.com", "", "", "^/admin/[a-z]+$",
"https://www.example.com/notadmin", false,
"should not match when path does not match policy path regex"},
{"https://www.example.com", "", "", "invalid[",
"https://www.example.com/invalid", false,
"should not match on invalid policy path regex"},
}
for _, tt := range tests {
srcURL, err := url.Parse(tt.source)
if err != nil {
panic(err)
}
src := &config.StringURL{URL: srcURL}
matcher := routeMatcherFuncFromPolicy(config.Policy{
Source: src,
Prefix: tt.prefix,
Path: tt.path,
Regex: tt.regex,
})
req, err := http.NewRequest("GET", tt.incomingURL, nil)
if err != nil {
panic(err)
}
actual := matcher(req, nil)
if actual != tt.expect {
t.Errorf("%s (source=%s prefix=%s path=%s regex=%s incoming-url=%s)",
tt.msg, tt.source, tt.prefix, tt.path, tt.regex, tt.incomingURL)
}
}
}
func TestPolicyPrefixRouting(t *testing.T) {
adminServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "admin: "+r.URL.Path)
}))
defer adminServer.Close()
publicServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "public: "+r.URL.Path)
}))
defer publicServer.Close()
opts := testOptions(t)
opts.Policies = []config.Policy{
{
From: "https://from.example.com",
To: "http://" + adminServer.Listener.Addr().String(),
Prefix: "/admin",
AllowPublicUnauthenticatedAccess: true,
},
{
From: "https://from.example.com",
To: "http://" + publicServer.Listener.Addr().String(),
AllowPublicUnauthenticatedAccess: true,
},
}
p, err := New(opts)
if err != nil {
t.Fatalf("error creating proxy: %v", err)
}
t.Run("admin", func(t *testing.T) {
req, err := http.NewRequest("GET", "https://from.example.com/admin/path", nil)
if err != nil {
t.Fatalf("error creating http request: %v", err)
}
rr := httptest.NewRecorder()
p.ServeHTTP(rr, req)
rr.Flush()
if rr.Body.String() != "admin: /admin/path" {
t.Errorf("expected admin request to go to the admin backend")
}
})
t.Run("non-admin", func(t *testing.T) {
req, err := http.NewRequest("GET", "https://from.example.com/nonadmin/path", nil)
if err != nil {
t.Fatalf("error creating http request: %v", err)
}
rr := httptest.NewRecorder()
p.ServeHTTP(rr, req)
rr.Flush()
if rr.Body.String() != "public: /nonadmin/path" {
t.Errorf("expected non-admin request to go to the public backend")
}
})
}

42
scripts/build-dev-docker.bash Executable file
View file

@ -0,0 +1,42 @@
#!/bin/bash
set -euxo pipefail
_dir=/tmp/pomerium-dev-docker
mkdir -p "$_dir"
# build linux binary
env GOOS=linux \
GOARCH=amd64 \
CGO_ENABLED=0 \
GO111MODULE=on \
go build \
-ldflags "-s -w" \
-o "$_dir/pomerium" \
./cmd/pomerium
# build docker image
(
cd $_dir
cat <<EOF >config.yaml
EOF
cat <<EOF >Dockerfile
FROM gcr.io/distroless/base:debug
WORKDIR /pomerium
COPY pomerium /bin/pomerium
COPY config.yaml /pomerium/config.yaml
ENTRYPOINT [ "/bin/pomerium" ]
CMD ["-config","/pomerium/config.yaml"]
EOF
docker build --tag=pomerium/pomerium:dev .
# build for minikube
if command -v minikube >/dev/null 2>&1 ; then
eval "$(minikube docker-env --shell=bash)"
docker build --tag=pomerium/pomerium:dev .
fi
)