pomerium/authorize/grpc_test.go
Caleb Doxsey 664358dfad
config: multiple endpoints for authorize and databroker (#1957)
* wip

* update docs

* remove dead code
2021-03-03 09:53:19 -07:00

556 lines
17 KiB
Go

package authorize
import (
"context"
"errors"
"net/url"
"testing"
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
"github.com/golang/protobuf/ptypes"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc"
"github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/pkg/grpc/databroker"
"github.com/pomerium/pomerium/pkg/grpc/session"
"github.com/pomerium/pomerium/pkg/grpc/user"
)
const certPEM = `
-----BEGIN CERTIFICATE-----
MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfRrObuSW5T7q
5CnSEqefEmtH4CCv6+5EckuriNr1CjfVvqzwfAhopXkLrq45EQm8vkmf7W96XJhC
7ZM0dYi1/qOCAU8wggFLMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAa
BgNVHREEEzARgg9tYWlsLmdvb2dsZS5jb20wCwYDVR0PBAQDAgeAMGgGCCsGAQUF
BwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcy
LmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5jb20vb2Nz
cDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/BAIwADAf
BgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHSAEEDAOMAwGCisG
AQQB1nkCBQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29t
L0dJQUcyLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAH6RYHxHdcGpMpFE3oxDoFnP+
gtuBCHan2yE2GRbJ2Cw8Lw0MmuKqHlf9RSeYfd3BXeKkj1qO6TVKwCh+0HdZk283
TZZyzmEOyclm3UGFYe82P/iDFt+CeQ3NpmBg+GoaVCuWAARJN/KfglbLyyYygcQq
0SgeDh8dRKUiaW3HQSoYvTvdTuqzwK4CXsr3b5/dAOY8uMuG/IAR3FgwTbZ1dtoW
RvOTa8hYiU6A475WuZKyEHcwnGYe57u2I2KbMgcKjPniocj4QzgYsVAVKW3IwaOh
yE+vPxsiUkvQHdO2fojCkY8jg70jxM+gu59tPDNbw3Uh/2Ij310FgTHsnGQMyA==
-----END CERTIFICATE-----`
func Test_getEvaluatorRequest(t *testing.T) {
a := &Authorize{currentOptions: config.NewAtomicOptions(), state: newAtomicAuthorizeState(new(authorizeState))}
encoder, _ := jws.NewHS256Signer([]byte{0, 0, 0, 0})
a.state.Load().encoder = encoder
a.currentOptions.Store(&config.Options{
Policies: []config.Policy{{
Source: &config.StringURL{URL: &url.URL{Host: "example.com"}},
SubPolicies: []config.SubPolicy{{
Rego: []string{"allow = true"},
}},
}},
})
actual, err := a.getEvaluatorRequestFromCheckRequest(
&envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Id: "id-1234",
Method: "GET",
Headers: map[string]string{
"accept": "text/html",
"x-forwarded-proto": "https",
},
Path: "/some/path?qs=1",
Host: "example.com",
Scheme: "http",
Body: "BODY",
},
},
},
},
&sessions.State{
ID: "SESSION_ID",
},
)
require.NoError(t, err)
expect := &evaluator.Request{
Session: evaluator.RequestSession{
ID: "SESSION_ID",
},
HTTP: evaluator.RequestHTTP{
Method: "GET",
URL: "http://example.com/some/path?qs=1",
Headers: map[string]string{
"Accept": "text/html",
"X-Forwarded-Proto": "https",
},
ClientCertificate: certPEM,
},
CustomPolicies: []string{"allow = true"},
}
assert.Equal(t, expect, actual)
}
func Test_handleForwardAuth(t *testing.T) {
tests := []struct {
name string
checkReq *envoy_service_auth_v2.CheckRequest
forwardAuthURL string
want bool
}{
{
name: "enabled",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri=" + url.QueryEscape("https://example.com/some/path?qs=1"),
Host: "forward-auth.example.com",
Scheme: "https",
},
},
},
},
forwardAuthURL: "https://forward-auth.example.com",
want: true,
},
{
name: "disabled",
checkReq: nil,
forwardAuthURL: "",
want: false,
},
{
name: "honor x-forwarded-uri set",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/",
Host: "forward-auth.example.com",
Scheme: "https",
Headers: map[string]string{
httputil.HeaderForwardedURI: "/foo/bar",
httputil.HeaderForwardedProto: "https",
httputil.HeaderForwardedHost: "example.com",
},
},
},
},
},
forwardAuthURL: "https://forward-auth.example.com",
want: true,
},
{
name: "request with invalid forward auth url",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri=" + url.QueryEscape("https://example.com?q=foo"),
Host: "fake-forward-auth.example.com",
Scheme: "https",
},
},
},
},
forwardAuthURL: "https://forward-auth.example.com",
want: false,
},
{
name: "request with invalid path",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/foo?uri=" + url.QueryEscape("https://example.com?q=foo"),
Host: "forward-auth.example.com",
Scheme: "https",
},
},
},
},
forwardAuthURL: "https://forward-auth.example.com",
want: true,
},
{
name: "request with empty uri",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri=",
Host: "forward-auth.example.com",
Scheme: "https",
},
},
},
},
forwardAuthURL: "https://forward-auth.example.com",
want: true,
},
{
name: "request with invalid uri",
checkReq: &envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri= http://example.com/foo",
Host: "forward-auth.example.com",
Scheme: "https",
},
},
},
},
forwardAuthURL: "https://forward-auth.example.com",
want: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
a := &Authorize{currentOptions: config.NewAtomicOptions(), state: newAtomicAuthorizeState(new(authorizeState))}
var fau *url.URL
if tc.forwardAuthURL != "" {
fau = mustParseURL(tc.forwardAuthURL)
}
a.currentOptions.Store(&config.Options{ForwardAuthURL: fau})
got := a.isForwardAuth(tc.checkReq)
if diff := cmp.Diff(got, tc.want); diff != "" {
t.Errorf("Authorize.Check() = %s", diff)
}
})
}
}
func Test_getEvaluatorRequestWithPortInHostHeader(t *testing.T) {
a := &Authorize{currentOptions: config.NewAtomicOptions(), state: newAtomicAuthorizeState(new(authorizeState))}
encoder, _ := jws.NewHS256Signer([]byte{0, 0, 0, 0})
a.state.Load().encoder = encoder
a.currentOptions.Store(&config.Options{
Policies: []config.Policy{{
Source: &config.StringURL{URL: &url.URL{Host: "example.com"}},
SubPolicies: []config.SubPolicy{{
Rego: []string{"allow = true"},
}},
}},
})
actual, err := a.getEvaluatorRequestFromCheckRequest(&envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Id: "id-1234",
Method: "GET",
Headers: map[string]string{
"accept": "text/html",
"x-forwarded-proto": "https",
},
Path: "/some/path?qs=1",
Host: "example.com:80",
Scheme: "http",
Body: "BODY",
},
},
},
}, nil)
require.NoError(t, err)
expect := &evaluator.Request{
Session: evaluator.RequestSession{},
HTTP: evaluator.RequestHTTP{
Method: "GET",
URL: "http://example.com/some/path?qs=1",
Headers: map[string]string{
"Accept": "text/html",
"X-Forwarded-Proto": "https",
},
ClientCertificate: certPEM,
},
CustomPolicies: []string{"allow = true"},
}
assert.Equal(t, expect, actual)
}
func TestSync(t *testing.T) {
mockSession := func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
data, _ := ptypes.MarshalAny(&session.Session{
Id: in.GetId(),
UserId: "user1",
})
return &databroker.GetResponse{
Record: &databroker.Record{
Version: 1,
Type: data.GetTypeUrl(),
Id: in.GetId(),
Data: data,
},
}, nil
}
mockUser := func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
data, _ := ptypes.MarshalAny(&user.User{Id: in.GetId()})
return &databroker.GetResponse{
Record: &databroker.Record{
Version: 1,
Type: data.GetTypeUrl(),
Id: in.GetId(),
Data: data,
},
}, nil
}
mockGetByType := map[string]func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error){
"type.googleapis.com/session.Session": mockSession,
"type.googleapis.com/user.User": mockUser,
}
dbdClient := mockDataBrokerServiceClient{
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
if in.GetId() == "not-existed-id" {
return nil, errors.New("not found")
}
f, ok := mockGetByType[in.GetType()]
if !ok {
return nil, errors.New("not found")
}
return f(ctx, in, opts...)
},
}
o := &config.Options{
AuthenticateURL: mustParseURL("https://authN.example.com"),
DataBrokerURLString: "https://databroker.example.com",
SharedKey: "gXK6ggrlIW2HyKyUF9rUO4azrDgxhDPWqw9y+lJU7B8=",
Policies: testPolicies(t),
}
ctx := context.Background()
tests := []struct {
name string
sessionState *sessions.State
databrokerClient mockDataBrokerServiceClient
wantErr bool
}{
{
"good with data in databroker data",
&sessions.State{ID: "dbd_session_id"},
mockDataBrokerServiceClient{
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
data, _ := ptypes.MarshalAny(&session.Session{
Id: in.GetId(),
UserId: "dbd_user1",
})
if in.GetType() == "type.googleapis.com/user.User" {
data, _ = ptypes.MarshalAny(&user.User{
Id: "dbd_user1",
})
}
return &databroker.GetResponse{
Record: &databroker.Record{
Version: 1,
Type: data.GetTypeUrl(),
Id: in.GetId(),
Data: data,
},
}, nil
},
},
false,
},
{"good", &sessions.State{ID: "SESSION_ID"}, dbdClient, false},
{"nil session state", nil, dbdClient, false},
{"not found session state", &sessions.State{ID: "not-existed-id"}, dbdClient, true},
{
"user not found",
&sessions.State{ID: "session_with_not_found_user"},
mockDataBrokerServiceClient{
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
if in.GetType() == "type.googleapis.com/user.User" {
return nil, errors.New("user not found")
}
data, _ := ptypes.MarshalAny(&session.Session{
Id: in.GetId(),
UserId: "user1",
})
return &databroker.GetResponse{
Record: &databroker.Record{
Version: 1,
Type: data.GetTypeUrl(),
Id: in.GetId(),
Data: data,
},
}, nil
},
},
false,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
a, err := New(&config.Config{Options: o})
require.NoError(t, err)
a.state.Load().dataBrokerClient = dbdClient
_, err = a.forceSync(ctx, tc.sessionState)
assert.True(t, (err != nil) == tc.wantErr)
})
}
}
func mustParseURL(str string) *url.URL {
u, err := url.Parse(str)
if err != nil {
panic(err)
}
return u
}
type mockDataBrokerServiceClient struct {
databroker.DataBrokerServiceClient
get func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error)
}
func (m mockDataBrokerServiceClient) Get(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
return m.get(ctx, in, opts...)
}
func TestAuthorize_Check(t *testing.T) {
opt := config.NewDefaultOptions()
opt.AuthenticateURL = mustParseURL("https://authenticate.example.com")
opt.DataBrokerURLString = "https://databroker.example.com"
opt.SharedKey = "E8wWIMnihUx+AUfRegAQDNs8eRb3UrB5G3zlJW9XJDM="
a, err := New(&config.Config{Options: opt})
if err != nil {
t.Fatal(err)
}
a.currentOptions.Store(&config.Options{ForwardAuthURL: mustParseURL("https://forward-auth.example.com")})
cmpOpts := []cmp.Option{
cmpopts.IgnoreUnexported(envoy_service_auth_v2.CheckResponse{}),
cmpopts.IgnoreUnexported(status.Status{}),
cmpopts.IgnoreTypes(envoy_service_auth_v2.DeniedHttpResponse{}),
}
tests := []struct {
name string
in *envoy_service_auth_v2.CheckRequest
want *envoy_service_auth_v2.CheckResponse
wantErr bool
}{
{
"basic deny",
&envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Id: "id-1234",
Method: "GET",
Headers: map[string]string{
"accept": "application/json",
"x-forwarded-proto": "https",
},
Path: "/some/path?qs=1",
Host: "example.com",
Scheme: "http",
Body: "BODY",
},
},
},
},
&envoy_service_auth_v2.CheckResponse{
Status: &status.Status{Code: 7, Message: "Access Denied"},
HttpResponse: &envoy_service_auth_v2.CheckResponse_DeniedResponse{
DeniedResponse: &envoy_service_auth_v2.DeniedHttpResponse{},
},
},
false,
},
{
"basic forward-auth deny",
&envoy_service_auth_v2.CheckRequest{
Attributes: &envoy_service_auth_v2.AttributeContext{
Source: &envoy_service_auth_v2.AttributeContext_Peer{
Certificate: url.QueryEscape(certPEM),
},
Request: &envoy_service_auth_v2.AttributeContext_Request{
Http: &envoy_service_auth_v2.AttributeContext_HttpRequest{
Method: "GET",
Path: "/verify?uri=" + url.QueryEscape("https://example.com/some/path?qs=1"),
Host: "forward-auth.example.com",
Scheme: "https",
},
},
},
},
&envoy_service_auth_v2.CheckResponse{
Status: &status.Status{Code: 7, Message: "Access Denied"},
HttpResponse: &envoy_service_auth_v2.CheckResponse_DeniedResponse{
DeniedResponse: &envoy_service_auth_v2.DeniedHttpResponse{},
},
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := a.Check(context.TODO(), tt.in)
if (err != nil) != tt.wantErr {
t.Errorf("Authorize.Check() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(got, tt.want, cmpOpts...); diff != "" {
t.Errorf("NewStore() = %s", diff)
}
})
}
}