pomerium/authorize/grpc.go
Cuong Manh Le 4a3fb5d44b
authorize: get claims from signed jwt (#954)
authorize: get claims from signed jwt

When doing databroker refactoring, all claims information were moved to
signed JWT instead of raw session JWT. But we are still looking for
claims info in raw session JWT, causes all X-Pomerium-Claim-* headers
being gone.

Fix this by looking for information from signed JWT instead.

Note that even with this fix, the X-Pomerium-Claim-Groups is still not
present, but it's another bug (see #941) and will be fixed later.

Fixes #936
2020-06-22 09:51:32 +07:00

297 lines
8.7 KiB
Go

package authorize
import (
"context"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/golang/protobuf/ptypes"
"github.com/rs/zerolog"
"github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/internal/grpc/databroker"
"github.com/pomerium/pomerium/internal/grpc/session"
"github.com/pomerium/pomerium/internal/grpc/user"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/sessions"
"github.com/pomerium/pomerium/internal/telemetry/requestid"
"github.com/pomerium/pomerium/internal/telemetry/trace"
"github.com/pomerium/pomerium/internal/urlutil"
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"
)
var sessionTypeURL, userTypeURL string
func init() {
any, _ := ptypes.MarshalAny(new(session.Session))
sessionTypeURL = any.GetTypeUrl()
any, _ = ptypes.MarshalAny(new(user.User))
userTypeURL = any.GetTypeUrl()
}
// Check implements the envoy auth server gRPC endpoint.
func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRequest) (*envoy_service_auth_v2.CheckResponse, error) {
ctx, span := trace.StartSpan(ctx, "authorize.grpc.Check")
defer span.End()
// maybe rewrite http request for forward auth
isForwardAuth := a.handleForwardAuth(in)
hreq := getHTTPRequestFromCheckRequest(in)
rawJWT, _ := loadRawSession(hreq, a.currentOptions.Load(), a.currentEncoder.Load())
sessionState, _ := loadSession(a.currentEncoder.Load(), rawJWT)
// only accept sessions whose databroker server versions match
if sessionState != nil {
a.dataBrokerDataLock.RLock()
if a.dataBrokerSessionServerVersion != sessionState.Version {
log.Warn().
Str("server_version", a.dataBrokerSessionServerVersion).
Str("session_version", sessionState.Version).
Msg("clearing session due to invalid version")
sessionState = nil
}
a.dataBrokerDataLock.RUnlock()
}
if sessionState != nil {
a.forceSync(ctx, sessionState.ID)
}
a.dataBrokerDataLock.RLock()
defer a.dataBrokerDataLock.RUnlock()
req := a.getEvaluatorRequestFromCheckRequest(in, sessionState)
reply, err := a.pe.Evaluate(ctx, req)
if err != nil {
log.Error().Err(err).Msg("error during OPA evaluation")
return nil, err
}
logAuthorizeCheck(ctx, in, reply)
switch {
case reply.Status == http.StatusOK:
return a.okResponse(reply), nil
case reply.Status == http.StatusUnauthorized:
if isForwardAuth {
return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil), nil
}
return a.redirectResponse(in), nil
}
return a.deniedResponse(in, int32(reply.Status), reply.Message, nil), nil
}
func (a *Authorize) forceSync(ctx context.Context, sessionID string) {
s := a.forceSyncSession(ctx, sessionID)
if s == nil {
return
}
a.forceSyncUser(ctx, s.GetUserId())
}
func (a *Authorize) forceSyncSession(ctx context.Context, sessionID string) *session.Session {
a.dataBrokerDataLock.RLock()
s, ok := a.dataBrokerData.Get(sessionTypeURL, sessionID).(*session.Session)
a.dataBrokerDataLock.RUnlock()
if ok {
return s
}
res, err := a.dataBrokerClient.Get(ctx, &databroker.GetRequest{
Type: sessionTypeURL,
Id: sessionID,
})
if err != nil {
log.Warn().Err(err).Msg("failed to get session from databroker")
return nil
}
a.dataBrokerDataLock.Lock()
if current := a.dataBrokerData.Get(sessionTypeURL, sessionID); current == nil {
a.dataBrokerData.Update(res.GetRecord())
}
s, _ = a.dataBrokerData.Get(sessionTypeURL, sessionID).(*session.Session)
a.dataBrokerDataLock.Unlock()
return s
}
func (a *Authorize) forceSyncUser(ctx context.Context, userID string) *user.User {
a.dataBrokerDataLock.RLock()
s, ok := a.dataBrokerData.Get(userTypeURL, userID).(*user.User)
a.dataBrokerDataLock.RUnlock()
if ok {
return s
}
res, err := a.dataBrokerClient.Get(ctx, &databroker.GetRequest{
Type: userTypeURL,
Id: userID,
})
if err != nil {
log.Warn().Err(err).Msg("failed to get user from databroker")
return nil
}
a.dataBrokerDataLock.Lock()
if current := a.dataBrokerData.Get(userTypeURL, userID); current == nil {
a.dataBrokerData.Update(res.GetRecord())
}
s, _ = a.dataBrokerData.Get(userTypeURL, userID).(*user.User)
a.dataBrokerDataLock.Unlock()
return s
}
func (a *Authorize) getEnvoyRequestHeaders(signedJWT string) ([]*envoy_api_v2_core.HeaderValueOption, error) {
var hvos []*envoy_api_v2_core.HeaderValueOption
hdrs, err := a.getJWTClaimHeaders(a.currentOptions.Load(), signedJWT)
if err != nil {
return nil, err
}
for k, v := range hdrs {
hvos = append(hvos, mkHeader(k, v))
}
return hvos, nil
}
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.GetForwardAuthURL().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 (a *Authorize) getEvaluatorRequestFromCheckRequest(in *envoy_service_auth_v2.CheckRequest, sessionState *sessions.State) *evaluator.Request {
requestURL := getCheckRequestURL(in)
req := &evaluator.Request{
DataBrokerData: a.dataBrokerData,
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,
ImpersonateEmail: sessionState.ImpersonateEmail,
ImpersonateGroups: sessionState.ImpersonateGroups,
}
}
return req
}
func getHTTPRequestFromCheckRequest(req *envoy_service_auth_v2.CheckRequest) *http.Request {
hattrs := req.GetAttributes().GetRequest().GetHttp()
hreq := &http.Request{
Method: hattrs.GetMethod(),
URL: getCheckRequestURL(req),
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_v2.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_v2.CheckRequest) *url.URL {
h := req.GetAttributes().GetRequest().GetHttp()
u := &url.URL{
Scheme: h.GetScheme(),
Host: h.GetHost(),
}
// 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
}
if h.GetHeaders() != nil {
if fwdProto, ok := h.GetHeaders()["x-forwarded-proto"]; ok {
u.Scheme = fwdProto
}
}
return u
}
// 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 *evaluator.Result,
) {
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.Str("check-request-id", hdrs["X-Request-Id"])
evt = evt.Str("method", hattrs.GetMethod())
evt = evt.Str("path", hattrs.GetPath())
evt = evt.Str("host", hattrs.GetHost())
evt = evt.Str("query", hattrs.GetQuery())
// reply
if reply != nil {
evt = evt.Bool("allow", reply.Status == http.StatusOK)
evt = evt.Int("status", reply.Status)
evt = evt.Str("message", reply.Message)
}
// potentially sensitive, only log if debug mode
if zerolog.GlobalLevel() <= zerolog.DebugLevel {
evt = evt.Interface("headers", hdrs)
}
evt.Msg("authorize check")
}