mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-05 04:13:11 +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"
|
||||
"errors"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
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 {
|
||||
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{
|
||||
Status: &status.Status{Code: int32(codes.OK), Message: "OK"},
|
||||
HttpResponse: &envoy_service_auth_v3.CheckResponse_OkResponse{
|
||||
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(
|
||||
ctx context.Context,
|
||||
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) {
|
||||
respHeader := []*envoy_config_core_v3.HeaderValueOption{}
|
||||
if headers == nil {
|
||||
headers = make(http.Header)
|
||||
}
|
||||
|
||||
var respBody []byte
|
||||
|
||||
|
@ -163,25 +157,21 @@ func (a *Authorize) deniedResponse(
|
|||
"reason": statusReason, // must correspond to k8s StatusReason strings
|
||||
"code": code, // http code
|
||||
})
|
||||
respHeader = append(respHeader,
|
||||
mkHeader("Content-Type", "application/json"))
|
||||
headers.Set("Content-Type", "application/json")
|
||||
case getCheckRequestURL(in).Path == "/robots.txt":
|
||||
code = 200
|
||||
respBody = []byte("User-agent: *\nDisallow: /")
|
||||
respHeader = append(respHeader,
|
||||
mkHeader("Content-Type", "text/plain"))
|
||||
headers.Set("Content-Type", "text/plain")
|
||||
case isJSONWebRequest(in):
|
||||
respBody, _ = json.Marshal(map[string]any{
|
||||
"error": reason,
|
||||
"request_id": requestid.FromContext(ctx),
|
||||
})
|
||||
respHeader = append(respHeader,
|
||||
mkHeader("Content-Type", "application/json"))
|
||||
headers.Set("Content-Type", "application/json")
|
||||
case isGRPCRequest(in):
|
||||
return deniedResponseForGRPC(code, reason, headers), nil
|
||||
case isGRPCWebRequest(in):
|
||||
respHeader = append(respHeader,
|
||||
mkHeader("Content-Type", "application/grpc-web+json"),
|
||||
mkHeader("grpc-status", strconv.Itoa(int(codes.Unauthenticated))),
|
||||
mkHeader("grpc-message", codes.Unauthenticated.String()))
|
||||
return deniedResponseForGRPCWeb(code, reason, headers), nil
|
||||
default:
|
||||
// create a http response writer recorder
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -210,27 +200,12 @@ func (a *Authorize) deniedResponse(
|
|||
log.Ctx(ctx).Error().Err(err).Msg("error executing error template")
|
||||
return nil, err
|
||||
}
|
||||
// convert go headers to envoy headers
|
||||
respHeader = append(respHeader, toEnvoyHeaders(resp.Header)...)
|
||||
for k, vs := range resp.Header {
|
||||
headers[k] = vs
|
||||
}
|
||||
}
|
||||
|
||||
// add any additional headers
|
||||
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
|
||||
return mkDeniedCheckResponse(code, headers, string(respBody)), nil
|
||||
}
|
||||
|
||||
func (a *Authorize) requireLoginResponse(
|
||||
|
@ -242,7 +217,7 @@ func (a *Authorize) requireLoginResponse(
|
|||
state := a.state.Load()
|
||||
|
||||
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)
|
||||
|
@ -260,8 +235,8 @@ func (a *Authorize) requireLoginResponse(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return a.deniedResponse(ctx, in, http.StatusFound, "Login", map[string]string{
|
||||
"Location": redirectTo,
|
||||
return a.deniedResponse(ctx, in, http.StatusFound, "Login", http.Header{
|
||||
"Location": {redirectTo},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -285,7 +260,7 @@ func (a *Authorize) requireWebAuthnResponse(
|
|||
}
|
||||
|
||||
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{}
|
||||
|
@ -303,11 +278,26 @@ func (a *Authorize) requireWebAuthnResponse(
|
|||
}
|
||||
q.Set(urlutil.QueryIdentityProviderID, idp.GetId())
|
||||
signinURL := urlutil.WebAuthnURL(getHTTPRequestFromCheckRequest(in), &checkRequestURL, state.sharedKey, q)
|
||||
return a.deniedResponse(ctx, in, http.StatusFound, "Login", map[string]string{
|
||||
"Location": signinURL,
|
||||
return a.deniedResponse(ctx, in, http.StatusFound, "Login", http.Header{
|
||||
"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 {
|
||||
return &envoy_config_core_v3.HeaderValueOption{
|
||||
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 {
|
||||
var ks []string
|
||||
for k := range headers {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
sort.Strings(ks)
|
||||
|
||||
envoyHeaders := make([]*envoy_config_core_v3.HeaderValueOption, 0, len(headers))
|
||||
for _, k := range ks {
|
||||
envoyHeaders = append(envoyHeaders, mkHeader(k, headers.Get(k)))
|
||||
for k, vs := range maps.All(headers) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -363,7 +350,7 @@ func (a *Authorize) shouldRedirect(in *envoy_service_auth_v3.CheckRequest) bool
|
|||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(requestHeaders["content-type"], "application/grpc") {
|
||||
if isGRPCRequest(in) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -387,29 +374,6 @@ func (a *Authorize) shouldRedirect(in *envoy_service_auth_v3.CheckRequest) bool
|
|||
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 {
|
||||
hdrs := in.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
||||
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)
|
||||
})
|
||||
|
||||
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.Parallel()
|
||||
ctx := context.Background()
|
||||
|
@ -256,11 +300,11 @@ func TestAuthorize_deniedResponse(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
||||
"header": { "key": "grpc-status", "value": "16" }
|
||||
"header": { "key": "grpc-message", "value": "ERROR" }
|
||||
},
|
||||
{
|
||||
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
||||
"header": { "key": "grpc-message", "value": "Unauthenticated" }
|
||||
"header": { "key": "grpc-status", "value": "13" }
|
||||
}
|
||||
],
|
||||
"status": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue