mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 18:36:30 +02:00
authorize: refactor and add additional unit tests (#757)
* authorize: clean up code, add test * authorize: additional test * authorize: additional test
This commit is contained in:
parent
5c3c020508
commit
a969f33d88
4 changed files with 314 additions and 157 deletions
|
@ -3,6 +3,7 @@ package authorize
|
|||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||
|
@ -11,12 +12,39 @@ import (
|
|||
"google.golang.org/genproto/googleapis/rpc/status"
|
||||
"google.golang.org/grpc/codes"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/grpc/authorize"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
)
|
||||
|
||||
func (a *Authorize) deniedResponse(in *envoy_service_auth_v2.CheckRequest,
|
||||
code int32, reason string, headers map[string]string) *envoy_service_auth_v2.CheckResponse {
|
||||
func (a *Authorize) okResponse(
|
||||
reply *authorize.IsAuthorizedReply,
|
||||
rawSession []byte,
|
||||
isNewSession bool,
|
||||
) *envoy_service_auth_v2.CheckResponse {
|
||||
|
||||
requestHeaders, err := a.getEnvoyRequestHeaders(rawSession, isNewSession)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("authorize: error generating new request headers")
|
||||
}
|
||||
requestHeaders = append(requestHeaders,
|
||||
mkHeader(httputil.HeaderPomeriumJWTAssertion, reply.SignedJwt))
|
||||
|
||||
return &envoy_service_auth_v2.CheckResponse{
|
||||
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
||||
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
||||
Headers: requestHeaders,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Authorize) deniedResponse(
|
||||
in *envoy_service_auth_v2.CheckRequest,
|
||||
code int32, reason string, headers map[string]string,
|
||||
) *envoy_service_auth_v2.CheckResponse {
|
||||
|
||||
returnHTMLError := true
|
||||
inHeaders := in.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
||||
|
@ -96,6 +124,20 @@ func (a *Authorize) plainTextDeniedResponse(code int32, reason string, headers m
|
|||
}
|
||||
}
|
||||
|
||||
func (a *Authorize) redirectResponse(in *envoy_service_auth_v2.CheckRequest) *envoy_service_auth_v2.CheckResponse {
|
||||
opts := a.currentOptions.Load()
|
||||
|
||||
signinURL := opts.AuthenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/sign_in"})
|
||||
q := signinURL.Query()
|
||||
q.Set(urlutil.QueryRedirectURI, getCheckRequestURL(in).String())
|
||||
signinURL.RawQuery = q.Encode()
|
||||
redirectTo := urlutil.NewSignedURL(opts.SharedKey, signinURL).String()
|
||||
|
||||
return a.deniedResponse(in, http.StatusFound, "Login", map[string]string{
|
||||
"Location": redirectTo,
|
||||
})
|
||||
}
|
||||
|
||||
func mkHeader(k, v string) *envoy_api_v2_core.HeaderValueOption {
|
||||
return &envoy_api_v2_core.HeaderValueOption{
|
||||
Header: &envoy_api_v2_core.HeaderValue{
|
|
@ -29,15 +29,11 @@ type Request struct {
|
|||
Method string `json:"method,omitempty"`
|
||||
// URL specifies either the URI being requested.
|
||||
URL string `json:"url,omitempty"`
|
||||
// The protocol version for incoming server requests.
|
||||
Proto string `json:"proto,omitempty"` // "HTTP/1.0"
|
||||
// Header contains the request header fields either received
|
||||
// by the server or to be sent by the client.
|
||||
Header map[string][]string `json:"headers,omitempty"`
|
||||
// Host specifies the host on which the URL is sought.
|
||||
Host string `json:"host,omitempty"`
|
||||
// RemoteAddr is the network address that sent the request.
|
||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||
// RequestURI is the unmodified request-target of the
|
||||
// Request-Line (RFC 7230, Section 3.1.1) as sent by the client
|
||||
// to a server. Usually the URL field should be used instead.
|
||||
|
|
|
@ -2,6 +2,7 @@ package authorize
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -10,7 +11,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/grpc/authorize"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
|
@ -20,8 +21,6 @@ import (
|
|||
|
||||
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
||||
"google.golang.org/genproto/googleapis/rpc/status"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// Check implements the envoy auth server gRPC endpoint.
|
||||
|
@ -29,133 +28,71 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRe
|
|||
ctx, span := trace.StartSpan(ctx, "authorize.grpc.Check")
|
||||
defer span.End()
|
||||
|
||||
opts := a.currentOptions.Load()
|
||||
|
||||
// maybe rewrite http request for forward auth
|
||||
isForwardAuth := handleForwardAuth(opts, in)
|
||||
|
||||
hattrs := in.GetAttributes().GetRequest().GetHttp()
|
||||
isForwardAuth := a.handleForwardAuth(in)
|
||||
hreq := getHTTPRequestFromCheckRequest(in)
|
||||
|
||||
hdrs := getCheckRequestHeaders(in)
|
||||
|
||||
isNewSession := false
|
||||
sess, sesserr := loadSession(hreq, a.currentOptions.Load(), a.currentEncoder.Load())
|
||||
if a.isExpired(sess) {
|
||||
rawJWT, sessionErr := loadSession(hreq, a.currentOptions.Load(), a.currentEncoder.Load())
|
||||
if a.isExpired(rawJWT) {
|
||||
log.Info().Msg("refreshing session")
|
||||
if newSession, err := a.refreshSession(ctx, sess); err == nil {
|
||||
sess = newSession
|
||||
sesserr = nil
|
||||
if newRawJWT, err := a.refreshSession(ctx, rawJWT); err == nil {
|
||||
rawJWT = newRawJWT
|
||||
sessionErr = nil
|
||||
isNewSession = true
|
||||
} else {
|
||||
log.Warn().Err(err).Msg("authorize: error refreshing session")
|
||||
// set the error to expired so that we can force a new login
|
||||
sessionErr = sessions.ErrExpired
|
||||
}
|
||||
}
|
||||
|
||||
requestHeaders, err := a.getEnvoyRequestHeaders(sess, isNewSession)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("authorize: error generating new request headers")
|
||||
}
|
||||
|
||||
requestURL := getCheckRequestURL(in)
|
||||
req := &evaluator.Request{
|
||||
User: string(sess),
|
||||
Header: hdrs,
|
||||
Host: hattrs.GetHost(),
|
||||
Method: hattrs.GetMethod(),
|
||||
RequestURI: requestURL.String(),
|
||||
RemoteAddr: in.GetAttributes().GetSource().GetAddress().String(),
|
||||
URL: requestURL.String(),
|
||||
ClientCertificate: getPeerCertificate(in),
|
||||
}
|
||||
req := getEvaluatorRequestFromCheckRequest(in, rawJWT)
|
||||
reply, err := a.pe.IsAuthorized(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logAuthorizeCheck(ctx, in, reply, rawJWT)
|
||||
|
||||
evt := log.Info().Str("service", "authorize")
|
||||
// request
|
||||
evt = evt.Str("request-id", requestid.FromContext(ctx))
|
||||
evt = evt.Strs("check-request-id", hdrs["X-Request-Id"])
|
||||
evt = evt.Str("method", hattrs.GetMethod())
|
||||
evt = evt.Interface("headers", hdrs)
|
||||
evt = evt.Str("path", hattrs.GetPath())
|
||||
evt = evt.Str("host", hattrs.GetHost())
|
||||
evt = evt.Str("query", hattrs.GetQuery())
|
||||
// reply
|
||||
evt = evt.Bool("allow", reply.GetAllow())
|
||||
evt = evt.Bool("session-expired", reply.GetSessionExpired())
|
||||
evt = evt.Strs("deny-reasons", reply.GetDenyReasons())
|
||||
evt = evt.Str("email", reply.GetEmail())
|
||||
evt = evt.Strs("groups", reply.GetGroups())
|
||||
if sess != nil {
|
||||
evt = evt.Str("session", string(sess))
|
||||
}
|
||||
if reply.GetHttpStatus() != nil {
|
||||
evt = evt.Interface("http_status", reply.GetHttpStatus())
|
||||
}
|
||||
evt.Msg("authorize check")
|
||||
|
||||
requestHeaders = append(requestHeaders,
|
||||
&envoy_api_v2_core.HeaderValueOption{
|
||||
Header: &envoy_api_v2_core.HeaderValue{
|
||||
Key: "x-pomerium-jwt-assertion",
|
||||
Value: reply.SignedJwt,
|
||||
},
|
||||
})
|
||||
|
||||
if reply.GetHttpStatus().GetCode() > 0 && reply.GetHttpStatus().GetCode() != http.StatusOK {
|
||||
switch {
|
||||
case reply.GetHttpStatus().GetCode() > 0 && reply.GetHttpStatus().GetCode() != http.StatusOK:
|
||||
// custom error from the IsAuthorized call
|
||||
return a.deniedResponse(in,
|
||||
reply.GetHttpStatus().GetCode(),
|
||||
reply.GetHttpStatus().GetMessage(),
|
||||
reply.GetHttpStatus().GetHeaders(),
|
||||
), nil
|
||||
}
|
||||
|
||||
if reply.Allow {
|
||||
return &envoy_service_auth_v2.CheckResponse{
|
||||
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
||||
HttpResponse: &envoy_service_auth_v2.CheckResponse_OkResponse{
|
||||
OkResponse: &envoy_service_auth_v2.OkHttpResponse{
|
||||
Headers: requestHeaders,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
case reply.Allow:
|
||||
// ok!
|
||||
return a.okResponse(reply, rawJWT, isNewSession), nil
|
||||
|
||||
if reply.SessionExpired {
|
||||
sesserr = sessions.ErrExpired
|
||||
}
|
||||
|
||||
switch sesserr {
|
||||
case sessions.ErrExpired, sessions.ErrIssuedInTheFuture, sessions.ErrMalformed, sessions.ErrNoSessionFound, sessions.ErrNotValidYet:
|
||||
case reply.SessionExpired,
|
||||
errors.Is(sessionErr, sessions.ErrExpired),
|
||||
errors.Is(sessionErr, sessions.ErrIssuedInTheFuture),
|
||||
errors.Is(sessionErr, sessions.ErrMalformed),
|
||||
errors.Is(sessionErr, sessions.ErrNoSessionFound),
|
||||
errors.Is(sessionErr, sessions.ErrNotValidYet):
|
||||
// redirect to login
|
||||
default:
|
||||
var msg string
|
||||
if sesserr != nil {
|
||||
msg = sesserr.Error()
|
||||
|
||||
// no redirect for forward auth, that's handled by a separate config setting
|
||||
if isForwardAuth {
|
||||
return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil), nil
|
||||
}
|
||||
|
||||
return a.redirectResponse(in), nil
|
||||
|
||||
default:
|
||||
// all other errors
|
||||
var msg string
|
||||
if sessionErr != nil {
|
||||
msg = sessionErr.Error()
|
||||
}
|
||||
return a.deniedResponse(in, http.StatusForbidden, msg, nil), nil
|
||||
}
|
||||
|
||||
// no redirect for forward auth, that's handled by a separate config setting
|
||||
if isForwardAuth {
|
||||
return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil), nil
|
||||
}
|
||||
|
||||
signinURL := opts.AuthenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/sign_in"})
|
||||
q := signinURL.Query()
|
||||
q.Set(urlutil.QueryRedirectURI, requestURL.String())
|
||||
signinURL.RawQuery = q.Encode()
|
||||
redirectTo := urlutil.NewSignedURL(opts.SharedKey, signinURL).String()
|
||||
|
||||
return a.deniedResponse(in, http.StatusFound, "Login", map[string]string{
|
||||
"Location": redirectTo,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (a *Authorize) getEnvoyRequestHeaders(rawjwt []byte, isNewSession bool) ([]*envoy_api_v2_core.HeaderValueOption, error) {
|
||||
func (a *Authorize) getEnvoyRequestHeaders(rawJWT []byte, isNewSession bool) ([]*envoy_api_v2_core.HeaderValueOption, error) {
|
||||
var hvos []*envoy_api_v2_core.HeaderValueOption
|
||||
|
||||
if isNewSession {
|
||||
|
@ -164,44 +101,28 @@ func (a *Authorize) getEnvoyRequestHeaders(rawjwt []byte, isNewSession bool) ([]
|
|||
return nil, err
|
||||
}
|
||||
|
||||
hdrs, err := getJWTSetCookieHeaders(cookieStore, rawjwt)
|
||||
hdrs, err := getJWTSetCookieHeaders(cookieStore, rawJWT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range hdrs {
|
||||
hvos = append(hvos, &envoy_api_v2_core.HeaderValueOption{
|
||||
Header: &envoy_api_v2_core.HeaderValue{
|
||||
Key: "x-pomerium-" + k,
|
||||
Value: v,
|
||||
},
|
||||
})
|
||||
hvos = append(hvos, mkHeader("x-pomerium-"+k, v))
|
||||
}
|
||||
}
|
||||
|
||||
hdrs, err := getJWTClaimHeaders(a.currentOptions.Load(), a.currentEncoder.Load(), rawjwt)
|
||||
hdrs, err := getJWTClaimHeaders(a.currentOptions.Load(), a.currentEncoder.Load(), rawJWT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range hdrs {
|
||||
hvos = append(hvos, &envoy_api_v2_core.HeaderValueOption{
|
||||
Header: &envoy_api_v2_core.HeaderValue{
|
||||
Key: k,
|
||||
Value: v,
|
||||
},
|
||||
})
|
||||
hvos = append(hvos, mkHeader(k, v))
|
||||
}
|
||||
|
||||
return hvos, nil
|
||||
}
|
||||
|
||||
func (a *Authorize) refreshSession(ctx context.Context, rawSession []byte) (newSession []byte, err error) {
|
||||
func (a *Authorize) refreshSession(ctx context.Context, rawJWT []byte) (newSession []byte, err error) {
|
||||
options := a.currentOptions.Load()
|
||||
encoder := a.currentEncoder.Load()
|
||||
|
||||
var state sessions.State
|
||||
if err := encoder.Unmarshal(rawSession, &state); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling raw session: %w", err)
|
||||
}
|
||||
|
||||
// 1 - build a signed url to call refresh on authenticate service
|
||||
refreshURI := options.AuthenticateURL.ResolveReference(&url.URL{Path: "/.pomerium/refresh"})
|
||||
|
@ -212,7 +133,7 @@ func (a *Authorize) refreshSession(ctx context.Context, rawSession []byte) (newS
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("authorize: refresh request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Pomerium %s", rawSession))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Pomerium %s", rawJWT))
|
||||
req.Header.Set("X-Requested-With", "XmlHttpRequest")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
|
@ -238,6 +159,49 @@ func (a *Authorize) isExpired(rawSession []byte) bool {
|
|||
return err == nil && state.IsExpired()
|
||||
}
|
||||
|
||||
func (a *Authorize) handleForwardAuth(req *envoy_service_auth_v2.CheckRequest) bool {
|
||||
opts := a.currentOptions.Load()
|
||||
|
||||
if opts.ForwardAuthURL == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
checkURL := getCheckRequestURL(req)
|
||||
if urlutil.StripPort(checkURL.Host) == urlutil.StripPort(opts.ForwardAuthURL.Host) {
|
||||
if (checkURL.Path == "/" || checkURL.Path == "/verify") && checkURL.Query().Get("uri") != "" {
|
||||
verifyURL, err := url.Parse(checkURL.Query().Get("uri"))
|
||||
if err != nil {
|
||||
log.Warn().Str("uri", checkURL.Query().Get("uri")).Err(err).Msg("failed to parse uri for forward authentication")
|
||||
return false
|
||||
}
|
||||
req.Attributes.Request.Http.Scheme = verifyURL.Scheme
|
||||
req.Attributes.Request.Http.Host = verifyURL.Host
|
||||
req.Attributes.Request.Http.Path = verifyURL.Path
|
||||
// envoy sends the query string as part of the path
|
||||
if verifyURL.RawQuery != "" {
|
||||
req.Attributes.Request.Http.Path += "?" + verifyURL.RawQuery
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getEvaluatorRequestFromCheckRequest(in *envoy_service_auth_v2.CheckRequest, rawJWT []byte) *evaluator.Request {
|
||||
requestURL := getCheckRequestURL(in)
|
||||
req := &evaluator.Request{
|
||||
User: string(rawJWT),
|
||||
Header: getCheckRequestHeaders(in),
|
||||
Host: in.GetAttributes().GetRequest().GetHttp().GetHost(),
|
||||
Method: in.GetAttributes().GetRequest().GetHttp().GetMethod(),
|
||||
RequestURI: requestURL.String(),
|
||||
URL: requestURL.String(),
|
||||
ClientCertificate: getPeerCertificate(in),
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func getHTTPRequestFromCheckRequest(req *envoy_service_auth_v2.CheckRequest) *http.Request {
|
||||
hattrs := req.GetAttributes().GetRequest().GetHttp()
|
||||
return &http.Request{
|
||||
|
@ -274,44 +238,49 @@ func getCheckRequestURL(req *envoy_service_auth_v2.CheckRequest) *url.URL {
|
|||
u.Path = path
|
||||
}
|
||||
|
||||
if h.Headers != nil {
|
||||
if fwdProto, ok := h.Headers["x-forwarded-proto"]; ok {
|
||||
if h.GetHeaders() != nil {
|
||||
if fwdProto, ok := h.GetHeaders()["x-forwarded-proto"]; ok {
|
||||
u.Scheme = fwdProto
|
||||
}
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func handleForwardAuth(opts config.Options, req *envoy_service_auth_v2.CheckRequest) bool {
|
||||
if opts.ForwardAuthURL == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
checkURL := getCheckRequestURL(req)
|
||||
if urlutil.StripPort(checkURL.Host) == urlutil.StripPort(opts.ForwardAuthURL.Host) {
|
||||
if (checkURL.Path == "/" || checkURL.Path == "/verify") && checkURL.Query().Get("uri") != "" {
|
||||
verifyURL, err := url.Parse(checkURL.Query().Get("uri"))
|
||||
if err != nil {
|
||||
log.Warn().Str("uri", checkURL.Query().Get("uri")).Err(err).Msg("failed to parse uri for forward authentication")
|
||||
return false
|
||||
}
|
||||
req.Attributes.Request.Http.Scheme = verifyURL.Scheme
|
||||
req.Attributes.Request.Http.Host = verifyURL.Host
|
||||
req.Attributes.Request.Http.Path = verifyURL.Path
|
||||
// envoy sends the query string as part of the path
|
||||
if verifyURL.RawQuery != "" {
|
||||
req.Attributes.Request.Http.Path += "?" + verifyURL.RawQuery
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getPeerCertificate gets the PEM-encoded peer certificate from the check request
|
||||
func getPeerCertificate(in *envoy_service_auth_v2.CheckRequest) string {
|
||||
// ignore the error as we will just return the empty string in that case
|
||||
cert, _ := url.QueryUnescape(in.GetAttributes().GetSource().GetCertificate())
|
||||
return cert
|
||||
}
|
||||
|
||||
func logAuthorizeCheck(
|
||||
ctx context.Context,
|
||||
in *envoy_service_auth_v2.CheckRequest,
|
||||
reply *authorize.IsAuthorizedReply,
|
||||
rawJWT []byte,
|
||||
) {
|
||||
hdrs := getCheckRequestHeaders(in)
|
||||
hattrs := in.GetAttributes().GetRequest().GetHttp()
|
||||
evt := log.Info().Str("service", "authorize")
|
||||
// request
|
||||
evt = evt.Str("request-id", requestid.FromContext(ctx))
|
||||
evt = evt.Strs("check-request-id", hdrs["X-Request-Id"])
|
||||
evt = evt.Str("method", hattrs.GetMethod())
|
||||
evt = evt.Interface("headers", hdrs)
|
||||
evt = evt.Str("path", hattrs.GetPath())
|
||||
evt = evt.Str("host", hattrs.GetHost())
|
||||
evt = evt.Str("query", hattrs.GetQuery())
|
||||
// reply
|
||||
evt = evt.Bool("allow", reply.GetAllow())
|
||||
evt = evt.Bool("session-expired", reply.GetSessionExpired())
|
||||
evt = evt.Strs("deny-reasons", reply.GetDenyReasons())
|
||||
evt = evt.Str("email", reply.GetEmail())
|
||||
evt = evt.Strs("groups", reply.GetGroups())
|
||||
if rawJWT != nil {
|
||||
evt = evt.Str("session", string(rawJWT))
|
||||
}
|
||||
if reply.GetHttpStatus() != nil {
|
||||
evt = evt.Interface("http_status", reply.GetHttpStatus())
|
||||
}
|
||||
evt.Msg("authorize check")
|
||||
}
|
||||
|
|
150
authorize/grpc_test.go
Normal file
150
authorize/grpc_test.go
Normal file
|
@ -0,0 +1,150 @@
|
|||
package authorize
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/pomerium/pomerium/authorize/evaluator"
|
||||
"github.com/pomerium/pomerium/config"
|
||||
)
|
||||
|
||||
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) {
|
||||
actual := 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []byte("HELLO WORLD"))
|
||||
expect := &evaluator.Request{
|
||||
User: "HELLO WORLD",
|
||||
Method: "GET",
|
||||
URL: "https://example.com/some/path?qs=1",
|
||||
Header: map[string][]string{
|
||||
"Accept": {"text/html"},
|
||||
"X-Forwarded-Proto": {"https"},
|
||||
},
|
||||
Host: "example.com",
|
||||
RequestURI: "https://example.com/some/path?qs=1",
|
||||
ClientCertificate: certPEM,
|
||||
}
|
||||
assert.Equal(t, expect, actual)
|
||||
}
|
||||
|
||||
func Test_handleForwardAuth(t *testing.T) {
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("enabled", func(t *testing.T) {
|
||||
a := new(Authorize)
|
||||
a.currentOptions.Store(config.Options{
|
||||
ForwardAuthURL: mustParseURL("https://forward-auth.example.com"),
|
||||
})
|
||||
isForwardAuth := a.handleForwardAuth(checkReq)
|
||||
assert.True(t, isForwardAuth)
|
||||
assert.Equal(t, &envoy_service_auth_v2.AttributeContext_HttpRequest{
|
||||
Method: "GET",
|
||||
Path: "/some/path?qs=1",
|
||||
Host: "example.com",
|
||||
Scheme: "https",
|
||||
}, checkReq.Attributes.Request.Http)
|
||||
})
|
||||
t.Run("disabled", func(t *testing.T) {
|
||||
a := new(Authorize)
|
||||
a.currentOptions.Store(config.Options{
|
||||
ForwardAuthURL: nil,
|
||||
})
|
||||
isForwardAuth := a.handleForwardAuth(checkReq)
|
||||
assert.False(t, isForwardAuth)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_refreshSession(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(struct {
|
||||
Authorization string
|
||||
}{
|
||||
Authorization: r.Header.Get("Authorization"),
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
sharedKey := make([]byte, 32)
|
||||
a := new(Authorize)
|
||||
a.currentOptions.Store(config.Options{
|
||||
AuthenticateURL: mustParseURL(srv.URL),
|
||||
SharedKey: base64.StdEncoding.EncodeToString(sharedKey),
|
||||
})
|
||||
|
||||
newSession, err := a.refreshSession(context.Background(), []byte("ABCD"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `{"Authorization":"Pomerium ABCD"}`, strings.TrimSpace(string(newSession)))
|
||||
}
|
||||
|
||||
func mustParseURL(str string) *url.URL {
|
||||
u, err := url.Parse(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u
|
||||
}
|
Loading…
Add table
Reference in a new issue