mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-10 07:37:33 +02:00
authorize: add support for .pomerium and unauthenticated routes (#639)
* authorize: add support for .pomerium and unauthenticated routes integration-tests: add test for forward auth dashboard urls * proxy: fix ctx error test to return a 200 when authorize allows it
This commit is contained in:
parent
e5c7c5b27e
commit
b1d3bbaf56
11 changed files with 158 additions and 69 deletions
|
@ -5,10 +5,15 @@ import data.shared_key
|
|||
|
||||
default allow = false
|
||||
|
||||
# allow public
|
||||
allow {
|
||||
route := first_allowed_route(input.url)
|
||||
route_policies[route].AllowPublicUnauthenticatedAccess == true
|
||||
}
|
||||
|
||||
# allow by email
|
||||
allow {
|
||||
some route
|
||||
allowed_route(input.url, route_policies[route])
|
||||
route := first_allowed_route(input.url)
|
||||
token.payload.email = route_policies[route].allowed_users[_]
|
||||
token.valid
|
||||
count(deny)==0
|
||||
|
@ -16,8 +21,7 @@ allow {
|
|||
|
||||
# allow group
|
||||
allow {
|
||||
some route
|
||||
allowed_route(input.url, route_policies[route])
|
||||
route := first_allowed_route(input.url)
|
||||
some group
|
||||
token.payload.groups[group] == route_policies[route].allowed_groups[_]
|
||||
token.valid
|
||||
|
@ -26,8 +30,7 @@ allow {
|
|||
|
||||
# allow by impersonate email
|
||||
allow {
|
||||
some route
|
||||
allowed_route(input.url, route_policies[route])
|
||||
route := first_allowed_route(input.url)
|
||||
token.payload.impersonate_email = route_policies[route].allowed_users[_]
|
||||
token.valid
|
||||
count(deny)==0
|
||||
|
@ -35,8 +38,7 @@ allow {
|
|||
|
||||
# allow by impersonate group
|
||||
allow {
|
||||
some route
|
||||
allowed_route(input.url, route_policies[route])
|
||||
route := first_allowed_route(input.url)
|
||||
some group
|
||||
token.payload.impersonate_groups[group] == route_policies[route].allowed_groups[_]
|
||||
token.valid
|
||||
|
@ -45,8 +47,7 @@ allow {
|
|||
|
||||
# allow by domain
|
||||
allow {
|
||||
some route
|
||||
allowed_route(input.url, route_policies[route])
|
||||
route := first_allowed_route(input.url)
|
||||
some domain
|
||||
email_in_domain(token.payload.email, route_policies[route].allowed_domains[domain])
|
||||
token.valid
|
||||
|
@ -55,14 +56,24 @@ allow {
|
|||
|
||||
# allow by impersonate domain
|
||||
allow {
|
||||
some route
|
||||
allowed_route(input.url, route_policies[route])
|
||||
route := first_allowed_route(input.url)
|
||||
some domain
|
||||
email_in_domain(token.payload.impersonate_email, route_policies[route].allowed_domains[domain])
|
||||
token.valid
|
||||
count(deny)==0
|
||||
}
|
||||
|
||||
# allow pomerium urls
|
||||
allow {
|
||||
contains(input.url, "/.pomerium/")
|
||||
not contains(input.url,"/.pomerium/admin")
|
||||
}
|
||||
|
||||
# returns the first matching route
|
||||
first_allowed_route(input_url) = route {
|
||||
route := [route | some route ; allowed_route(input.url, route_policies[route])][0]
|
||||
}
|
||||
|
||||
allowed_route(input_url, policy){
|
||||
input_url_obj := parse_url(input_url)
|
||||
allowed_route_source(input_url_obj, policy)
|
||||
|
|
|
@ -65,6 +65,51 @@ test_email_denied {
|
|||
}
|
||||
}
|
||||
|
||||
test_public_allowed {
|
||||
allow with data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"AllowPublicUnauthenticatedAccess": true
|
||||
}] with input as {
|
||||
"url": "http://example.com",
|
||||
"host": "example.com"
|
||||
}
|
||||
}
|
||||
test_public_denied {
|
||||
not allow with data.route_policies as [
|
||||
{
|
||||
"source": "example.com",
|
||||
"prefix": "/by-user",
|
||||
"allowed_users": ["bob@example.com"]
|
||||
},
|
||||
{
|
||||
"source": "example.com",
|
||||
"AllowPublicUnauthenticatedAccess": true
|
||||
}
|
||||
] with input as {
|
||||
"url": "http://example.com/by-user",
|
||||
"host": "example.com"
|
||||
}
|
||||
}
|
||||
|
||||
test_pomerium_allowed {
|
||||
allow with data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["bob@example.com"]
|
||||
}] with input as {
|
||||
"url": "http://example.com/.pomerium/",
|
||||
"host": "example.com"
|
||||
}
|
||||
}
|
||||
test_pomerium_denied {
|
||||
not allow with data.route_policies as [{
|
||||
"source": "example.com",
|
||||
"allowed_users": ["bob@example.com"]
|
||||
}] with input as {
|
||||
"url": "http://example.com/.pomerium/admin",
|
||||
"host": "example.com"
|
||||
}
|
||||
}
|
||||
|
||||
test_parse_url {
|
||||
url := parse_url("http://example.com/some/path?qs")
|
||||
url.scheme == "http"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
||||
"github.com/pomerium/pomerium/internal/grpc/authorize"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
||||
)
|
||||
|
||||
|
@ -25,7 +26,19 @@ func (a *Authorize) IsAuthorized(ctx context.Context, in *authorize.IsAuthorized
|
|||
RemoteAddr: in.GetRequestRemoteAddr(),
|
||||
URL: getFullURL(in.GetRequestUrl(), in.GetRequestHost()),
|
||||
}
|
||||
return a.pe.IsAuthorized(ctx, req)
|
||||
reply, err := a.pe.IsAuthorized(ctx, req)
|
||||
log.Info().
|
||||
// request
|
||||
Str("method", req.Method).
|
||||
Str("url", req.URL).
|
||||
// reply
|
||||
Bool("allow", reply.Allow).
|
||||
Strs("deny-reasons", reply.DenyReasons).
|
||||
Str("user", reply.User).
|
||||
Str("email", reply.Email).
|
||||
Strs("groups", reply.Groups).
|
||||
Msg("authorize.grpc.IsAuthorized")
|
||||
return reply, err
|
||||
}
|
||||
|
||||
type protoHeader map[string]*authorize.IsAuthorizedRequest_Headers
|
||||
|
|
6
go.sum
6
go.sum
|
@ -108,12 +108,11 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
|
@ -513,8 +512,6 @@ google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEn
|
|||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.14.0 h1:uMf5uLi4eQMRrMKhCplNik4U4H8Z6C1br3zOtAa/aDE=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0 h1:J1Pl9P2lnmYFSJvgs70DKELqHNh8CNWXPbud4njEE2s=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
|
@ -549,6 +546,7 @@ google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLY
|
|||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
|
|
@ -16,8 +16,6 @@ func TestAuthorization(t *testing.T) {
|
|||
defer clearTimeout()
|
||||
|
||||
t.Run("public", func(t *testing.T) {
|
||||
t.Skip() // pomerium doesn't currently handle unauthenticated public routes
|
||||
|
||||
client := testcluster.NewHTTPClient()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://httpdetails.localhost.pomerium.io", nil)
|
||||
|
@ -33,7 +31,6 @@ func TestAuthorization(t *testing.T) {
|
|||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "unexpected status code, headers=%v", res.Header)
|
||||
})
|
||||
|
||||
t.Run("domains", func(t *testing.T) {
|
||||
t.Run("allowed", func(t *testing.T) {
|
||||
client := testcluster.NewHTTPClient()
|
||||
|
@ -78,7 +75,7 @@ func TestAuthorization(t *testing.T) {
|
|||
client := testcluster.NewHTTPClient()
|
||||
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"), "joe@cats.test", []string{"user"})
|
||||
if assert.NoError(t, err) {
|
||||
assertDeniedAccess(t, res, "expected Forbidden for user")
|
||||
assertDeniedAccess(t, res, "expected Forbidden for user, but got %d", res.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -28,6 +28,23 @@ func TestDashboard(t *testing.T) {
|
|||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "unexpected status code")
|
||||
assert.Equal(t, "image/svg+xml", res.Header.Get("Content-Type"))
|
||||
})
|
||||
t.Run("forward auth image asset", func(t *testing.T) {
|
||||
client := testcluster.NewHTTPClient()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://fa-httpdetails.localhost.pomerium.io/.pomerium/assets/img/pomerium.svg", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if !assert.NoError(t, err, "unexpected http error") {
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "unexpected status code")
|
||||
assert.Equal(t, "image/svg+xml", res.Header.Get("Content-Type"))
|
||||
})
|
||||
|
|
|
@ -1,30 +1,33 @@
|
|||
local tls = import './tls.libsonnet';
|
||||
|
||||
local PomeriumPolicy = function() [
|
||||
local PomeriumPolicy = function() std.flattenArrays([
|
||||
[
|
||||
{
|
||||
from: 'http://httpdetails.localhost.pomerium.io',
|
||||
from: 'http://' + domain + '.localhost.pomerium.io',
|
||||
prefix: '/by-domain',
|
||||
to: 'http://httpdetails.default.svc.cluster.local',
|
||||
to: 'http://' + domain + '.default.svc.cluster.local',
|
||||
allowed_domains: ['dogs.test'],
|
||||
},
|
||||
{
|
||||
from: 'http://httpdetails.localhost.pomerium.io',
|
||||
from: 'http://' + domain + '.localhost.pomerium.io',
|
||||
prefix: '/by-user',
|
||||
to: 'http://httpdetails.default.svc.cluster.local',
|
||||
to: 'http://' + domain + '.default.svc.cluster.local',
|
||||
allowed_users: ['bob@dogs.test'],
|
||||
},
|
||||
{
|
||||
from: 'http://httpdetails.localhost.pomerium.io',
|
||||
from: 'http://' + domain + '.localhost.pomerium.io',
|
||||
prefix: '/by-group',
|
||||
to: 'http://httpdetails.default.svc.cluster.local',
|
||||
to: 'http://' + domain + '.default.svc.cluster.local',
|
||||
allowed_groups: ['admin'],
|
||||
},
|
||||
{
|
||||
from: 'http://httpdetails.localhost.pomerium.io',
|
||||
to: 'http://httpdetails.default.svc.cluster.local',
|
||||
from: 'http://' + domain + '.localhost.pomerium.io',
|
||||
to: 'http://' + domain + '.default.svc.cluster.local',
|
||||
allow_public_unauthenticated_access: true,
|
||||
},
|
||||
];
|
||||
]
|
||||
for domain in ['httpdetails', 'fa-httpdetails']
|
||||
]);
|
||||
|
||||
local PomeriumPolicyHash = std.base64(std.md5(std.manifestJsonEx(PomeriumPolicy(), '')));
|
||||
|
||||
|
@ -292,20 +295,27 @@ local PomeriumForwardAuthIngress = function() {
|
|||
tls: [
|
||||
{
|
||||
hosts: [
|
||||
'fa-httpecho.localhost.pomerium.io',
|
||||
'fa-httpdetails.localhost.pomerium.io',
|
||||
],
|
||||
secretName: 'pomerium-tls',
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
host: 'fa-httpecho.localhost.pomerium.io',
|
||||
host: 'fa-httpdetails.localhost.pomerium.io',
|
||||
http: {
|
||||
paths: [
|
||||
{
|
||||
path: '/.pomerium/',
|
||||
backend: {
|
||||
serviceName: 'proxy',
|
||||
servicePort: 'https',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
backend: {
|
||||
serviceName: 'httpecho',
|
||||
serviceName: 'httpdetails',
|
||||
servicePort: 'http',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -117,6 +117,8 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler {
|
|||
}
|
||||
originalRequest := p.getOriginalRequest(r, uri)
|
||||
|
||||
if err := p.authorize(w, originalRequest); err != nil {
|
||||
// no session, so redirect
|
||||
if _, err := sessions.FromContext(r.Context()); err != nil {
|
||||
if verifyOnly {
|
||||
return httputil.NewError(http.StatusUnauthorized, err)
|
||||
|
@ -131,7 +133,6 @@ func (p *Proxy) Verify(verifyOnly bool) http.Handler {
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := p.authorize(w, originalRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -103,10 +103,7 @@ func (p *Proxy) AuthorizeSession(next http.Handler) http.Handler {
|
|||
func (p *Proxy) authorize(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx, span := trace.StartSpan(r.Context(), "proxy.authorize")
|
||||
defer span.End()
|
||||
jwt, err := sessions.FromContext(ctx)
|
||||
if err != nil {
|
||||
return httputil.NewError(http.StatusInternalServerError, err)
|
||||
}
|
||||
jwt, _ := sessions.FromContext(ctx)
|
||||
authz, err := p.AuthorizeClient.Authorize(ctx, jwt, r)
|
||||
if err != nil {
|
||||
return httputil.NewError(http.StatusInternalServerError, err)
|
||||
|
|
|
@ -159,7 +159,7 @@ func TestProxy_AuthorizeSession(t *testing.T) {
|
|||
}{
|
||||
{"user is authorized", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: true}}, nil, identity.MockProvider{}, http.StatusOK},
|
||||
{"user is not authorized", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: false}}, nil, identity.MockProvider{}, http.StatusForbidden},
|
||||
{"ctx error", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: true}}, errors.New("hi"), identity.MockProvider{}, http.StatusInternalServerError},
|
||||
{"ctx error", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{Allow: true}}, errors.New("hi"), identity.MockProvider{}, http.StatusOK},
|
||||
{"authz client error", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeError: errors.New("err")}, nil, identity.MockProvider{}, http.StatusInternalServerError},
|
||||
{"expired, reauth failed", 200, &mstore.Store{Session: &sessions.State{Email: "user@test.example", Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second))}}, client.MockAuthorize{AuthorizeResponse: &authorize.IsAuthorizedReply{SessionExpired: true}}, nil, identity.MockProvider{}, http.StatusForbidden},
|
||||
//todo(bdd): it's a bit tricky to test the refresh flow
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue