pomerium/authorize/grpc.go
Caleb Doxsey f4c4fe314a
authorize: audit logging (#2050)
* authorize: add databroker server and record version to result, force sync via polling

* authorize: audit logging
2021-04-05 09:58:55 -06:00

226 lines
6.3 KiB
Go

package authorize
import (
"context"
"encoding/base64"
"io/ioutil"
"net/http"
"net/url"
"strings"
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
"github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/telemetry/trace"
"github.com/pomerium/pomerium/internal/urlutil"
)
// Check implements the envoy auth server gRPC endpoint.
func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRequest) (out *envoy_service_auth_v3.CheckResponse, err error) {
ctx, span := trace.StartSpan(ctx, "authorize.grpc.Check")
defer span.End()
state := a.state.Load()
// convert the incoming envoy-style http request into a go-style http request
hreq := getHTTPRequestFromCheckRequest(in)
isForwardAuth := a.isForwardAuth(in)
if isForwardAuth {
// update the incoming http request's uri to match the forwarded URI
fwdAuthURI := getForwardAuthURL(hreq)
in.Attributes.Request.Http.Scheme = fwdAuthURI.Scheme
in.Attributes.Request.Http.Host = fwdAuthURI.Host
in.Attributes.Request.Http.Path = fwdAuthURI.EscapedPath()
if fwdAuthURI.RawQuery != "" {
in.Attributes.Request.Http.Path += "?" + fwdAuthURI.RawQuery
}
}
rawJWT, _ := loadRawSession(hreq, a.currentOptions.Load(), state.encoder)
sessionState, _ := loadSession(state.encoder, rawJWT)
u, err := a.forceSync(ctx, sessionState)
if err != nil {
log.Warn().Err(err).Msg("clearing session due to force sync failed")
sessionState = nil
}
req, err := a.getEvaluatorRequestFromCheckRequest(in, sessionState)
if err != nil {
log.Warn().Err(err).Msg("error building evaluator request")
return nil, err
}
reply, err := state.evaluator.Evaluate(ctx, req)
if err != nil {
log.Error().Err(err).Msg("error during OPA evaluation")
return nil, err
}
defer func() {
a.logAuthorizeCheck(ctx, in, out, reply, u)
}()
switch {
case reply.Status == http.StatusOK:
return a.okResponse(reply), nil
case reply.Status == http.StatusUnauthorized:
if isForwardAuth && hreq.URL.Path == "/verify" {
return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil)
}
return a.redirectResponse(in)
}
return a.deniedResponse(in, int32(reply.Status), reply.Message, nil)
}
func getForwardAuthURL(r *http.Request) *url.URL {
urqQuery := r.URL.Query().Get("uri")
u, _ := urlutil.ParseAndValidateURL(urqQuery)
if u == nil {
u = &url.URL{
Scheme: r.Header.Get(httputil.HeaderForwardedProto),
Host: r.Header.Get(httputil.HeaderForwardedHost),
Path: r.Header.Get(httputil.HeaderForwardedURI),
}
}
originalURL := r.Header.Get(httputil.HeaderOriginalURL)
if originalURL != "" {
k, _ := urlutil.ParseAndValidateURL(originalURL)
if k != nil {
u = k
}
}
return u
}
// isForwardAuth returns if the current request is a forward auth route.
func (a *Authorize) isForwardAuth(req *envoy_service_auth_v3.CheckRequest) bool {
opts := a.currentOptions.Load()
forwardAuthURL, err := opts.GetForwardAuthURL()
if err != nil || forwardAuthURL == nil {
return false
}
checkURL := getCheckRequestURL(req)
return urlutil.StripPort(checkURL.Host) == urlutil.StripPort(forwardAuthURL.Host)
}
func (a *Authorize) getEvaluatorRequestFromCheckRequest(
in *envoy_service_auth_v3.CheckRequest,
sessionState *sessions.State,
) (*evaluator.Request, error) {
requestURL := getCheckRequestURL(in)
req := &evaluator.Request{
HTTP: evaluator.RequestHTTP{
Method: in.GetAttributes().GetRequest().GetHttp().GetMethod(),
URL: requestURL.String(),
Headers: getCheckRequestHeaders(in),
ClientCertificate: getPeerCertificate(in),
},
}
if sessionState != nil {
req.Session = evaluator.RequestSession{
ID: sessionState.ID,
}
}
p := a.getMatchingPolicy(requestURL)
if p != nil {
for _, sp := range p.SubPolicies {
req.CustomPolicies = append(req.CustomPolicies, sp.Rego...)
}
}
ca, err := a.getDownstreamClientCA(p)
if err != nil {
return nil, err
}
req.ClientCA = ca
return req, nil
}
func (a *Authorize) getDownstreamClientCA(policy *config.Policy) (string, error) {
options := a.currentOptions.Load()
if policy != nil && policy.TLSDownstreamClientCA != "" {
bs, err := base64.StdEncoding.DecodeString(policy.TLSDownstreamClientCA)
if err != nil {
return "", err
}
return string(bs), nil
}
ca, err := options.GetClientCA()
if err != nil {
return "", err
}
return string(ca), nil
}
func (a *Authorize) getMatchingPolicy(requestURL url.URL) *config.Policy {
options := a.currentOptions.Load()
for _, p := range options.GetAllPolicies() {
if p.Matches(requestURL) {
return &p
}
}
return nil
}
func getHTTPRequestFromCheckRequest(req *envoy_service_auth_v3.CheckRequest) *http.Request {
hattrs := req.GetAttributes().GetRequest().GetHttp()
u := getCheckRequestURL(req)
hreq := &http.Request{
Method: hattrs.GetMethod(),
URL: &u,
Header: make(http.Header),
Body: ioutil.NopCloser(strings.NewReader(hattrs.GetBody())),
Host: hattrs.GetHost(),
RequestURI: hattrs.GetPath(),
}
for k, v := range getCheckRequestHeaders(req) {
hreq.Header.Set(k, v)
}
return hreq
}
func getCheckRequestHeaders(req *envoy_service_auth_v3.CheckRequest) map[string]string {
hdrs := make(map[string]string)
ch := req.GetAttributes().GetRequest().GetHttp().GetHeaders()
for k, v := range ch {
hdrs[http.CanonicalHeaderKey(k)] = v
}
return hdrs
}
func getCheckRequestURL(req *envoy_service_auth_v3.CheckRequest) url.URL {
h := req.GetAttributes().GetRequest().GetHttp()
u := url.URL{
Scheme: h.GetScheme(),
Host: h.GetHost(),
}
u.Host = urlutil.GetDomainsForURL(u)[0]
// envoy sends the query string as part of the path
path := h.GetPath()
if idx := strings.Index(path, "?"); idx != -1 {
u.Path, u.RawQuery = path[:idx], path[idx+1:]
} else {
u.Path = path
}
return u
}
// getPeerCertificate gets the PEM-encoded peer certificate from the check request
func getPeerCertificate(in *envoy_service_auth_v3.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
}