mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-08 13:52:53 +02:00
authorize: handle gRPC requests (#5400)
This commit is contained in:
parent
84da474816
commit
85ef08b3a0
3 changed files with 170 additions and 81 deletions
|
@ -5,11 +5,11 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
|
@ -109,19 +109,11 @@ func invalidClientCertReason(reasons criteria.Reasons) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) okResponse(headers http.Header) *envoy_service_auth_v3.CheckResponse {
|
func (a *Authorize) okResponse(headers http.Header) *envoy_service_auth_v3.CheckResponse {
|
||||||
var requestHeaders []*envoy_config_core_v3.HeaderValueOption
|
|
||||||
for k, vs := range headers {
|
|
||||||
requestHeaders = append(requestHeaders, mkHeader(k, strings.Join(vs, ",")))
|
|
||||||
}
|
|
||||||
// ensure request headers are sorted by key for deterministic output
|
|
||||||
sort.Slice(requestHeaders, func(i, j int) bool {
|
|
||||||
return requestHeaders[i].Header.Key < requestHeaders[j].Header.Value
|
|
||||||
})
|
|
||||||
return &envoy_service_auth_v3.CheckResponse{
|
return &envoy_service_auth_v3.CheckResponse{
|
||||||
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
||||||
HttpResponse: &envoy_service_auth_v3.CheckResponse_OkResponse{
|
HttpResponse: &envoy_service_auth_v3.CheckResponse_OkResponse{
|
||||||
OkResponse: &envoy_service_auth_v3.OkHttpResponse{
|
OkResponse: &envoy_service_auth_v3.OkHttpResponse{
|
||||||
Headers: requestHeaders,
|
Headers: toEnvoyHeaders(headers),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -130,9 +122,11 @@ func (a *Authorize) okResponse(headers http.Header) *envoy_service_auth_v3.Check
|
||||||
func (a *Authorize) deniedResponse(
|
func (a *Authorize) deniedResponse(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
in *envoy_service_auth_v3.CheckRequest,
|
in *envoy_service_auth_v3.CheckRequest,
|
||||||
code int32, reason string, headers map[string]string,
|
code int32, reason string, headers http.Header,
|
||||||
) (*envoy_service_auth_v3.CheckResponse, error) {
|
) (*envoy_service_auth_v3.CheckResponse, error) {
|
||||||
respHeader := []*envoy_config_core_v3.HeaderValueOption{}
|
if headers == nil {
|
||||||
|
headers = make(http.Header)
|
||||||
|
}
|
||||||
|
|
||||||
var respBody []byte
|
var respBody []byte
|
||||||
|
|
||||||
|
@ -163,25 +157,21 @@ func (a *Authorize) deniedResponse(
|
||||||
"reason": statusReason, // must correspond to k8s StatusReason strings
|
"reason": statusReason, // must correspond to k8s StatusReason strings
|
||||||
"code": code, // http code
|
"code": code, // http code
|
||||||
})
|
})
|
||||||
respHeader = append(respHeader,
|
headers.Set("Content-Type", "application/json")
|
||||||
mkHeader("Content-Type", "application/json"))
|
|
||||||
case getCheckRequestURL(in).Path == "/robots.txt":
|
case getCheckRequestURL(in).Path == "/robots.txt":
|
||||||
code = 200
|
code = 200
|
||||||
respBody = []byte("User-agent: *\nDisallow: /")
|
respBody = []byte("User-agent: *\nDisallow: /")
|
||||||
respHeader = append(respHeader,
|
headers.Set("Content-Type", "text/plain")
|
||||||
mkHeader("Content-Type", "text/plain"))
|
|
||||||
case isJSONWebRequest(in):
|
case isJSONWebRequest(in):
|
||||||
respBody, _ = json.Marshal(map[string]any{
|
respBody, _ = json.Marshal(map[string]any{
|
||||||
"error": reason,
|
"error": reason,
|
||||||
"request_id": requestid.FromContext(ctx),
|
"request_id": requestid.FromContext(ctx),
|
||||||
})
|
})
|
||||||
respHeader = append(respHeader,
|
headers.Set("Content-Type", "application/json")
|
||||||
mkHeader("Content-Type", "application/json"))
|
case isGRPCRequest(in):
|
||||||
|
return deniedResponseForGRPC(code, reason, headers), nil
|
||||||
case isGRPCWebRequest(in):
|
case isGRPCWebRequest(in):
|
||||||
respHeader = append(respHeader,
|
return deniedResponseForGRPCWeb(code, reason, headers), nil
|
||||||
mkHeader("Content-Type", "application/grpc-web+json"),
|
|
||||||
mkHeader("grpc-status", strconv.Itoa(int(codes.Unauthenticated))),
|
|
||||||
mkHeader("grpc-message", codes.Unauthenticated.String()))
|
|
||||||
default:
|
default:
|
||||||
// create a http response writer recorder
|
// create a http response writer recorder
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
@ -210,27 +200,12 @@ func (a *Authorize) deniedResponse(
|
||||||
log.Ctx(ctx).Error().Err(err).Msg("error executing error template")
|
log.Ctx(ctx).Error().Err(err).Msg("error executing error template")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// convert go headers to envoy headers
|
for k, vs := range resp.Header {
|
||||||
respHeader = append(respHeader, toEnvoyHeaders(resp.Header)...)
|
headers[k] = vs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add any additional headers
|
return mkDeniedCheckResponse(code, headers, string(respBody)), nil
|
||||||
for k, v := range headers {
|
|
||||||
respHeader = append(respHeader, mkHeader(k, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &envoy_service_auth_v3.CheckResponse{
|
|
||||||
Status: &status.Status{Code: int32(codes.PermissionDenied), Message: "Access Denied"},
|
|
||||||
HttpResponse: &envoy_service_auth_v3.CheckResponse_DeniedResponse{
|
|
||||||
DeniedResponse: &envoy_service_auth_v3.DeniedHttpResponse{
|
|
||||||
Status: &envoy_type_v3.HttpStatus{
|
|
||||||
Code: envoy_type_v3.StatusCode(code),
|
|
||||||
},
|
|
||||||
Headers: respHeader,
|
|
||||||
Body: string(respBody),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authorize) requireLoginResponse(
|
func (a *Authorize) requireLoginResponse(
|
||||||
|
@ -242,7 +217,7 @@ func (a *Authorize) requireLoginResponse(
|
||||||
state := a.state.Load()
|
state := a.state.Load()
|
||||||
|
|
||||||
if !a.shouldRedirect(in) {
|
if !a.shouldRedirect(in) {
|
||||||
return a.deniedResponse(ctx, in, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), nil)
|
return a.deniedResponse(ctx, in, http.StatusUnauthorized, "Unauthenticated", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
idp, err := options.GetIdentityProviderForPolicy(request.Policy)
|
idp, err := options.GetIdentityProviderForPolicy(request.Policy)
|
||||||
|
@ -260,8 +235,8 @@ func (a *Authorize) requireLoginResponse(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.deniedResponse(ctx, in, http.StatusFound, "Login", map[string]string{
|
return a.deniedResponse(ctx, in, http.StatusFound, "Login", http.Header{
|
||||||
"Location": redirectTo,
|
"Location": {redirectTo},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,7 +260,7 @@ func (a *Authorize) requireWebAuthnResponse(
|
||||||
}
|
}
|
||||||
|
|
||||||
if !a.shouldRedirect(in) {
|
if !a.shouldRedirect(in) {
|
||||||
return a.deniedResponse(ctx, in, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), nil)
|
return a.deniedResponse(ctx, in, http.StatusUnauthorized, "Unauthenticated", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
|
@ -303,11 +278,26 @@ func (a *Authorize) requireWebAuthnResponse(
|
||||||
}
|
}
|
||||||
q.Set(urlutil.QueryIdentityProviderID, idp.GetId())
|
q.Set(urlutil.QueryIdentityProviderID, idp.GetId())
|
||||||
signinURL := urlutil.WebAuthnURL(getHTTPRequestFromCheckRequest(in), &checkRequestURL, state.sharedKey, q)
|
signinURL := urlutil.WebAuthnURL(getHTTPRequestFromCheckRequest(in), &checkRequestURL, state.sharedKey, q)
|
||||||
return a.deniedResponse(ctx, in, http.StatusFound, "Login", map[string]string{
|
return a.deniedResponse(ctx, in, http.StatusFound, "Login", http.Header{
|
||||||
"Location": signinURL,
|
"Location": {signinURL},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mkDeniedCheckResponse(httpStatusCode int32, headers http.Header, body string) *envoy_service_auth_v3.CheckResponse {
|
||||||
|
return &envoy_service_auth_v3.CheckResponse{
|
||||||
|
Status: &status.Status{Code: int32(codes.PermissionDenied), Message: "Access Denied"},
|
||||||
|
HttpResponse: &envoy_service_auth_v3.CheckResponse_DeniedResponse{
|
||||||
|
DeniedResponse: &envoy_service_auth_v3.DeniedHttpResponse{
|
||||||
|
Status: &envoy_type_v3.HttpStatus{
|
||||||
|
Code: envoy_type_v3.StatusCode(httpStatusCode),
|
||||||
|
},
|
||||||
|
Headers: toEnvoyHeaders(headers),
|
||||||
|
Body: body,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mkHeader(k, v string) *envoy_config_core_v3.HeaderValueOption {
|
func mkHeader(k, v string) *envoy_config_core_v3.HeaderValueOption {
|
||||||
return &envoy_config_core_v3.HeaderValueOption{
|
return &envoy_config_core_v3.HeaderValueOption{
|
||||||
Header: &envoy_config_core_v3.HeaderValue{
|
Header: &envoy_config_core_v3.HeaderValue{
|
||||||
|
@ -319,16 +309,13 @@ func mkHeader(k, v string) *envoy_config_core_v3.HeaderValueOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
func toEnvoyHeaders(headers http.Header) []*envoy_config_core_v3.HeaderValueOption {
|
func toEnvoyHeaders(headers http.Header) []*envoy_config_core_v3.HeaderValueOption {
|
||||||
var ks []string
|
|
||||||
for k := range headers {
|
|
||||||
ks = append(ks, k)
|
|
||||||
}
|
|
||||||
sort.Strings(ks)
|
|
||||||
|
|
||||||
envoyHeaders := make([]*envoy_config_core_v3.HeaderValueOption, 0, len(headers))
|
envoyHeaders := make([]*envoy_config_core_v3.HeaderValueOption, 0, len(headers))
|
||||||
for _, k := range ks {
|
for k, vs := range maps.All(headers) {
|
||||||
envoyHeaders = append(envoyHeaders, mkHeader(k, headers.Get(k)))
|
envoyHeaders = append(envoyHeaders, mkHeader(k, strings.Join(vs, ",")))
|
||||||
}
|
}
|
||||||
|
sort.Slice(envoyHeaders, func(i, j int) bool {
|
||||||
|
return envoyHeaders[i].GetHeader().GetKey() < envoyHeaders[j].GetHeader().GetKey()
|
||||||
|
})
|
||||||
return envoyHeaders
|
return envoyHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,7 +350,7 @@ func (a *Authorize) shouldRedirect(in *envoy_service_auth_v3.CheckRequest) bool
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(requestHeaders["content-type"], "application/grpc") {
|
if isGRPCRequest(in) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,29 +374,6 @@ func (a *Authorize) shouldRedirect(in *envoy_service_auth_v3.CheckRequest) bool
|
||||||
return mediaType == "text/html"
|
return mediaType == "text/html"
|
||||||
}
|
}
|
||||||
|
|
||||||
func isGRPCWebRequest(in *envoy_service_auth_v3.CheckRequest) bool {
|
|
||||||
hdrs := in.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
|
||||||
if hdrs == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
v := getHeader(hdrs, "Accept")
|
|
||||||
if v == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
accept, err := rfc7231.ParseAccept(v)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaType, _ := accept.MostAcceptable([]string{
|
|
||||||
"text/html",
|
|
||||||
"application/grpc-web-text",
|
|
||||||
})
|
|
||||||
return mediaType == "application/grpc-web-text"
|
|
||||||
}
|
|
||||||
|
|
||||||
func isJSONWebRequest(in *envoy_service_auth_v3.CheckRequest) bool {
|
func isJSONWebRequest(in *envoy_service_auth_v3.CheckRequest) bool {
|
||||||
hdrs := in.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
hdrs := in.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
||||||
if hdrs == nil {
|
if hdrs == nil {
|
||||||
|
|
81
authorize/check_response_grpc.go
Normal file
81
authorize/check_response_grpc.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package authorize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
||||||
|
"github.com/tniswong/go.rfcx/rfc7231"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isGRPCRequest(in *envoy_service_auth_v3.CheckRequest) bool {
|
||||||
|
hdrs := in.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
||||||
|
if hdrs == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hdrs["content-type"] == "application/grpc" || strings.HasPrefix(hdrs["content-type"], "application/grpc+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGRPCWebRequest(in *envoy_service_auth_v3.CheckRequest) bool {
|
||||||
|
hdrs := in.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
||||||
|
if hdrs == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
v := getHeader(hdrs, "Accept")
|
||||||
|
if v == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
accept, err := rfc7231.ParseAccept(v)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaType, _ := accept.MostAcceptable([]string{
|
||||||
|
"text/html",
|
||||||
|
"application/grpc-web-text",
|
||||||
|
})
|
||||||
|
return mediaType == "application/grpc-web-text"
|
||||||
|
}
|
||||||
|
|
||||||
|
func deniedResponseForGRPC(
|
||||||
|
code int32, reason string, headers http.Header,
|
||||||
|
) *envoy_service_auth_v3.CheckResponse {
|
||||||
|
headers.Set("Content-Type", "application/grpc+json")
|
||||||
|
headers["grpc-status"] = []string{strconv.Itoa(int(httpStatusCodeToGRPCStatusCode(code)))}
|
||||||
|
headers["grpc-message"] = []string{reason}
|
||||||
|
return mkDeniedCheckResponse(code, headers, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func deniedResponseForGRPCWeb(
|
||||||
|
code int32, reason string, headers http.Header,
|
||||||
|
) *envoy_service_auth_v3.CheckResponse {
|
||||||
|
headers.Set("Content-Type", "application/grpc-web+json")
|
||||||
|
headers["grpc-status"] = []string{strconv.Itoa(int(httpStatusCodeToGRPCStatusCode(code)))}
|
||||||
|
headers["grpc-message"] = []string{reason}
|
||||||
|
return mkDeniedCheckResponse(code, headers, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpStatusCodeToGRPCStatusCode(httpStatusCode int32) codes.Code {
|
||||||
|
// from https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
|
||||||
|
switch httpStatusCode {
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
return codes.Internal
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
return codes.Unauthenticated
|
||||||
|
case http.StatusForbidden:
|
||||||
|
return codes.PermissionDenied
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return codes.Unimplemented
|
||||||
|
case http.StatusTooManyRequests,
|
||||||
|
http.StatusBadGateway,
|
||||||
|
http.StatusServiceUnavailable,
|
||||||
|
http.StatusGatewayTimeout:
|
||||||
|
return codes.Unavailable
|
||||||
|
default:
|
||||||
|
return codes.Unknown
|
||||||
|
}
|
||||||
|
}
|
|
@ -230,6 +230,50 @@ func TestAuthorize_deniedResponse(t *testing.T) {
|
||||||
}`, res)
|
}`, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("grpc", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = requestid.WithValue(ctx, "REQUESTID")
|
||||||
|
|
||||||
|
res, err := a.deniedResponse(ctx, &envoy_service_auth_v3.CheckRequest{
|
||||||
|
Attributes: &envoy_service_auth_v3.AttributeContext{
|
||||||
|
Request: &envoy_service_auth_v3.AttributeContext_Request{
|
||||||
|
Http: &envoy_service_auth_v3.AttributeContext_HttpRequest{
|
||||||
|
Headers: map[string]string{
|
||||||
|
"content-type": "application/grpc+json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, http.StatusBadRequest, "ERROR", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
testutil.AssertProtoJSONEqual(t, `{
|
||||||
|
"deniedResponse": {
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
||||||
|
"header": { "key": "Content-Type", "value": "application/grpc+json" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
||||||
|
"header": { "key": "grpc-message", "value": "ERROR" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
||||||
|
"header": { "key": "grpc-status", "value": "13" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": {
|
||||||
|
"code": "BadRequest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"code": 7,
|
||||||
|
"message": "Access Denied"
|
||||||
|
}
|
||||||
|
}`, res)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("grpc-web", func(t *testing.T) {
|
t.Run("grpc-web", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
@ -256,11 +300,11 @@ func TestAuthorize_deniedResponse(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
||||||
"header": { "key": "grpc-status", "value": "16" }
|
"header": { "key": "grpc-message", "value": "ERROR" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
||||||
"header": { "key": "grpc-message", "value": "Unauthenticated" }
|
"header": { "key": "grpc-status", "value": "13" }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"status": {
|
"status": {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue