Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for envoy gRPC v3 external authorization API #469

Merged
merged 77 commits into from
Mar 6, 2023
Merged
Changes from 1 commit
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
dba4695
initial commit for envoy grpc support
dadrus Jan 29, 2023
dd46c22
access context subject and error handling moved to its own package
dadrus Jan 30, 2023
137bd75
further implementation
dadrus Jan 31, 2023
39cf037
package structure refactored
dadrus Jan 31, 2023
362f41c
small updates in descriptions
dadrus Jan 31, 2023
b0cf884
metris implementation updated to take tunneled http requests from env…
dadrus Jan 31, 2023
602e667
type label added
dadrus Jan 31, 2023
e4fc676
type label changed
dadrus Jan 31, 2023
478ae0d
Merge branch 'main' into feat/envoy_grpc
dadrus Jan 31, 2023
94f47b0
linter warnings resolved
dadrus Feb 1, 2023
c9a30ad
tests fixed
dadrus Feb 1, 2023
8c40761
additional labels for metrics removed
dadrus Feb 1, 2023
e35ec75
namcpace set to grpc
dadrus Feb 1, 2023
5fbda2e
Merge branch 'main' into feat/envoy_grpc
dadrus Feb 1, 2023
94bdc35
access logger implementation simplified
dadrus Feb 2, 2023
a65c95e
further simplifications
dadrus Feb 2, 2023
63ebc47
unused parameters
dadrus Feb 2, 2023
8465e26
log message changed
dadrus Feb 3, 2023
9b5d5f0
grpc error handler middleware implementation enhanced
dadrus Feb 3, 2023
0a19758
Merge branch 'main' into feat/envoy_grpc
dadrus Feb 6, 2023
30b7565
idle timeout configured
dadrus Feb 8, 2023
3d0c2f7
Merge branch 'main' into feat/envoy_grpc
dadrus Feb 8, 2023
17a7491
Merge branch 'main' into feat/envoy_grpc
dadrus Feb 8, 2023
3eb4e2c
method renamed
dadrus Feb 8, 2023
81ea03c
header canonicalization and some simplifications
dadrus Feb 8, 2023
fa36179
tests for grpc access log interceptor
dadrus Feb 9, 2023
45cc2e0
some renamings & more tests
dadrus Feb 9, 2023
d103adb
linter warnings disabled for specific places as errors are irrelevant
dadrus Feb 9, 2023
2facb5f
small refactorings & simplifications
dadrus Feb 9, 2023
134f418
some renaming and more tests
dadrus Feb 9, 2023
12d9fb4
functionality related to format negotiation on verbose errors moved t…
dadrus Feb 9, 2023
66dffeb
comment added
dadrus Feb 9, 2023
9fb141d
linter enabled again
dadrus Feb 9, 2023
485891b
file renamed
dadrus Feb 9, 2023
6472ea4
error content negotiation simplified & more tests
dadrus Feb 10, 2023
494e2cb
forgotten go.mod & sum
dadrus Feb 10, 2023
0f4adcf
http label names updated to be more meaningfull especially, when the …
dadrus Feb 10, 2023
3ada333
fixes and more tests
dadrus Feb 10, 2023
6254ab6
license header added
dadrus Feb 11, 2023
71c3a8d
Merge branch 'main' into feat/envoy_grpc
dadrus Feb 22, 2023
5a16c5b
more tests and handling of unknown services/methods in access log and…
dadrus Mar 3, 2023
ae7fb34
Merge branch 'main' into feat/envoy_grpc
dadrus Mar 3, 2023
1969e41
linter warnings resolved
dadrus Mar 3, 2023
d2906d8
license header added
dadrus Mar 3, 2023
7664ff9
some updates to old tests
dadrus Mar 3, 2023
8aba38a
small test refactorings to simplify them
dadrus Mar 3, 2023
ebd7632
new tests and fixes for found bugs
dadrus Mar 3, 2023
438f18d
more tests
dadrus Mar 3, 2023
42ec809
new tests and fixes for identified bugs
dadrus Mar 3, 2023
b9691c2
test enhanced
dadrus Mar 3, 2023
0b40826
new test to bootstrap envoyproxy decision service
dadrus Mar 3, 2023
fd8bfd3
metrics configured in test
dadrus Mar 3, 2023
96c5a5b
mock moved
dadrus Mar 3, 2023
37762a3
Merge branch 'main' into feat/envoy_grpc
dadrus Mar 3, 2023
6d2328d
new test
dadrus Mar 3, 2023
e125e59
Merge branch 'main' into feat/envoy_grpc
dadrus Mar 4, 2023
fb319b8
logged accesslog events updated
dadrus Mar 4, 2023
4bc7a8a
additional check to avoid nil map access
dadrus Mar 4, 2023
dee4c86
using x-forwarded-for set by envoy to retrieve ips of the hops
dadrus Mar 4, 2023
d7814be
Merge branch 'main' into feat/envoy_grpc
dadrus Mar 4, 2023
25da4e1
linter warning resolved
dadrus Mar 5, 2023
78adaa1
using rpc status code instead of an HTTP one as required by the API
dadrus Mar 5, 2023
af8bb4c
test updated to check for rpc status code instead of HTTP one
dadrus Mar 5, 2023
c2c6e60
using codes from google package
dadrus Mar 5, 2023
a77ae26
proper use of grpc error codes
dadrus Mar 5, 2023
2266317
linter warnings resolved
dadrus Mar 5, 2023
25af22d
docker-compose quickstarts updateds to cover integration with envoy u…
dadrus Mar 5, 2023
8b29e0f
logging grpc status code
dadrus Mar 5, 2023
d5cdd5a
comment added
dadrus Mar 5, 2023
6b85527
linter warning resolved
dadrus Mar 5, 2023
8e38c3e
flag description updated
dadrus Mar 5, 2023
78ecabb
documentation added
dadrus Mar 5, 2023
b01429e
fla renamed
dadrus Mar 5, 2023
2dbaefd
documentation update
dadrus Mar 5, 2023
2c6c6ba
documentation update
dadrus Mar 5, 2023
fea779e
documentation update
dadrus Mar 5, 2023
06f196e
docker-compose quickstarts updated
dadrus Mar 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
grpc error handler middleware implementation enhanced
  • Loading branch information
dadrus committed Feb 3, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 9b5d5f0722caf06376ee415565f9b6051aba6f7a
Original file line number Diff line number Diff line change
@@ -30,8 +30,8 @@ var defaultOptions = opts{ //nolint:gochecknoglobals
internalError: responseWith(http.StatusInternalServerError),
}

func responseWith(code int) func(err error, verbose bool) (any, error) {
return func(err error, verbose bool) (any, error) {
return createDeniedResponse(code, err, verbose), nil
func responseWith(code int) func(err error, verbose bool, mimeType string) (any, error) {
return func(err error, verbose bool, mimeType string) (any, error) {
return errorResponse(code, err, verbose, mimeType), nil
}
}
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import (

envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/rs/zerolog"
"google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc"
@@ -47,7 +48,7 @@ type handler struct {
}

func (h *handler) handle(
ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (any, error) { //nolint:cyclop
res, err := handler(ctx, req)
if err == nil {
@@ -58,17 +59,17 @@ func (h *handler) handle(

switch {
case errors.Is(err, heimdall.ErrAuthentication):
return h.authenticationError(err, h.verboseErrors)
return h.authenticationError(err, h.verboseErrors, acceptType(req))
case errors.Is(err, heimdall.ErrAuthorization):
return h.authorizationError(err, h.verboseErrors)
return h.authorizationError(err, h.verboseErrors, acceptType(req))
case errors.Is(err, heimdall.ErrCommunicationTimeout) || errors.Is(err, heimdall.ErrCommunication):
return h.communicationError(err, h.verboseErrors)
return h.communicationError(err, h.verboseErrors, acceptType(req))
case errors.Is(err, heimdall.ErrArgument):
return h.preconditionError(err, h.verboseErrors)
return h.preconditionError(err, h.verboseErrors, acceptType(req))
case errors.Is(err, heimdall.ErrMethodNotAllowed):
return h.badMethodError(err, h.verboseErrors)
return h.badMethodError(err, h.verboseErrors, acceptType(req))
case errors.Is(err, heimdall.ErrNoRuleFound):
return h.noRuleError(err, h.verboseErrors)
return h.noRuleError(err, h.verboseErrors, acceptType(req))
case errors.Is(err, &heimdall.RedirectError{}):
var redirectError *heimdall.RedirectError

@@ -77,21 +78,32 @@ func (h *handler) handle(
return &envoy_auth.CheckResponse{
Status: &status.Status{Code: int32(redirectError.Code)},
HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{
DeniedResponse: &envoy_auth.DeniedHttpResponse{Headers: []*envoy_core.HeaderValueOption{
{
Header: &envoy_core.HeaderValue{
Key: "Location",
Value: redirectError.RedirectTo,
DeniedResponse: &envoy_auth.DeniedHttpResponse{
Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode(redirectError.Code)},
Headers: []*envoy_core.HeaderValueOption{
{
Header: &envoy_core.HeaderValue{
Key: "Location",
Value: redirectError.RedirectTo,
},
},
},
}},
},
},
}, nil

default:
logger := zerolog.Ctx(ctx)
logger.Error().Err(err).Msg("Internal error occurred")

return h.internalError(err, h.verboseErrors)
return h.internalError(err, h.verboseErrors, acceptType(req))
}
}

func acceptType(req any) string {
if req, ok := req.(*envoy_auth.CheckRequest); ok {
return req.Attributes.Request.Http.Headers["accept"]
}

return ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package errorhandler

import (
"encoding/xml"
"fmt"
"strings"

envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/goccy/go-json"
"google.golang.org/genproto/googleapis/rpc/status"
)

func errorResponse(code int, err error, verbose bool, mimeType string) *envoy_auth.CheckResponse {
if verbose {
body, responseType, _ := format(mimeType, err)

return &envoy_auth.CheckResponse{
Status: &status.Status{Code: int32(code)},
HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{
DeniedResponse: &envoy_auth.DeniedHttpResponse{
Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode(code)},
Headers: []*envoy_core.HeaderValueOption{
{
Header: &envoy_core.HeaderValue{Key: "Content-Type", Value: responseType},
},
},
Body: body,
},
},
}
}

return &envoy_auth.CheckResponse{
Status: &status.Status{Code: int32(code)},
HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{
DeniedResponse: &envoy_auth.DeniedHttpResponse{
Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode(code)},
},
},
}
}

func format(accepted string, body any) (string, string, error) {
contentType := negotiate(accepted, "text/html", "application/json", "test/plain", "application/xml")

switch contentType {
case "text/html":
return fmt.Sprintf("<p>%s</p>", body), contentType, nil
case "application/json":
res, err := json.Marshal(body)

return string(res), contentType, err
case "application/xml":
res, err := xml.Marshal(body)

return string(res), contentType, err
case "test/plain":
fallthrough
default:
return fmt.Sprintf("%s", body), contentType, nil
}
}

func negotiate(accepted string, offered ...string) string {
if len(accepted) == 0 {
return offered[0]
}

spec, commaPos, header := "", 0, accepted
for len(header) > 0 && commaPos != -1 {
commaPos = strings.IndexByte(header, ',')
if commaPos != -1 {
spec = strings.Trim(header[:commaPos], " ")
} else {
spec = strings.TrimLeft(header, " ")
}

if factorSign := strings.IndexByte(spec, ';'); factorSign != -1 {
spec = spec[:factorSign]
}

for _, offer := range offered {
if len(offer) == 0 {
continue
} else if spec == "*/*" {
return offer
}

if strings.Contains(spec, offer) {
return offer
}
}

if commaPos != -1 {
header = header[commaPos+1:]
}
}

return ""
}
Original file line number Diff line number Diff line change
@@ -16,32 +16,24 @@

package errorhandler

import (
"fmt"

envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"google.golang.org/genproto/googleapis/rpc/status"
)

type opts struct {
verboseErrors bool
authenticationError func(err error, verbose bool) (any, error)
authorizationError func(err error, verbose bool) (any, error)
communicationError func(err error, verbose bool) (any, error)
preconditionError func(err error, verbose bool) (any, error)
badMethodError func(err error, verbose bool) (any, error)
noRuleError func(err error, verbose bool) (any, error)
internalError func(err error, verbose bool) (any, error)
authenticationError func(err error, verbose bool, mimeType string) (any, error)
authorizationError func(err error, verbose bool, mimeType string) (any, error)
communicationError func(err error, verbose bool, mimeType string) (any, error)
preconditionError func(err error, verbose bool, mimeType string) (any, error)
badMethodError func(err error, verbose bool, mimeType string) (any, error)
noRuleError func(err error, verbose bool, mimeType string) (any, error)
internalError func(err error, verbose bool, mimeType string) (any, error)
}

type Option func(*opts)

func WithPreconditionErrorCode(code int) Option {
return func(o *opts) {
if code != 0 {
o.preconditionError = func(err error, verbose bool) (any, error) {
return createDeniedResponse(code, err, verbose), nil
o.preconditionError = func(err error, verbose bool, mimeType string) (any, error) {
return errorResponse(code, err, verbose, mimeType), nil
}
}
}
@@ -50,8 +42,8 @@ func WithPreconditionErrorCode(code int) Option {
func WithAuthenticationErrorCode(code int) Option {
return func(o *opts) {
if code != 0 {
o.authenticationError = func(err error, verbose bool) (any, error) {
return createDeniedResponse(code, err, verbose), nil
o.authenticationError = func(err error, verbose bool, mimeType string) (any, error) {
return errorResponse(code, err, verbose, mimeType), nil
}
}
}
@@ -60,8 +52,8 @@ func WithAuthenticationErrorCode(code int) Option {
func WithAuthorizationErrorCode(code int) Option {
return func(o *opts) {
if code != 0 {
o.authorizationError = func(err error, verbose bool) (any, error) {
return createDeniedResponse(code, err, verbose), nil
o.authorizationError = func(err error, verbose bool, mimeType string) (any, error) {
return errorResponse(code, err, verbose, mimeType), nil
}
}
}
@@ -70,8 +62,8 @@ func WithAuthorizationErrorCode(code int) Option {
func WithCommunicationErrorCode(code int) Option {
return func(o *opts) {
if code != 0 {
o.communicationError = func(err error, verbose bool) (any, error) {
return createDeniedResponse(code, err, verbose), nil
o.communicationError = func(err error, verbose bool, mimeType string) (any, error) {
return errorResponse(code, err, verbose, mimeType), nil
}
}
}
@@ -80,8 +72,8 @@ func WithCommunicationErrorCode(code int) Option {
func WithInternalServerErrorCode(code int) Option {
return func(o *opts) {
if code != 0 {
o.internalError = func(err error, verbose bool) (any, error) {
return createDeniedResponse(code, err, verbose), nil
o.internalError = func(err error, verbose bool, mimeType string) (any, error) {
return errorResponse(code, err, verbose, mimeType), nil
}
}
}
@@ -90,8 +82,8 @@ func WithInternalServerErrorCode(code int) Option {
func WithMethodErrorCode(code int) Option {
return func(o *opts) {
if code != 0 {
o.badMethodError = func(err error, verbose bool) (any, error) {
return createDeniedResponse(code, err, verbose), nil
o.badMethodError = func(err error, verbose bool, mimeType string) (any, error) {
return errorResponse(code, err, verbose, mimeType), nil
}
}
}
@@ -100,8 +92,8 @@ func WithMethodErrorCode(code int) Option {
func WithNoRuleErrorCode(code int) Option {
return func(o *opts) {
if code != 0 {
o.noRuleError = func(err error, verbose bool) (any, error) {
return createDeniedResponse(code, err, verbose), nil
o.noRuleError = func(err error, verbose bool, mimeType string) (any, error) {
return errorResponse(code, err, verbose, mimeType), nil
}
}
}
@@ -112,28 +104,3 @@ func WithVerboseErrors(flag bool) Option {
o.verboseErrors = flag
}
}

func createDeniedResponse(code int, err error, verbose bool) *envoy_auth.CheckResponse {
return &envoy_auth.CheckResponse{
Status: &status.Status{Code: int32(code)},
HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{
DeniedResponse: &envoy_auth.DeniedHttpResponse{
Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode(code)},
Body: messageFrom(err, verbose),
},
},
}
}

func messageFrom(err error, verbose bool) string {
if !verbose {
return ""
}

// checking by intention if the outer error implements the fmt.Stringer interface
if se, ok := err.(fmt.Stringer); ok { // nolint: errorlint
return se.String()
}

return err.Error()
}