From dba46955108db3cfabfe91240a150ab2736861f5 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 29 Jan 2023 13:29:39 +0100 Subject: [PATCH 01/67] initial commit for envoy grpc support --- go.mod | 5 +- go.sum | 9 +- .../handler/envoyextauthz/grpc/v3/handler.go | 60 ++++++++ .../handler/envoyextauthz/grpc/v3/module.go | 17 +++ .../envoyextauthz/grpc/v3/request_context.go | 129 ++++++++++++++++++ 5 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 internal/handler/envoyextauthz/grpc/v3/handler.go create mode 100644 internal/handler/envoyextauthz/grpc/v3/module.go create mode 100644 internal/handler/envoyextauthz/grpc/v3/request_context.go diff --git a/go.mod b/go.mod index 6da703093..2fc78b8c7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/dlclark/regexp2 v1.8.0 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 + github.com/envoyproxy/go-control-plane v0.10.3 github.com/fsnotify/fsnotify v1.6.0 github.com/go-co-op/gocron v1.18.0 github.com/go-logr/zerologr v1.2.2 @@ -49,6 +50,7 @@ require ( go.uber.org/fx v1.19.1 gocloud.dev v0.28.0 golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 + google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.26.1 @@ -96,7 +98,9 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/envoyproxy/protoc-gen-validate v0.6.13 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/go-logr/logr v1.2.3 // indirect @@ -168,7 +172,6 @@ require ( golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.103.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3 // indirect google.golang.org/grpc v1.51.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index cf5eb2aed..a69a2bae9 100644 --- a/go.sum +++ b/go.sum @@ -649,6 +649,7 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc h1:PYXxkRUBGUMa5xgMVMDl62vEklZvKpVaxQeN9ie7Hfk= github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= @@ -848,9 +849,11 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3 h1:xdCVXxEe0Y3FQith+0cj2irwZudqGYvecuLB1HtdexY= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.6.13 h1:TvDcILLkjuZV3ER58VkBmncKsLUBqBDxra/XctCzuMM= github.com/envoyproxy/protoc-gen-validate v0.6.13/go.mod h1:qEySVqXrEugbHKvmhI8ZqtQi75/RHSSRNpffvB4I6Bw= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -1720,8 +1723,6 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= -github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -2087,8 +2088,6 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws= -golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 h1:fiNkyhJPUvxbRPbCqY/D9qdjmPzfHcpK3P4bM4gioSY= golang.org/x/exp v0.0.0-20230118134722-a68e582fa157/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -2907,8 +2906,6 @@ k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.80.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= -k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M= k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= diff --git a/internal/handler/envoyextauthz/grpc/v3/handler.go b/internal/handler/envoyextauthz/grpc/v3/handler.go new file mode 100644 index 000000000..2f86e52fd --- /dev/null +++ b/internal/handler/envoyextauthz/grpc/v3/handler.go @@ -0,0 +1,60 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package v3 + +import ( + "context" + + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/rs/zerolog" + + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules" + "github.com/dadrus/heimdall/internal/x/errorchain" +) + +type Handler struct { + r rules.Repository + s heimdall.JWTSigner + code int +} + +func (h *Handler) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*envoy_auth.CheckResponse, error) { + logger := zerolog.Ctx(ctx) + logger.Debug().Msg("Decision Envoy ExtAuthZ endpoint called") + + reqCtx := NewRequestContext(ctx, req, h.s) + + rule, err := h.r.FindRule(reqCtx.RequestURL()) + if err != nil { + return nil, err + } + + if !rule.MatchesMethod(reqCtx.RequestMethod()) { + return nil, errorchain.NewWithMessagef(heimdall.ErrMethodNotAllowed, + "rule doesn't match %s method", reqCtx.RequestMethod()) + } + + _, err = rule.Execute(reqCtx) + if err != nil { + return nil, err + } + + logger.Debug().Msg("Finalizing request") + + return reqCtx.Finalize(h.code) +} diff --git a/internal/handler/envoyextauthz/grpc/v3/module.go b/internal/handler/envoyextauthz/grpc/v3/module.go new file mode 100644 index 000000000..5c98b8602 --- /dev/null +++ b/internal/handler/envoyextauthz/grpc/v3/module.go @@ -0,0 +1,17 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package v3 diff --git a/internal/handler/envoyextauthz/grpc/v3/request_context.go b/internal/handler/envoyextauthz/grpc/v3/request_context.go new file mode 100644 index 000000000..08a92f7d0 --- /dev/null +++ b/internal/handler/envoyextauthz/grpc/v3/request_context.go @@ -0,0 +1,129 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package v3 + +import ( + "context" + "fmt" + "net/http" + "net/url" + "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" + "google.golang.org/genproto/googleapis/rpc/status" + + "github.com/dadrus/heimdall/internal/heimdall" +) + +type RequestContext struct { + ctx context.Context + req *envoy_auth.CheckRequest + reqURL *url.URL + upstreamHeaders http.Header + upstreamCookies map[string]string + jwtSigner heimdall.JWTSigner + err error +} + +func NewRequestContext(ctx context.Context, req *envoy_auth.CheckRequest, signer heimdall.JWTSigner) *RequestContext { + return &RequestContext{ + ctx: ctx, + req: req, + reqURL: &url.URL{ + Scheme: req.Attributes.Request.Http.Scheme, + Host: req.Attributes.Request.Http.Host, + Path: req.Attributes.Request.Http.Path, + RawQuery: req.Attributes.Request.Http.Query, + Fragment: req.Attributes.Request.Http.Fragment, + }, + jwtSigner: signer, + upstreamHeaders: make(http.Header), + upstreamCookies: make(map[string]string), + } +} + +func (s *RequestContext) RequestMethod() string { return s.req.Attributes.Request.Http.Method } +func (s *RequestContext) RequestHeaders() map[string]string { + return s.req.Attributes.Request.Http.Headers +} +func (s *RequestContext) RequestHeader(name string) string { + return s.req.Attributes.Request.Http.Headers[name] +} +func (s *RequestContext) RequestCookie(name string) string { return "" } +func (s *RequestContext) RequestQueryParameter(name string) string { + return s.reqURL.Query().Get(name) +} +func (s *RequestContext) RequestFormParameter(name string) string { + if s.req.Attributes.Request.Http.Headers["content-type"] != "application/x-www-form-urlencoded" { + return "" + } + + values, err := url.ParseQuery(s.req.Attributes.Request.Http.Body) + if err != nil { + return "" + } + + return values.Get(name) +} +func (s *RequestContext) RequestBody() []byte { return s.req.Attributes.Request.Http.RawBody } +func (s *RequestContext) AppContext() context.Context { return s.ctx } +func (s *RequestContext) SetPipelineError(err error) { s.err = err } +func (s *RequestContext) AddHeaderForUpstream(name, value string) { s.upstreamHeaders.Add(name, value) } +func (s *RequestContext) AddCookieForUpstream(name, value string) { s.upstreamCookies[name] = value } +func (s *RequestContext) Signer() heimdall.JWTSigner { return s.jwtSigner } +func (s *RequestContext) RequestURL() *url.URL { return s.reqURL } +func (s *RequestContext) RequestClientIPs() []string { return nil } + +func (s *RequestContext) Finalize(statusCode int) (*envoy_auth.CheckResponse, error) { + if s.err != nil { + return nil, s.err + } + + var headers []*envoy_core.HeaderValueOption + + for k := range s.upstreamHeaders { + headers = append(headers, &envoy_core.HeaderValueOption{ + Header: &envoy_core.HeaderValue{ + Key: k, + Value: strings.Join(s.upstreamHeaders.Values(k), ","), + }, + }) + } + + if len(s.upstreamCookies) != 0 { + var cookies []string + + for k, v := range s.upstreamCookies { + cookies = append(cookies, fmt.Sprintf("%s=%s", k, v)) + } + + headers = append(headers, &envoy_core.HeaderValueOption{ + Header: &envoy_core.HeaderValue{ + Key: "Cookie", + Value: strings.Join(cookies, ";"), + }, + }) + } + + return &envoy_auth.CheckResponse{ + Status: &status.Status{Code: int32(statusCode)}, + HttpResponse: &envoy_auth.CheckResponse_OkResponse{ + OkResponse: &envoy_auth.OkHttpResponse{Headers: headers}, + }, + }, nil +} From dd46c228ce2102c369e0197cdd5e1edda0bf424e Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Mon, 30 Jan 2023 08:39:02 +0100 Subject: [PATCH 02/67] access context subject and error handling moved to its own package --- internal/accesscontext/access_context.go | 58 +++ .../middleware/accesslog/access_context.go | 38 -- .../middleware/accesslog/accesslog_handler.go | 221 +++++---- .../accesslog/accesslog_handler_test.go | 431 +++++++++--------- .../middleware/errorhandler/error_handler.go | 100 ++-- internal/rules/composite_subject_creator.go | 48 +- 6 files changed, 458 insertions(+), 438 deletions(-) create mode 100644 internal/accesscontext/access_context.go delete mode 100644 internal/fiber/middleware/accesslog/access_context.go diff --git a/internal/accesscontext/access_context.go b/internal/accesscontext/access_context.go new file mode 100644 index 000000000..e3fac5e89 --- /dev/null +++ b/internal/accesscontext/access_context.go @@ -0,0 +1,58 @@ +// Copyright 2022 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package accesscontext + +import "context" + +type ctxKey struct{} + +type accessContext struct { + err error + subject string +} + +func New(ctx context.Context) context.Context { + return context.WithValue(ctx, ctxKey{}, &accessContext{}) +} + +func Error(ctx context.Context) error { + if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { + return c.err + } + + return nil +} + +func SetError(ctx context.Context, err error) { + if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { + c.err = err + } +} + +func Subject(ctx context.Context) string { + if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { + return c.subject + } + + return "" +} + +func SetSubject(ctx context.Context, subject string) { + if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { + c.subject = subject + } +} diff --git a/internal/fiber/middleware/accesslog/access_context.go b/internal/fiber/middleware/accesslog/access_context.go deleted file mode 100644 index 822aa3912..000000000 --- a/internal/fiber/middleware/accesslog/access_context.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package accesslog - -import "context" - -type ctxKey struct{} - -type accessContext struct { - err error - subject string -} - -func AddError(ctx context.Context, err error) { - if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { - c.err = err - } -} - -func AddSubject(ctx context.Context, subject string) { - if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { - c.subject = subject - } -} diff --git a/internal/fiber/middleware/accesslog/accesslog_handler.go b/internal/fiber/middleware/accesslog/accesslog_handler.go index efc9832ab..b37c22b9d 100644 --- a/internal/fiber/middleware/accesslog/accesslog_handler.go +++ b/internal/fiber/middleware/accesslog/accesslog_handler.go @@ -17,132 +17,131 @@ package accesslog import ( - "context" - "time" + "time" - "github.com/gofiber/fiber/v2" - "github.com/rs/zerolog" + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog" - "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" ) func New(logger zerolog.Logger) fiber.Handler { - return func(c *fiber.Ctx) error { - start := time.Now() - traceCtx := tracecontext.Extract(c.UserContext()) - alc := &accessContext{} + return func(c *fiber.Ctx) error { + start := time.Now() + traceCtx := tracecontext.Extract(c.UserContext()) + c.SetUserContext(accesscontext.New(c.UserContext())) - c.SetUserContext(context.WithValue(c.UserContext(), ctxKey{}, alc)) + accLog := createAccessLogger(c, logger, start, traceCtx) + accLog.Info().Msg("TX started") - accLog := createAccessLogger(c, logger, start, traceCtx) - accLog.Info().Msg("TX started") + err := c.Next() - err := c.Next() + createAccessLogFinalizationEvent(c, accLog, err, start, traceCtx).Msg("TX finished") - createAccessLogFinalizationEvent(c, accLog, err, start, alc, traceCtx).Msg("TX finished") - - return err - } + return err + } } func createAccessLogger( - c *fiber.Ctx, - logger zerolog.Logger, - start time.Time, - traceCtx *tracecontext.TraceContext, + c *fiber.Ctx, + logger zerolog.Logger, + start time.Time, + traceCtx *tracecontext.TraceContext, ) zerolog.Logger { - startTime := start.Unix() - - logCtx := logger.Level(zerolog.InfoLevel).With(). - Int64("_tx_start", startTime). - Str("_client_ip", c.IP()). - Str("_http_method", c.Method()). - Str("_http_path", c.Path()). - Str("_http_user_agent", c.Get("User-Agent")). - Str("_http_host", string(c.Request().URI().Host())). - Str("_http_scheme", string(c.Request().URI().Scheme())) - - if traceCtx != nil { - logCtx = logCtx. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) - - if len(traceCtx.ParentID) != 0 { - logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) - } - } - - if c.IsProxyTrusted() { // nolint: nestif - if headerValue := c.Get("X-Forwarded-Proto"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_proto", headerValue) - } - - if headerValue := c.Get("X-Forwarded-Host"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_host", headerValue) - } - - if headerValue := c.Get("X-Forwarded-Path"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_path", headerValue) - } - - if headerValue := c.Get("X-Forwarded-Uri"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_uri", headerValue) - } - - if headerValue := c.Get("X-Forwarded-For"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_for", headerValue) - } - - if headerValue := c.Get("Forwarded"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_forwarded", headerValue) - } - } - - return logCtx.Logger() + startTime := start.Unix() + + logCtx := logger.Level(zerolog.InfoLevel).With(). + Int64("_tx_start", startTime). + Str("_client_ip", c.IP()). + Str("_http_method", c.Method()). + Str("_http_path", c.Path()). + Str("_http_user_agent", c.Get("User-Agent")). + Str("_http_host", string(c.Request().URI().Host())). + Str("_http_scheme", string(c.Request().URI().Scheme())) + + if traceCtx != nil { + logCtx = logCtx. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) + } + } + + if c.IsProxyTrusted() { // nolint: nestif + if headerValue := c.Get("X-Forwarded-Proto"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_proto", headerValue) + } + + if headerValue := c.Get("X-Forwarded-Host"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_host", headerValue) + } + + if headerValue := c.Get("X-Forwarded-Path"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_path", headerValue) + } + + if headerValue := c.Get("X-Forwarded-Uri"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_uri", headerValue) + } + + if headerValue := c.Get("X-Forwarded-For"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_for", headerValue) + } + + if headerValue := c.Get("Forwarded"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_forwarded", headerValue) + } + } + + return logCtx.Logger() } func createAccessLogFinalizationEvent( - c *fiber.Ctx, - accessLogger zerolog.Logger, - err error, - start time.Time, - alc *accessContext, - traceCtx *tracecontext.TraceContext, + c *fiber.Ctx, + accessLogger zerolog.Logger, + err error, + start time.Time, + traceCtx *tracecontext.TraceContext, ) *zerolog.Event { - end := time.Now() - duration := end.Sub(start) - - event := accessLogger.Info(). - Int("_body_bytes_sent", len(c.Response().Body())). - Int("_http_status_code", c.Response().StatusCode()). - Int64("_tx_duration_ms", duration.Milliseconds()) - - if traceCtx != nil { - event = event. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) - - if len(traceCtx.ParentID) != 0 { - event = event.Str("_parent_id", traceCtx.ParentID) - } - } - - switch { - case err != nil: - if len(alc.subject) != 0 { - event = event.Str("_subject", alc.subject) - } - - event = event.Err(err).Bool("_access_granted", false) - case alc.err != nil: - if len(alc.subject) != 0 { - event = event.Str("_subject", alc.subject) - } - - event = event.Err(alc.err).Bool("_access_granted", false) - default: - event = event.Str("_subject", alc.subject).Bool("_access_granted", true) - } - - return event + end := time.Now() + duration := end.Sub(start) + subject := accesscontext.Subject(c.UserContext()) + accessErr := accesscontext.Error(c.UserContext()) + + event := accessLogger.Info(). + Int("_body_bytes_sent", len(c.Response().Body())). + Int("_http_status_code", c.Response().StatusCode()). + Int64("_tx_duration_ms", duration.Milliseconds()) + + if traceCtx != nil { + event = event. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + event = event.Str("_parent_id", traceCtx.ParentID) + } + } + + switch { + case err != nil: + if len(subject) != 0 { + event = event.Str("_subject", subject) + } + + event = event.Err(err).Bool("_access_granted", false) + case accessErr != nil: + if len(subject) != 0 { + event = event.Str("_subject", subject) + } + + event = event.Err(accessErr).Bool("_access_granted", false) + default: + event = event.Str("_subject", subject).Bool("_access_granted", true) + } + + return event } diff --git a/internal/fiber/middleware/accesslog/accesslog_handler_test.go b/internal/fiber/middleware/accesslog/accesslog_handler_test.go index 69560bb1b..3fee9f343 100644 --- a/internal/fiber/middleware/accesslog/accesslog_handler_test.go +++ b/internal/fiber/middleware/accesslog/accesslog_handler_test.go @@ -17,248 +17,249 @@ package accesslog import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" - "github.com/goccy/go-json" - "github.com/gofiber/fiber/v2" - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" + "github.com/goccy/go-json" + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" - tracingmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/opentelemetry" - "github.com/dadrus/heimdall/internal/x/testsupport" + "github.com/dadrus/heimdall/internal/accesscontext" + tracingmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/opentelemetry" + "github.com/dadrus/heimdall/internal/x/testsupport" ) func TestLoggerHandler(t *testing.T) { - // GIVEN - otel.SetTracerProvider(sdktrace.NewTracerProvider()) - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) + // GIVEN + otel.SetTracerProvider(sdktrace.NewTracerProvider()) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) - parentCtx := trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: trace.TraceID{1}, SpanID: trace.SpanID{2}, TraceFlags: trace.FlagsSampled, - }) + parentCtx := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{1}, SpanID: trace.SpanID{2}, TraceFlags: trace.FlagsSampled, + }) - for _, tc := range []struct { - uc string - setHeader func(t *testing.T, req *http.Request) - configureHandler func(t *testing.T, ctx *fiber.Ctx) error - assert func(t *testing.T, logEvent1, logEvent2 map[string]any) - }{ - { - uc: "without tracing, x-* header and errors", - setHeader: func(t *testing.T, req *http.Request) { t.Helper() }, - configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { - t.Helper() + for _, tc := range []struct { + uc string + setHeader func(t *testing.T, req *http.Request) + configureHandler func(t *testing.T, ctx *fiber.Ctx) error + assert func(t *testing.T, logEvent1, logEvent2 map[string]any) + }{ + { + uc: "without tracing, x-* header and errors", + setHeader: func(t *testing.T, req *http.Request) { t.Helper() }, + configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { + t.Helper() - AddSubject(ctx.UserContext(), "foo") + accesscontext.SetSubject(ctx.UserContext(), "foo") - return nil - }, - assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { - t.Helper() + return nil + }, + assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { + t.Helper() - require.Len(t, logEvent1, 11) - assert.Equal(t, "info", logEvent1["level"]) - assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_client_ip") - assert.Contains(t, logEvent1, "_http_user_agent") - assert.Equal(t, "GET", logEvent1["_http_method"]) - assert.Equal(t, "example.com", logEvent1["_http_host"]) - assert.Equal(t, "/test", logEvent1["_http_path"]) - assert.Equal(t, "http", logEvent1["_http_scheme"]) - assert.Contains(t, logEvent1, "_trace_id") - assert.Contains(t, logEvent1, "_trace_id") - assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) - assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) - assert.Equal(t, "TX started", logEvent1["message"]) + require.Len(t, logEvent1, 11) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_client_ip") + assert.Contains(t, logEvent1, "_http_user_agent") + assert.Equal(t, "GET", logEvent1["_http_method"]) + assert.Equal(t, "example.com", logEvent1["_http_host"]) + assert.Equal(t, "/test", logEvent1["_http_path"]) + assert.Equal(t, "http", logEvent1["_http_scheme"]) + assert.Contains(t, logEvent1, "_trace_id") + assert.Contains(t, logEvent1, "_trace_id") + assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) + assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) + assert.Equal(t, "TX started", logEvent1["message"]) - require.Len(t, logEvent2, 16) - assert.Equal(t, "info", logEvent2["level"]) - assert.Contains(t, logEvent2, "_tx_start") - assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_client_ip") - assert.Equal(t, "GET", logEvent2["_http_method"]) - assert.Equal(t, "example.com", logEvent2["_http_host"]) - assert.Equal(t, "/test", logEvent2["_http_path"]) - assert.Equal(t, "http", logEvent2["_http_scheme"]) - assert.Contains(t, logEvent2, "_trace_id") - assert.Contains(t, logEvent2, "_trace_id") - assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) - assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) - assert.Contains(t, logEvent2, "_body_bytes_sent") - assert.Equal(t, float64(200), logEvent2["_http_status_code"]) - assert.Equal(t, true, logEvent2["_access_granted"]) - assert.Equal(t, "foo", logEvent2["_subject"]) - assert.Contains(t, logEvent2, "_http_user_agent") - assert.Equal(t, "TX finished", logEvent2["message"]) - }, - }, - { - uc: "with tracing, x-* header and error", - setHeader: func(t *testing.T, req *http.Request) { - t.Helper() + require.Len(t, logEvent2, 16) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, "GET", logEvent2["_http_method"]) + assert.Equal(t, "example.com", logEvent2["_http_host"]) + assert.Equal(t, "/test", logEvent2["_http_path"]) + assert.Equal(t, "http", logEvent2["_http_scheme"]) + assert.Contains(t, logEvent2, "_trace_id") + assert.Contains(t, logEvent2, "_trace_id") + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Contains(t, logEvent2, "_body_bytes_sent") + assert.Equal(t, float64(200), logEvent2["_http_status_code"]) + assert.Equal(t, true, logEvent2["_access_granted"]) + assert.Equal(t, "foo", logEvent2["_subject"]) + assert.Contains(t, logEvent2, "_http_user_agent") + assert.Equal(t, "TX finished", logEvent2["message"]) + }, + }, + { + uc: "with tracing, x-* header and error", + setHeader: func(t *testing.T, req *http.Request) { + t.Helper() - // nolint: contextcheck - otel.GetTextMapPropagator().Inject( - trace.ContextWithRemoteSpanContext(context.Background(), parentCtx), - propagation.HeaderCarrier(req.Header)) + // nolint: contextcheck + otel.GetTextMapPropagator().Inject( + trace.ContextWithRemoteSpanContext(context.Background(), parentCtx), + propagation.HeaderCarrier(req.Header)) - req.Header.Set("X-Forwarded-Proto", "https") - req.Header.Set("X-Forwarded-Host", "foobar.com") - req.Header.Set("X-Forwarded-Path", "/bar") - req.Header.Set("X-Forwarded-Uri", "https://foobar.com/bar") - req.Header.Set("X-Forwarded-For", "127.0.0.1") - req.Header.Set("Forwarded", "for=127.0.0.1") - }, - configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { - t.Helper() + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-Host", "foobar.com") + req.Header.Set("X-Forwarded-Path", "/bar") + req.Header.Set("X-Forwarded-Uri", "https://foobar.com/bar") + req.Header.Set("X-Forwarded-For", "127.0.0.1") + req.Header.Set("Forwarded", "for=127.0.0.1") + }, + configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { + t.Helper() - return fmt.Errorf("test error") // nolint: goerr113 - }, - assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { - t.Helper() + return fmt.Errorf("test error") // nolint: goerr113 + }, + assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { + t.Helper() - require.Len(t, logEvent1, 18) - assert.Equal(t, "info", logEvent1["level"]) - assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_client_ip") - assert.Contains(t, logEvent1, "_http_user_agent") - assert.Equal(t, "GET", logEvent1["_http_method"]) - assert.Equal(t, "example.com", logEvent1["_http_host"]) - assert.Equal(t, "/test", logEvent1["_http_path"]) - assert.Equal(t, "http", logEvent1["_http_scheme"]) - assert.Contains(t, logEvent1, "_span_id") - assert.Equal(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) - assert.Equal(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) - assert.Equal(t, "TX started", logEvent1["message"]) - assert.Equal(t, "https", logEvent1["_http_x_forwarded_proto"]) - assert.Equal(t, "foobar.com", logEvent1["_http_x_forwarded_host"]) - assert.Equal(t, "/bar", logEvent1["_http_x_forwarded_path"]) - assert.Equal(t, "https://foobar.com/bar", logEvent1["_http_x_forwarded_uri"]) - assert.Equal(t, "127.0.0.1", logEvent1["_http_x_forwarded_for"]) - assert.Equal(t, "for=127.0.0.1", logEvent1["_http_forwarded"]) + require.Len(t, logEvent1, 18) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_client_ip") + assert.Contains(t, logEvent1, "_http_user_agent") + assert.Equal(t, "GET", logEvent1["_http_method"]) + assert.Equal(t, "example.com", logEvent1["_http_host"]) + assert.Equal(t, "/test", logEvent1["_http_path"]) + assert.Equal(t, "http", logEvent1["_http_scheme"]) + assert.Contains(t, logEvent1, "_span_id") + assert.Equal(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) + assert.Equal(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) + assert.Equal(t, "TX started", logEvent1["message"]) + assert.Equal(t, "https", logEvent1["_http_x_forwarded_proto"]) + assert.Equal(t, "foobar.com", logEvent1["_http_x_forwarded_host"]) + assert.Equal(t, "/bar", logEvent1["_http_x_forwarded_path"]) + assert.Equal(t, "https://foobar.com/bar", logEvent1["_http_x_forwarded_uri"]) + assert.Equal(t, "127.0.0.1", logEvent1["_http_x_forwarded_for"]) + assert.Equal(t, "for=127.0.0.1", logEvent1["_http_forwarded"]) - require.Len(t, logEvent2, 23) - assert.Equal(t, "info", logEvent2["level"]) - assert.Contains(t, logEvent2, "_tx_start") - assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_client_ip") - assert.Equal(t, "GET", logEvent2["_http_method"]) - assert.Equal(t, "example.com", logEvent2["_http_host"]) - assert.Equal(t, "/test", logEvent2["_http_path"]) - assert.Equal(t, "http", logEvent2["_http_scheme"]) - assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) - assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) - assert.Equal(t, logEvent2["_span_id"], logEvent2["_span_id"]) - assert.Contains(t, logEvent2, "_body_bytes_sent") - assert.Equal(t, float64(200), logEvent2["_http_status_code"]) - assert.Equal(t, false, logEvent2["_access_granted"]) - assert.Equal(t, "test error", logEvent2["error"]) - assert.Contains(t, logEvent2, "_http_user_agent") - assert.Equal(t, "TX finished", logEvent2["message"]) - assert.Equal(t, "https", logEvent1["_http_x_forwarded_proto"]) - assert.Equal(t, "foobar.com", logEvent1["_http_x_forwarded_host"]) - assert.Equal(t, "/bar", logEvent1["_http_x_forwarded_path"]) - assert.Equal(t, "https://foobar.com/bar", logEvent1["_http_x_forwarded_uri"]) - assert.Equal(t, "127.0.0.1", logEvent1["_http_x_forwarded_for"]) - assert.Equal(t, "for=127.0.0.1", logEvent1["_http_forwarded"]) - }, - }, - { - uc: "without tracing and x-* header, but with subject and error set on context", - setHeader: func(t *testing.T, req *http.Request) { t.Helper() }, - configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { - t.Helper() + require.Len(t, logEvent2, 23) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, "GET", logEvent2["_http_method"]) + assert.Equal(t, "example.com", logEvent2["_http_host"]) + assert.Equal(t, "/test", logEvent2["_http_path"]) + assert.Equal(t, "http", logEvent2["_http_scheme"]) + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Equal(t, logEvent2["_span_id"], logEvent2["_span_id"]) + assert.Contains(t, logEvent2, "_body_bytes_sent") + assert.Equal(t, float64(200), logEvent2["_http_status_code"]) + assert.Equal(t, false, logEvent2["_access_granted"]) + assert.Equal(t, "test error", logEvent2["error"]) + assert.Contains(t, logEvent2, "_http_user_agent") + assert.Equal(t, "TX finished", logEvent2["message"]) + assert.Equal(t, "https", logEvent1["_http_x_forwarded_proto"]) + assert.Equal(t, "foobar.com", logEvent1["_http_x_forwarded_host"]) + assert.Equal(t, "/bar", logEvent1["_http_x_forwarded_path"]) + assert.Equal(t, "https://foobar.com/bar", logEvent1["_http_x_forwarded_uri"]) + assert.Equal(t, "127.0.0.1", logEvent1["_http_x_forwarded_for"]) + assert.Equal(t, "for=127.0.0.1", logEvent1["_http_forwarded"]) + }, + }, + { + uc: "without tracing and x-* header, but with subject and error set on context", + setHeader: func(t *testing.T, req *http.Request) { t.Helper() }, + configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { + t.Helper() - AddSubject(ctx.UserContext(), "bar") - AddError(ctx.UserContext(), fmt.Errorf("test error")) // nolint: goerr113 + accesscontext.SetSubject(ctx.UserContext(), "bar") + accesscontext.SetError(ctx.UserContext(), fmt.Errorf("test error")) // nolint: goerr113 - return nil - }, - assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { - t.Helper() + return nil + }, + assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { + t.Helper() - require.Len(t, logEvent1, 11) - assert.Equal(t, "info", logEvent1["level"]) - assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_client_ip") - assert.Contains(t, logEvent1, "_http_user_agent") - assert.Equal(t, "GET", logEvent1["_http_method"]) - assert.Equal(t, "example.com", logEvent1["_http_host"]) - assert.Equal(t, "/test", logEvent1["_http_path"]) - assert.Equal(t, "http", logEvent1["_http_scheme"]) - assert.Contains(t, logEvent1, "_trace_id") - assert.Contains(t, logEvent1, "_trace_id") - assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) - assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) - assert.Equal(t, "TX started", logEvent1["message"]) + require.Len(t, logEvent1, 11) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_client_ip") + assert.Contains(t, logEvent1, "_http_user_agent") + assert.Equal(t, "GET", logEvent1["_http_method"]) + assert.Equal(t, "example.com", logEvent1["_http_host"]) + assert.Equal(t, "/test", logEvent1["_http_path"]) + assert.Equal(t, "http", logEvent1["_http_scheme"]) + assert.Contains(t, logEvent1, "_trace_id") + assert.Contains(t, logEvent1, "_trace_id") + assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) + assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) + assert.Equal(t, "TX started", logEvent1["message"]) - require.Len(t, logEvent2, 17) - assert.Equal(t, "info", logEvent2["level"]) - assert.Contains(t, logEvent2, "_tx_start") - assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_client_ip") - assert.Equal(t, "GET", logEvent2["_http_method"]) - assert.Equal(t, "example.com", logEvent2["_http_host"]) - assert.Equal(t, "/test", logEvent2["_http_path"]) - assert.Equal(t, "http", logEvent2["_http_scheme"]) - assert.Contains(t, logEvent2, "_trace_id") - assert.Contains(t, logEvent2, "_trace_id") - assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) - assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) - assert.Contains(t, logEvent2, "_body_bytes_sent") - assert.Equal(t, float64(200), logEvent2["_http_status_code"]) - assert.Equal(t, false, logEvent2["_access_granted"]) - assert.Equal(t, "bar", logEvent2["_subject"]) - assert.Equal(t, "test error", logEvent2["error"]) - assert.Contains(t, logEvent2, "_http_user_agent") - assert.Equal(t, "TX finished", logEvent2["message"]) - }, - }, - } { - t.Run(tc.uc, func(t *testing.T) { - // GIVEN - tb := &testsupport.TestingLog{TB: t} - logger := zerolog.New(zerolog.TestWriter{T: tb}) + require.Len(t, logEvent2, 17) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, "GET", logEvent2["_http_method"]) + assert.Equal(t, "example.com", logEvent2["_http_host"]) + assert.Equal(t, "/test", logEvent2["_http_path"]) + assert.Equal(t, "http", logEvent2["_http_scheme"]) + assert.Contains(t, logEvent2, "_trace_id") + assert.Contains(t, logEvent2, "_trace_id") + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Contains(t, logEvent2, "_body_bytes_sent") + assert.Equal(t, float64(200), logEvent2["_http_status_code"]) + assert.Equal(t, false, logEvent2["_access_granted"]) + assert.Equal(t, "bar", logEvent2["_subject"]) + assert.Equal(t, "test error", logEvent2["error"]) + assert.Contains(t, logEvent2, "_http_user_agent") + assert.Equal(t, "TX finished", logEvent2["message"]) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + tb := &testsupport.TestingLog{TB: t} + logger := zerolog.New(zerolog.TestWriter{T: tb}) - app := fiber.New() - app.Use(tracingmiddleware.New()) - app.Use(New(logger)) - app.Get("/test", func(ctx *fiber.Ctx) error { return tc.configureHandler(t, ctx) }) + app := fiber.New() + app.Use(tracingmiddleware.New()) + app.Use(New(logger)) + app.Get("/test", func(ctx *fiber.Ctx) error { return tc.configureHandler(t, ctx) }) - req := httptest.NewRequest(http.MethodGet, "/test", nil) + req := httptest.NewRequest(http.MethodGet, "/test", nil) - tc.setHeader(t, req) + tc.setHeader(t, req) - // WHEN - resp, err := app.Test(req, 1000000) - require.NoError(t, app.Shutdown()) + // WHEN + resp, err := app.Test(req, 1000000) + require.NoError(t, app.Shutdown()) - // THEN - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) + // THEN + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) - events := strings.Split(tb.CollectedLog(), "}") - require.Len(t, events, 3) + events := strings.Split(tb.CollectedLog(), "}") + require.Len(t, events, 3) - var ( - logLine1 map[string]any - logLine2 map[string]any - ) + var ( + logLine1 map[string]any + logLine2 map[string]any + ) - require.NoError(t, json.Unmarshal([]byte(events[0]+"}"), &logLine1)) - require.NoError(t, json.Unmarshal([]byte(events[1]+"}"), &logLine2)) + require.NoError(t, json.Unmarshal([]byte(events[0]+"}"), &logLine1)) + require.NoError(t, json.Unmarshal([]byte(events[1]+"}"), &logLine2)) - tc.assert(t, logLine1, logLine2) - }) - } + tc.assert(t, logLine1, logLine2) + }) + } } diff --git a/internal/fiber/middleware/errorhandler/error_handler.go b/internal/fiber/middleware/errorhandler/error_handler.go index b3e3a1264..4ab1427f2 100644 --- a/internal/fiber/middleware/errorhandler/error_handler.go +++ b/internal/fiber/middleware/errorhandler/error_handler.go @@ -17,68 +17,68 @@ package errorhandler import ( - "errors" + "errors" - "github.com/gofiber/fiber/v2" - "github.com/rs/zerolog" + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog" - "github.com/dadrus/heimdall/internal/fiber/middleware/accesslog" - "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/heimdall" ) func New(opts ...Option) fiber.Handler { - options := defaultOptions + options := defaultOptions - for _, opt := range opts { - opt(&options) - } + for _, opt := range opts { + opt(&options) + } - h := &handler{opts: options} + h := &handler{opts: options} - return h.handle + return h.handle } type handler struct { - opts + opts } func (h *handler) handle(ctx *fiber.Ctx) error { //nolint:cyclop - err := ctx.Next() - if err == nil { - return nil - } - - accesslog.AddError(ctx.UserContext(), err) - - switch { - case errors.Is(err, heimdall.ErrAuthentication): - h.onAuthenticationError(ctx) - case errors.Is(err, heimdall.ErrAuthorization): - h.onAuthorizationError(ctx) - case errors.Is(err, heimdall.ErrCommunicationTimeout) || errors.Is(err, heimdall.ErrCommunication): - h.onCommunicationError(ctx) - case errors.Is(err, heimdall.ErrArgument): - h.onPreconditionError(ctx) - case errors.Is(err, heimdall.ErrMethodNotAllowed): - h.onBadMethodError(ctx) - case errors.Is(err, heimdall.ErrNoRuleFound): - h.onNoRuleError(ctx) - case errors.Is(err, &heimdall.RedirectError{}): - var redirectError *heimdall.RedirectError - - errors.As(err, &redirectError) - - return ctx.Redirect(redirectError.RedirectTo, redirectError.Code) - default: - logger := zerolog.Ctx(ctx.UserContext()) - logger.Error().Err(err).Msg("Internal error occurred") - - h.onInternalError(ctx) - } - - if h.verboseErrors { - return ctx.Format(err) - } - - return nil + err := ctx.Next() + if err == nil { + return nil + } + + accesscontext.SetError(ctx.UserContext(), err) + + switch { + case errors.Is(err, heimdall.ErrAuthentication): + h.onAuthenticationError(ctx) + case errors.Is(err, heimdall.ErrAuthorization): + h.onAuthorizationError(ctx) + case errors.Is(err, heimdall.ErrCommunicationTimeout) || errors.Is(err, heimdall.ErrCommunication): + h.onCommunicationError(ctx) + case errors.Is(err, heimdall.ErrArgument): + h.onPreconditionError(ctx) + case errors.Is(err, heimdall.ErrMethodNotAllowed): + h.onBadMethodError(ctx) + case errors.Is(err, heimdall.ErrNoRuleFound): + h.onNoRuleError(ctx) + case errors.Is(err, &heimdall.RedirectError{}): + var redirectError *heimdall.RedirectError + + errors.As(err, &redirectError) + + return ctx.Redirect(redirectError.RedirectTo, redirectError.Code) + default: + logger := zerolog.Ctx(ctx.UserContext()) + logger.Error().Err(err).Msg("Internal error occurred") + + h.onInternalError(ctx) + } + + if h.verboseErrors { + return ctx.Format(err) + } + + return nil } diff --git a/internal/rules/composite_subject_creator.go b/internal/rules/composite_subject_creator.go index 3b332ac27..c9b8fd280 100644 --- a/internal/rules/composite_subject_creator.go +++ b/internal/rules/composite_subject_creator.go @@ -17,43 +17,43 @@ package rules import ( - "errors" + "errors" - "github.com/rs/zerolog" + "github.com/rs/zerolog" - "github.com/dadrus/heimdall/internal/fiber/middleware/accesslog" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) type compositeSubjectCreator []subjectCreator func (ca compositeSubjectCreator) Execute(ctx heimdall.Context) (*subject.Subject, error) { - logger := zerolog.Ctx(ctx.AppContext()) + logger := zerolog.Ctx(ctx.AppContext()) - var ( - sub *subject.Subject - err error - ) + var ( + sub *subject.Subject + err error + ) - for idx, a := range ca { - sub, err = a.Execute(ctx) - if err != nil { - logger.Info().Err(err).Msg("Pipeline step execution failed") + for idx, a := range ca { + sub, err = a.Execute(ctx) + if err != nil { + logger.Info().Err(err).Msg("Pipeline step execution failed") - if (errors.Is(err, heimdall.ErrArgument) || a.IsFallbackOnErrorAllowed()) && idx < len(ca) { - logger.Info().Msg("Falling back to next configured one.") + if (errors.Is(err, heimdall.ErrArgument) || a.IsFallbackOnErrorAllowed()) && idx < len(ca) { + logger.Info().Msg("Falling back to next configured one.") - continue - } + continue + } - break - } + break + } - accesslog.AddSubject(ctx.AppContext(), sub.ID) + accesscontext.SetSubject(ctx.AppContext(), sub.ID) - return sub, nil - } + return sub, nil + } - return nil, err + return nil, err } From 137bd7551335efc08a0d92833d6093449435757e Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Tue, 31 Jan 2023 13:22:41 +0100 Subject: [PATCH 03/67] further implementation --- cmd/serve/decision.go | 78 +++-- go.mod | 5 +- go.sum | 3 + .../middleware/accesslog/accesslog_handler.go | 143 ++++++++ .../grpc/middleware/cache/cache.go | 31 ++ .../grpc/middleware/errorhandler/defaults.go | 46 +++ .../middleware/errorhandler/error_handler.go | 97 ++++++ .../grpc/middleware/errorhandler/options.go | 125 +++++++ .../grpc/middleware/logger/handler.go | 45 +++ .../grpc/middleware/prometheus/defaults.go} | 13 +- .../grpc/middleware/prometheus/handler.go | 125 +++++++ .../grpc/middleware/prometheus/options.go | 80 +++++ .../middleware/prometheus/options_test.go | 317 ++++++++++++++++++ .../grpc/v3/handler.go | 9 +- .../handler/envoyextauth/grpc/v3/module.go | 93 +++++ .../grpc/v3/request_context.go | 21 +- .../handler/envoyextauth/grpc/v3/service.go | 67 ++++ internal/x/errorchain/error_chain.go | 184 +++++----- 18 files changed, 1350 insertions(+), 132 deletions(-) create mode 100644 internal/handler/envoyextauth/grpc/middleware/accesslog/accesslog_handler.go create mode 100644 internal/handler/envoyextauth/grpc/middleware/cache/cache.go create mode 100644 internal/handler/envoyextauth/grpc/middleware/errorhandler/defaults.go create mode 100644 internal/handler/envoyextauth/grpc/middleware/errorhandler/error_handler.go create mode 100644 internal/handler/envoyextauth/grpc/middleware/errorhandler/options.go create mode 100644 internal/handler/envoyextauth/grpc/middleware/logger/handler.go rename internal/handler/{envoyextauthz/grpc/v3/module.go => envoyextauth/grpc/middleware/prometheus/defaults.go} (71%) create mode 100644 internal/handler/envoyextauth/grpc/middleware/prometheus/handler.go create mode 100644 internal/handler/envoyextauth/grpc/middleware/prometheus/options.go create mode 100644 internal/handler/envoyextauth/grpc/middleware/prometheus/options_test.go rename internal/handler/{envoyextauthz => envoyextauth}/grpc/v3/handler.go (90%) create mode 100644 internal/handler/envoyextauth/grpc/v3/module.go rename internal/handler/{envoyextauthz => envoyextauth}/grpc/v3/request_context.go (87%) create mode 100644 internal/handler/envoyextauth/grpc/v3/service.go diff --git a/cmd/serve/decision.go b/cmd/serve/decision.go index 6df9744d3..eee56a4e0 100644 --- a/cmd/serve/decision.go +++ b/cmd/serve/decision.go @@ -17,44 +17,58 @@ package serve import ( - "github.com/spf13/cobra" - "go.uber.org/fx" + "github.com/spf13/cobra" + "go.uber.org/fx" - "github.com/dadrus/heimdall/internal" - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/handler/decision" + "github.com/dadrus/heimdall/internal" + "github.com/dadrus/heimdall/internal/config" + "github.com/dadrus/heimdall/internal/handler/decision" + envoy_extauth "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/v3" ) // NewDecisionCommand represents the "serve decision" command. func NewDecisionCommand() *cobra.Command { - return &cobra.Command{ - Use: "decision", - Short: "Starts heimdall in Decision operation mode", - Example: "heimdall serve decision", - Run: func(cmd *cobra.Command, _ []string) { - app, err := createDecisionApp(cmd) - if err != nil { - cmd.PrintErrf("Failed to initialize decision service: %v", err) - panic(err) - } - - app.Run() - }, - } + cmd := &cobra.Command{ + Use: "decision", + Short: "Starts heimdall in Decision operation mode", + Example: "heimdall serve decision", + Run: func(cmd *cobra.Command, _ []string) { + app, err := createDecisionApp(cmd) + if err != nil { + cmd.PrintErrf("Failed to initialize decision service: %v", err) + panic(err) + } + + app.Run() + }, + } + + cmd.PersistentFlags().Bool("envoy-extauth", false, + "Whether to start the decision mode for integration with envoy extauth gRPC service") + + return cmd } func createDecisionApp(cmd *cobra.Command) (*fx.App, error) { - configPath, _ := cmd.Flags().GetString("config") - envPrefix, _ := cmd.Flags().GetString("env-config-prefix") - - app := fx.New( - fx.NopLogger, - fx.Supply( - config.ConfigurationPath(configPath), - config.EnvVarPrefix(envPrefix)), - internal.Module, - decision.Module, - ) - - return app, app.Err() + configPath, _ := cmd.Flags().GetString("config") + envPrefix, _ := cmd.Flags().GetString("env-config-prefix") + useEnvoyExtAuth, _ := cmd.Flags().GetBool("envoy-extauth") + + opts := []fx.Option{ + fx.NopLogger, + fx.Supply( + config.ConfigurationPath(configPath), + config.EnvVarPrefix(envPrefix)), + internal.Module, + } + + if useEnvoyExtAuth { + opts = append(opts, envoy_extauth.Module) + } else { + opts = append(opts, decision.Module) + } + + app := fx.New(opts...) + + return app, app.Err() } diff --git a/go.mod b/go.mod index 2fc78b8c7..d9fa7b301 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,8 @@ require ( github.com/gofiber/fiber/v2 v2.41.0 github.com/google/cel-go v0.13.0 github.com/google/uuid v1.3.0 + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/iancoleman/strcase v0.2.0 github.com/instana/go-otel-exporter v0.0.0-20220908102301-52c5d8dbfd86 github.com/jellydator/ttlcache/v3 v3.0.1 @@ -36,6 +38,7 @@ require ( github.com/ybbus/httpretry v1.0.1 github.com/yl2chen/cidranger v1.0.2 github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.37.0 go.opentelemetry.io/contrib/propagators/autoprop v0.37.0 go.opentelemetry.io/otel v1.11.2 @@ -51,6 +54,7 @@ require ( gocloud.dev v0.28.0 golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3 + google.golang.org/grpc v1.51.0 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.26.1 @@ -172,7 +176,6 @@ require ( golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.103.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/grpc v1.51.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index a69a2bae9..039405615 100644 --- a/go.sum +++ b/go.sum @@ -1181,7 +1181,9 @@ github.com/grafana/regexp v0.0.0-20221005093135-b4c2bcb0a4b6/go.mod h1:M5qHK+eWf github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -1927,6 +1929,7 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 h1:Ky1MObd188aGbgb5OgNnwGuEEwI9MVIcc7rBW6zk5Ak= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0/go.mod h1:PFmBsWbldL1kiWZk9+0LBZz2brhByaGsvp6pRICMlPE= diff --git a/internal/handler/envoyextauth/grpc/middleware/accesslog/accesslog_handler.go b/internal/handler/envoyextauth/grpc/middleware/accesslog/accesslog_handler.go new file mode 100644 index 000000000..b68283dea --- /dev/null +++ b/internal/handler/envoyextauth/grpc/middleware/accesslog/accesslog_handler.go @@ -0,0 +1,143 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package accesslog + +import ( + "context" + "time" + + "github.com/rs/zerolog" + "google.golang.org/grpc" + + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" +) + +func New(logger zerolog.Logger) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + start := time.Now() + traceCtx := tracecontext.Extract(ctx) + ctx = accesscontext.New(ctx) + + accLog := createAccessLogger(logger, start, traceCtx) + accLog.Info().Msg("TX started") + + res, err := handler(ctx, req) + + createAccessLogFinalizationEvent(ctx, accLog, err, start, traceCtx).Msg("TX finished") + + return res, err + } +} + +func createAccessLogger(logger zerolog.Logger, start time.Time, traceCtx *tracecontext.TraceContext) zerolog.Logger { + startTime := start.Unix() + + logCtx := logger.Level(zerolog.InfoLevel).With(). + Int64("_tx_start", startTime) + // Str("_client_ip", c.IP()). + // Str("_http_method", c.Method()). + // Str("_http_path", c.Path()). + // Str("_http_user_agent", c.Get("User-Agent")). + // Str("_http_host", string(c.Request().URI().Host())). + // Str("_http_scheme", string(c.Request().URI().Scheme())) + + if traceCtx != nil { + logCtx = logCtx. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) + } + } + + // if c.IsProxyTrusted() { // nolint: nestif + // if headerValue := c.Get("X-Forwarded-Proto"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_proto", headerValue) + // } + // + // if headerValue := c.Get("X-Forwarded-Host"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_host", headerValue) + // } + // + // if headerValue := c.Get("X-Forwarded-Path"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_path", headerValue) + // } + // + // if headerValue := c.Get("X-Forwarded-Uri"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_uri", headerValue) + // } + // + // if headerValue := c.Get("X-Forwarded-For"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_for", headerValue) + // } + // + // if headerValue := c.Get("Forwarded"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_forwarded", headerValue) + // } + // } + + return logCtx.Logger() +} + +func createAccessLogFinalizationEvent( + ctx context.Context, + accessLogger zerolog.Logger, + err error, + start time.Time, + traceCtx *tracecontext.TraceContext, +) *zerolog.Event { + end := time.Now() + duration := end.Sub(start) + subject := accesscontext.Subject(ctx) + accessErr := accesscontext.Error(ctx) + + event := accessLogger.Info(). + // Int("_body_bytes_sent", len(c.Response().Body())). + // Int("_http_status_code", c.Response().StatusCode()). + Int64("_tx_duration_ms", duration.Milliseconds()) + + if traceCtx != nil { + event = event. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + event = event.Str("_parent_id", traceCtx.ParentID) + } + } + + switch { + case err != nil: + if len(subject) != 0 { + event = event.Str("_subject", subject) + } + + event = event.Err(err).Bool("_access_granted", false) + case accessErr != nil: + if len(subject) != 0 { + event = event.Str("_subject", subject) + } + + event = event.Err(accessErr).Bool("_access_granted", false) + default: + event = event.Str("_subject", subject).Bool("_access_granted", true) + } + + return event +} diff --git a/internal/handler/envoyextauth/grpc/middleware/cache/cache.go b/internal/handler/envoyextauth/grpc/middleware/cache/cache.go new file mode 100644 index 000000000..43f3a74c3 --- /dev/null +++ b/internal/handler/envoyextauth/grpc/middleware/cache/cache.go @@ -0,0 +1,31 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package cache + +import ( + "context" + + "google.golang.org/grpc" + + "github.com/dadrus/heimdall/internal/cache" +) + +func New(cch cache.Cache) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + return handler(cache.WithContext(ctx, cch), req) + } +} diff --git a/internal/handler/envoyextauth/grpc/middleware/errorhandler/defaults.go b/internal/handler/envoyextauth/grpc/middleware/errorhandler/defaults.go new file mode 100644 index 000000000..50640e58d --- /dev/null +++ b/internal/handler/envoyextauth/grpc/middleware/errorhandler/defaults.go @@ -0,0 +1,46 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package errorhandler + +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var defaultOptions = opts{ //nolint:gochecknoglobals + authenticationError: func(err error, verbose bool) error { + return status.Error(codes.Unauthenticated, messageFrom(err, verbose)) + }, + authorizationError: func(err error, verbose bool) error { + return status.Error(codes.PermissionDenied, messageFrom(err, verbose)) + }, + communicationError: func(err error, verbose bool) error { + return status.Error(codes.Aborted, messageFrom(err, verbose)) + }, + preconditionError: func(err error, verbose bool) error { + return status.Error(codes.FailedPrecondition, messageFrom(err, verbose)) + }, + badMethodError: func(err error, verbose bool) error { + return status.Error(codes.FailedPrecondition, messageFrom(err, verbose)) + }, + noRuleError: func(err error, verbose bool) error { + return status.Error(codes.NotFound, messageFrom(err, verbose)) + }, + internalError: func(err error, verbose bool) error { + return status.Error(codes.Internal, messageFrom(err, verbose)) + }, +} diff --git a/internal/handler/envoyextauth/grpc/middleware/errorhandler/error_handler.go b/internal/handler/envoyextauth/grpc/middleware/errorhandler/error_handler.go new file mode 100644 index 000000000..9d7525552 --- /dev/null +++ b/internal/handler/envoyextauth/grpc/middleware/errorhandler/error_handler.go @@ -0,0 +1,97 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package errorhandler + +import ( + "context" + "errors" + + envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/rs/zerolog" + "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc" + + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/heimdall" +) + +func New(opts ...Option) grpc.UnaryServerInterceptor { + options := defaultOptions + + for _, opt := range opts { + opt(&options) + } + + h := &handler{opts: options} + + return h.handle +} + +type handler struct { + opts +} + +func (h *handler) handle(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { //nolint:cyclop + res, err := handler(ctx, req) + if err == nil { + return res, nil + } + + accesscontext.SetError(ctx, err) + + switch { + case errors.Is(err, heimdall.ErrAuthentication): + err = h.authenticationError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrAuthorization): + err = h.authorizationError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrCommunicationTimeout) || errors.Is(err, heimdall.ErrCommunication): + err = h.communicationError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrArgument): + err = h.preconditionError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrMethodNotAllowed): + err = h.badMethodError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrNoRuleFound): + err = h.noRuleError(err, h.verboseErrors) + case errors.Is(err, &heimdall.RedirectError{}): + var redirectError *heimdall.RedirectError + + errors.As(err, &redirectError) + + return &envoy_auth.CheckResponse{ + Status: &status.Status{Code: int32(redirectError.Code)}, + HttpResponse: &envoy_auth.CheckResponse_OkResponse{ + OkResponse: &envoy_auth.OkHttpResponse{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") + + err = h.internalError(err, h.verboseErrors) + } + + return req, err +} diff --git a/internal/handler/envoyextauth/grpc/middleware/errorhandler/options.go b/internal/handler/envoyextauth/grpc/middleware/errorhandler/options.go new file mode 100644 index 000000000..818e0d73f --- /dev/null +++ b/internal/handler/envoyextauth/grpc/middleware/errorhandler/options.go @@ -0,0 +1,125 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package errorhandler + +import ( + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type opts struct { + verboseErrors bool + authenticationError func(err error, verbose bool) error + authorizationError func(err error, verbose bool) error + communicationError func(err error, verbose bool) error + preconditionError func(err error, verbose bool) error + badMethodError func(err error, verbose bool) error + noRuleError func(err error, verbose bool) error + internalError func(err error, verbose bool) error +} + +type Option func(*opts) + +func WithPreconditionErrorCode(code int) Option { + return func(o *opts) { + if code != 0 { + o.preconditionError = func(err error, verbose bool) error { + return status.Error(codes.Code(code), messageFrom(err, verbose)) + } + } + } +} + +func WithAuthenticationErrorCode(code int) Option { + return func(o *opts) { + if code != 0 { + o.authenticationError = func(err error, verbose bool) error { + return status.Error(codes.Code(code), messageFrom(err, verbose)) + } + } + } +} + +func WithAuthorizationErrorCode(code int) Option { + return func(o *opts) { + if code != 0 { + o.authorizationError = func(err error, verbose bool) error { + return status.Error(codes.Code(code), messageFrom(err, verbose)) + } + } + } +} + +func WithCommunicationErrorCode(code int) Option { + return func(o *opts) { + if code != 0 { + o.communicationError = func(err error, verbose bool) error { + return status.Error(codes.Code(code), messageFrom(err, verbose)) + } + } + } +} + +func WithInternalServerErrorCode(code int) Option { + return func(o *opts) { + if code != 0 { + o.internalError = func(err error, verbose bool) error { + return status.Error(codes.Code(code), messageFrom(err, verbose)) + } + } + } +} + +func WithMethodErrorCode(code int) Option { + return func(o *opts) { + if code != 0 { + o.badMethodError = func(err error, verbose bool) error { + return status.Error(codes.Code(code), messageFrom(err, verbose)) + } + } + } +} + +func WithNoRuleErrorCode(code int) Option { + return func(o *opts) { + if code != 0 { + o.noRuleError = func(err error, verbose bool) error { + return status.Error(codes.Code(code), messageFrom(err, verbose)) + } + } + } +} + +func WithVerboseErrors(flag bool) Option { + return func(o *opts) { + o.verboseErrors = flag + } +} + +func messageFrom(err error, verbose bool) string { + if !verbose { + return "" + } + + if se, ok := err.(fmt.Stringer); ok { + return se.String() + } else { + return err.Error() + } +} diff --git a/internal/handler/envoyextauth/grpc/middleware/logger/handler.go b/internal/handler/envoyextauth/grpc/middleware/logger/handler.go new file mode 100644 index 000000000..dc9b252f2 --- /dev/null +++ b/internal/handler/envoyextauth/grpc/middleware/logger/handler.go @@ -0,0 +1,45 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package logger + +import ( + "context" + + "github.com/rs/zerolog" + "google.golang.org/grpc" + + "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" +) + +func New(logger zerolog.Logger) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + logCtx := logger.With() + traceCtx := tracecontext.Extract(ctx) + + if traceCtx != nil { + logCtx = logCtx. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) + } + } + + return handler(logCtx.Logger().WithContext(ctx), req) + } +} diff --git a/internal/handler/envoyextauthz/grpc/v3/module.go b/internal/handler/envoyextauth/grpc/middleware/prometheus/defaults.go similarity index 71% rename from internal/handler/envoyextauthz/grpc/v3/module.go rename to internal/handler/envoyextauth/grpc/middleware/prometheus/defaults.go index 5c98b8602..118dd75d2 100644 --- a/internal/handler/envoyextauthz/grpc/v3/module.go +++ b/internal/handler/envoyextauth/grpc/middleware/prometheus/defaults.go @@ -14,4 +14,15 @@ // // SPDX-License-Identifier: Apache-2.0 -package v3 +package prometheus + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +// nolint: gochecknoglobals +var defaultOptions = opts{ + registerer: prometheus.DefaultRegisterer, + namespace: "grpc", + labels: make(prometheus.Labels), +} diff --git a/internal/handler/envoyextauth/grpc/middleware/prometheus/handler.go b/internal/handler/envoyextauth/grpc/middleware/prometheus/handler.go new file mode 100644 index 000000000..e3961b07d --- /dev/null +++ b/internal/handler/envoyextauth/grpc/middleware/prometheus/handler.go @@ -0,0 +1,125 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package prometheus + +import ( + "context" + "strconv" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type metricsHandler struct { + reqCounter *prometheus.CounterVec + reqHistogram *prometheus.HistogramVec + reqInFlight *prometheus.GaugeVec +} + +func New(opts ...Option) grpc.UnaryServerInterceptor { + options := defaultOptions + + for _, opt := range opts { + opt(&options) + } + + counter := promauto.With(options.registerer).NewCounterVec( + prometheus.CounterOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), + Help: "Count all grpc requests by status code, service and method.", + ConstLabels: options.labels, + }, + []string{"grpc_code", "grpc_service", "grpc_method"}, + ) + + histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), + Help: "Duration of all grpc requests by code, service and method.", + ConstLabels: options.labels, + Buckets: []float64{ + 0.00001, 0.000025, 0.00005, 0.000075, // 10, 25, 50, 75µs + 0.0001, 0.00025, 0.0005, 0.00075, // 100, 250, 500, 750µs + 0.001, 0.0025, 0.005, 0.0075, // 1, 2.5, 5, 7.5ms + 0.01, 0.025, 0.05, 0.075, // 10, 25, 50, 75ms + 0.1, 0.25, 0.5, 0.75, // 100, 250, 500 750ms + 1.0, 2.0, // 1, 2s + }, + }, + []string{"grpc_code", "grpc_service", "grpc_method"}, + ) + + gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), + Help: "All the requests in progress", + ConstLabels: options.labels, + }, []string{"grpc_service", "grpc_method"}) + + handler := &metricsHandler{ + reqCounter: counter, + reqHistogram: histogram, + reqInFlight: gauge, + } + + return handler.observeRequest +} + +func (h *metricsHandler) observeRequest( + ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, +) (any, error) { + const MagicNumber = 1e9 + + start := time.Now() + serviceName, methodName := splitMethodName(info.FullMethod) + + h.reqInFlight.WithLabelValues(serviceName, methodName).Inc() + + defer func() { + h.reqInFlight.WithLabelValues(serviceName, methodName).Dec() + }() + + resp, err := handler(ctx, req) + // initialize with default error code + code := codes.Internal + + if err != nil { + s, _ := status.FromError(err) + code = s.Code() + } else { + code = codes.OK + } + + statusCode := strconv.Itoa(int(code)) + h.reqCounter.WithLabelValues(statusCode, serviceName, methodName).Inc() + + elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber + h.reqHistogram.WithLabelValues(statusCode, serviceName, methodName).Observe(elapsed) + + return resp, err +} + +func splitMethodName(fullMethodName string) (string, string) { + fullMethodName = strings.TrimPrefix(fullMethodName, "/") // remove leading slash + if i := strings.Index(fullMethodName, "/"); i >= 0 { + return fullMethodName[:i], fullMethodName[i+1:] + } + return "unknown", "unknown" +} diff --git a/internal/handler/envoyextauth/grpc/middleware/prometheus/options.go b/internal/handler/envoyextauth/grpc/middleware/prometheus/options.go new file mode 100644 index 000000000..4fcc5067f --- /dev/null +++ b/internal/handler/envoyextauth/grpc/middleware/prometheus/options.go @@ -0,0 +1,80 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package prometheus + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +type opts struct { + registerer prometheus.Registerer + labels prometheus.Labels + namespace string + subsystem string +} + +type Option func(*opts) + +func WithRegisterer(registerer prometheus.Registerer) Option { + return func(o *opts) { + if registerer != nil { + o.registerer = registerer + } + } +} + +func WithServiceName(name string) Option { + return func(o *opts) { + if len(name) != 0 { + o.labels["service"] = name + } + } +} + +func WithNamespace(name string) Option { + return func(o *opts) { + if len(name) != 0 { + o.namespace = name + } + } +} + +func WithSubsystem(name string) Option { + return func(o *opts) { + if len(name) != 0 { + o.subsystem = name + } + } +} + +func WithLabel(label, value string) Option { + return func(o *opts) { + if len(label) != 0 && len(value) != 0 { + o.labels[label] = value + } + } +} + +func WithLabels(labels map[string]string) Option { + return func(o *opts) { + for label, value := range labels { + if len(label) != 0 && len(value) != 0 { + o.labels[label] = value + } + } + } +} diff --git a/internal/handler/envoyextauth/grpc/middleware/prometheus/options_test.go b/internal/handler/envoyextauth/grpc/middleware/prometheus/options_test.go new file mode 100644 index 000000000..cdbecbe8f --- /dev/null +++ b/internal/handler/envoyextauth/grpc/middleware/prometheus/options_test.go @@ -0,0 +1,317 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package prometheus + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" +) + +func TestOptionsWithRegisterer(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + value prometheus.Registerer + assert func(t *testing.T, opt *opts) + }{ + { + uc: "nil registerer", + opt: defaultOptions, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Equal(t, prometheus.DefaultRegisterer, opt.registerer) + }, + }, + { + uc: "not nil registerer", + opt: defaultOptions, + value: prometheus.NewRegistry(), + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.NotNil(t, opt.registerer) + assert.NotEqual(t, prometheus.DefaultRegisterer, opt.registerer) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithRegisterer(tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } +} + +func TestOptionsWithServiceName(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + value string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty service name", + opt: opts{}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "not empty service name", + opt: opts{labels: make(prometheus.Labels)}, + value: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Len(t, opt.labels, 1) + assert.Equal(t, "foo", opt.labels["service"]) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithServiceName(tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } +} + +func TestOptionsWithNamespace(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + value string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty namespace", + opt: opts{}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.namespace) + }, + }, + { + uc: "not empty service name", + opt: opts{}, + value: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Equal(t, "foo", opt.namespace) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithNamespace(tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } +} + +func TestOptionsWithSubsystem(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + value string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty subsystem", + opt: opts{}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.subsystem) + }, + }, + { + uc: "not empty subsystem", + opt: opts{}, + value: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Equal(t, "foo", opt.subsystem) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithSubsystem(tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } +} + +func TestOptionsWithLabel(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + name string + value string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty label name", + opt: opts{}, + value: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "empty label value", + opt: opts{}, + name: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "not empty label name & value", + opt: opts{labels: make(prometheus.Labels)}, + name: "foo", + value: "bar", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Len(t, opt.labels, 1) + assert.Equal(t, "bar", opt.labels["foo"]) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithLabel(tc.name, tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } +} + +func TestOptionsWithLabels(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + labels map[string]string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty labels map", + opt: opts{}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "map with empty key", + opt: opts{}, + labels: map[string]string{"": "bar"}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "map with empty value", + opt: opts{}, + labels: map[string]string{"foo": ""}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "map with multiple not empty label name & value", + opt: opts{labels: make(prometheus.Labels)}, + labels: map[string]string{ + "foo": "bar", + "baz": "zab", + }, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Len(t, opt.labels, 2) + assert.Equal(t, "bar", opt.labels["foo"]) + assert.Equal(t, "zab", opt.labels["baz"]) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithLabels(tc.labels) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } +} diff --git a/internal/handler/envoyextauthz/grpc/v3/handler.go b/internal/handler/envoyextauth/grpc/v3/handler.go similarity index 90% rename from internal/handler/envoyextauthz/grpc/v3/handler.go rename to internal/handler/envoyextauth/grpc/v3/handler.go index 2f86e52fd..1b9a93eca 100644 --- a/internal/handler/envoyextauthz/grpc/v3/handler.go +++ b/internal/handler/envoyextauth/grpc/v3/handler.go @@ -28,14 +28,13 @@ import ( ) type Handler struct { - r rules.Repository - s heimdall.JWTSigner - code int + r rules.Repository + s heimdall.JWTSigner } func (h *Handler) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*envoy_auth.CheckResponse, error) { logger := zerolog.Ctx(ctx) - logger.Debug().Msg("Decision Envoy ExtAuthZ endpoint called") + logger.Debug().Msg("Decision Envoy ExtAuth endpoint called") reqCtx := NewRequestContext(ctx, req, h.s) @@ -56,5 +55,5 @@ func (h *Handler) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*env logger.Debug().Msg("Finalizing request") - return reqCtx.Finalize(h.code) + return reqCtx.Finalize() } diff --git a/internal/handler/envoyextauth/grpc/v3/module.go b/internal/handler/envoyextauth/grpc/v3/module.go new file mode 100644 index 000000000..56651af36 --- /dev/null +++ b/internal/handler/envoyextauth/grpc/v3/module.go @@ -0,0 +1,93 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package v3 + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog" + "go.uber.org/fx" + + "github.com/dadrus/heimdall/internal/cache" + "github.com/dadrus/heimdall/internal/config" + "github.com/dadrus/heimdall/internal/handler/listener" + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules" +) + +var Module = fx.Options( // nolint: gochecknoglobals + fx.Invoke(registerHooks), +) + +type hooksArgs struct { + fx.In + + Lifecycle fx.Lifecycle + Config *config.Configuration + Logger zerolog.Logger + Repository rules.Repository + Signer heimdall.JWTSigner + Registerer prometheus.Registerer + Cache cache.Cache +} + +func registerHooks(args hooksArgs) { + ln, err := listener.New("tcp4", args.Config.Serve.Decision) + if err != nil { + args.Logger.Fatal().Err(err).Msg("Could not create listener for the Decision Envoy ExtAuth service") + + return + } + + service := newService(args.Config, args.Registerer, args.Cache, args.Logger, args.Repository, args.Signer) + + args.Lifecycle.Append( + fx.Hook{ + OnStart: func(ctx context.Context) error { + go func() { + args.Logger.Info().Str("_address", ln.Addr().String()). + Msg("Decision Envoy ExtAuth service starts listening") + + if err = service.Serve(ln); err != nil { + args.Logger.Fatal().Err(err).Msg("Could not start Decision Envoy ExtAuth service") + } + }() + + return nil + }, + OnStop: func(ctx context.Context) error { + args.Logger.Info().Msg("Tearing down Decision Envoy ExtAuth service") + + done := make(chan struct{}) + + go func() { + service.GracefulStop() + close(done) + }() + + select { + case <-done: + case <-ctx.Done(): + service.Stop() + } + + return nil + }, + }, + ) +} diff --git a/internal/handler/envoyextauthz/grpc/v3/request_context.go b/internal/handler/envoyextauth/grpc/v3/request_context.go similarity index 87% rename from internal/handler/envoyextauthz/grpc/v3/request_context.go rename to internal/handler/envoyextauth/grpc/v3/request_context.go index 08a92f7d0..8df79069f 100644 --- a/internal/handler/envoyextauthz/grpc/v3/request_context.go +++ b/internal/handler/envoyextauth/grpc/v3/request_context.go @@ -25,6 +25,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" "google.golang.org/genproto/googleapis/rpc/status" "github.com/dadrus/heimdall/internal/heimdall" @@ -64,7 +65,21 @@ func (s *RequestContext) RequestHeaders() map[string]string { func (s *RequestContext) RequestHeader(name string) string { return s.req.Attributes.Request.Http.Headers[name] } -func (s *RequestContext) RequestCookie(name string) string { return "" } +func (s *RequestContext) RequestCookie(name string) string { + values, ok := s.req.Attributes.Request.Http.Headers["cookie"] + if !ok { + return "" + } + + for _, cookie := range strings.Split(values, ";") { + if cookieName, cookieValue, ok := strings.Cut(cookie, "="); + ok && strings.TrimSpace(cookieName) == name { + return strings.TrimSpace(cookieValue) + } + } + + return "" +} func (s *RequestContext) RequestQueryParameter(name string) string { return s.reqURL.Query().Get(name) } @@ -89,7 +104,7 @@ func (s *RequestContext) Signer() heimdall.JWTSigner { return s.jwt func (s *RequestContext) RequestURL() *url.URL { return s.reqURL } func (s *RequestContext) RequestClientIPs() []string { return nil } -func (s *RequestContext) Finalize(statusCode int) (*envoy_auth.CheckResponse, error) { +func (s *RequestContext) Finalize() (*envoy_auth.CheckResponse, error) { if s.err != nil { return nil, s.err } @@ -121,7 +136,7 @@ func (s *RequestContext) Finalize(statusCode int) (*envoy_auth.CheckResponse, er } return &envoy_auth.CheckResponse{ - Status: &status.Status{Code: int32(statusCode)}, + Status: &status.Status{Code: int32(envoy_type.StatusCode_OK)}, HttpResponse: &envoy_auth.CheckResponse_OkResponse{ OkResponse: &envoy_auth.OkHttpResponse{Headers: headers}, }, diff --git a/internal/handler/envoyextauth/grpc/v3/service.go b/internal/handler/envoyextauth/grpc/v3/service.go new file mode 100644 index 000000000..022b38ce6 --- /dev/null +++ b/internal/handler/envoyextauth/grpc/v3/service.go @@ -0,0 +1,67 @@ +package v3 + +import ( + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/grpc-ecosystem/go-grpc-middleware/recovery" + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + + "github.com/dadrus/heimdall/internal/cache" + "github.com/dadrus/heimdall/internal/config" + accesslogmiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/accesslog" + cachemiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/cache" + errormiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/errorhandler" + loggermiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/logger" + prometheus2 "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/prometheus" + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules" +) + +func newService( + conf *config.Configuration, + registrer prometheus.Registerer, + cch cache.Cache, + logger zerolog.Logger, + repository rules.Repository, + signer heimdall.JWTSigner, +) *grpc.Server { + service := conf.Serve.Decision + + interceptors := []grpc.UnaryServerInterceptor{ + grpc_recovery.UnaryServerInterceptor(), + otelgrpc.UnaryServerInterceptor(), + } + + if conf.Metrics.Enabled { + interceptors = append(interceptors, + prometheus2.New( + prometheus2.WithServiceName("decision"), + prometheus2.WithRegisterer(registrer), + ), + ) + } + + interceptors = append(interceptors, + accesslogmiddleware.New(logger), + loggermiddleware.New(logger), + errormiddleware.New( + errormiddleware.WithVerboseErrors(service.Respond.Verbose), + errormiddleware.WithPreconditionErrorCode(service.Respond.With.ArgumentError.Code), + errormiddleware.WithAuthenticationErrorCode(service.Respond.With.AuthenticationError.Code), + errormiddleware.WithAuthorizationErrorCode(service.Respond.With.AuthorizationError.Code), + errormiddleware.WithCommunicationErrorCode(service.Respond.With.CommunicationError.Code), + errormiddleware.WithMethodErrorCode(service.Respond.With.BadMethodError.Code), + errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), + errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), + ), + cachemiddleware.New(cch), + ) + + srv := grpc.NewServer(grpc.ChainUnaryInterceptor(interceptors...)) + + envoy_auth.RegisterAuthorizationServer(srv, &Handler{r: repository, s: signer}) + + return srv +} diff --git a/internal/x/errorchain/error_chain.go b/internal/x/errorchain/error_chain.go index e58c2fd76..c137a4472 100644 --- a/internal/x/errorchain/error_chain.go +++ b/internal/x/errorchain/error_chain.go @@ -17,168 +17,172 @@ package errorchain import ( - "encoding/xml" - "errors" - "fmt" - "reflect" - "strings" - - "github.com/goccy/go-json" - "github.com/iancoleman/strcase" + "encoding/xml" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/goccy/go-json" + "github.com/iancoleman/strcase" ) type element struct { - err error - msg string - next *element + err error + msg string + next *element } type ErrorChain struct { // nolint: errname - head *element - tail *element - context any + head *element + tail *element + context any } func New(err error) *ErrorChain { - chain := &ErrorChain{} + chain := &ErrorChain{} - return chain.causedBy(err, "") + return chain.causedBy(err, "") } func NewWithMessage(err error, message string) *ErrorChain { - chain := &ErrorChain{} + chain := &ErrorChain{} - return chain.causedBy(err, message) + return chain.causedBy(err, message) } func NewWithMessagef(err error, format string, a ...any) *ErrorChain { - chain := &ErrorChain{} + chain := &ErrorChain{} - return chain.causedBy(err, fmt.Sprintf(format, a...)) + return chain.causedBy(err, fmt.Sprintf(format, a...)) } func (ec *ErrorChain) Error() string { - var errs []string + var errs []string - for c := ec.head; c != nil; c = c.next { - if len(c.msg) == 0 { - errs = append(errs, c.err.Error()) - } else { - errs = append(errs, fmt.Sprintf("%s: %s", c.err.Error(), c.msg)) - } - } + for c := ec.head; c != nil; c = c.next { + if len(c.msg) == 0 { + errs = append(errs, c.err.Error()) + } else { + errs = append(errs, fmt.Sprintf("%s: %s", c.err.Error(), c.msg)) + } + } - return strings.Join(errs, ": ") + return strings.Join(errs, ": ") } func (ec *ErrorChain) causedBy(err error, msg string) *ErrorChain { - wrappedError := &element{err: err, msg: msg} + wrappedError := &element{err: err, msg: msg} - if ec.head == nil { - ec.head = wrappedError - ec.tail = wrappedError + if ec.head == nil { + ec.head = wrappedError + ec.tail = wrappedError - return ec - } + return ec + } - ec.tail.next = wrappedError - ec.tail = wrappedError + ec.tail.next = wrappedError + ec.tail = wrappedError - return ec + return ec } func (ec *ErrorChain) CausedBy(err error) *ErrorChain { - return ec.causedBy(err, "") + return ec.causedBy(err, "") } func (ec *ErrorChain) WithErrorContext(context any) *ErrorChain { - ec.context = context + ec.context = context - return ec + return ec } func (ec *ErrorChain) Unwrap() error { - if ec.head == nil || ec.head.next == nil { - return nil - } + if ec.head == nil || ec.head.next == nil { + return nil + } - return &ErrorChain{ - head: ec.head.next, - tail: ec.tail, - context: ec.context, - } + return &ErrorChain{ + head: ec.head.next, + tail: ec.tail, + context: ec.context, + } } func (ec *ErrorChain) Is(target error) bool { - if ec.head == nil { - return false - } + if ec.head == nil { + return false + } - return errors.Is(ec.head.err, target) + return errors.Is(ec.head.err, target) } func (ec *ErrorChain) As(target any) bool { - if ec.head == nil { - return false - } + if ec.head == nil { + return false + } - if ec.asTarget(target) { - return true - } + if ec.asTarget(target) { + return true + } - return errors.As(ec.head.err, target) + return errors.As(ec.head.err, target) } func (ec *ErrorChain) asTarget(target any) bool { - if ec.context == nil { - return false - } + if ec.context == nil { + return false + } - val := reflect.ValueOf(target) - targetType := val.Type().Elem() + val := reflect.ValueOf(target) + targetType := val.Type().Elem() - if targetType.Kind() != reflect.Interface || !reflect.TypeOf(ec.context).AssignableTo(targetType) { - return false - } + if targetType.Kind() != reflect.Interface || !reflect.TypeOf(ec.context).AssignableTo(targetType) { + return false + } - val.Elem().Set(reflect.ValueOf(ec.context)) + val.Elem().Set(reflect.ValueOf(ec.context)) - return true + return true } func (ec *ErrorChain) ErrorContext() any { - return ec.context + return ec.context } func (ec *ErrorChain) Errors() []error { - var errs []error + var errs []error - for c := ec.head; c != nil; c = c.next { - errs = append(errs, c.err) - } + for c := ec.head; c != nil; c = c.next { + errs = append(errs, c.err) + } - return errs + return errs } func (ec *ErrorChain) MarshalJSON() ([]byte, error) { - return json.Marshal( - message{ - Code: strcase.ToLowerCamel(ec.head.err.Error()), - Message: ec.head.msg, - }) + return json.Marshal( + message{ + Code: strcase.ToLowerCamel(ec.head.err.Error()), + Message: ec.head.msg, + }) } func (ec *ErrorChain) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error { - return encoder.Encode( - message{ - XMLName: xml.Name{Local: "error"}, - Code: strcase.ToLowerCamel(ec.head.err.Error()), - Message: ec.head.msg, - }) + return encoder.Encode( + message{ + XMLName: xml.Name{Local: "error"}, + Code: strcase.ToLowerCamel(ec.head.err.Error()), + Message: ec.head.msg, + }) +} + +func (ec *ErrorChain) String() string { + return fmt.Sprintf("%s: %s", ec.head.err.Error(), ec.head.msg) } type message struct { - XMLName xml.Name `json:"-"` - Code string `xml:"code" json:"code"` - Message string `xml:"message,omitempty" json:"message,omitempty"` + XMLName xml.Name `json:"-"` + Code string `xml:"code" json:"code"` + Message string `xml:"message,omitempty" json:"message,omitempty"` } From 39cf037924eca096d4c6f52845c13337a85ae5d9 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Tue, 31 Jan 2023 13:57:10 +0100 Subject: [PATCH 04/67] package structure refactored --- cmd/serve/decision.go | 2 +- .../grpc/middleware/errorhandler/defaults.go | 46 --------------- .../{grpc/v3 => grpcv3}/handler.go | 2 +- .../middleware/accesslog/accesslog_handler.go | 0 .../middleware/cache/cache.go | 0 .../middleware/errorhandler/defaults.go | 45 ++++++++++++++ .../middleware/errorhandler/error_handler.go | 20 +++---- .../middleware/errorhandler/options.go | 59 +++++++++++-------- .../middleware/logger/handler.go | 0 .../middleware/prometheus/defaults.go | 0 .../middleware/prometheus/handler.go | 0 .../middleware/prometheus/options.go | 0 .../middleware/prometheus/options_test.go | 0 .../{grpc/v3 => grpcv3}/module.go | 2 +- .../{grpc/v3 => grpcv3}/request_context.go | 2 +- .../{grpc/v3 => grpcv3}/service.go | 22 +++---- 16 files changed, 105 insertions(+), 95 deletions(-) delete mode 100644 internal/handler/envoyextauth/grpc/middleware/errorhandler/defaults.go rename internal/handler/envoyextauth/{grpc/v3 => grpcv3}/handler.go (99%) rename internal/handler/envoyextauth/{grpc => grpcv3}/middleware/accesslog/accesslog_handler.go (100%) rename internal/handler/envoyextauth/{grpc => grpcv3}/middleware/cache/cache.go (100%) create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go rename internal/handler/envoyextauth/{grpc => grpcv3}/middleware/errorhandler/error_handler.go (82%) rename internal/handler/envoyextauth/{grpc => grpcv3}/middleware/errorhandler/options.go (54%) rename internal/handler/envoyextauth/{grpc => grpcv3}/middleware/logger/handler.go (100%) rename internal/handler/envoyextauth/{grpc => grpcv3}/middleware/prometheus/defaults.go (100%) rename internal/handler/envoyextauth/{grpc => grpcv3}/middleware/prometheus/handler.go (100%) rename internal/handler/envoyextauth/{grpc => grpcv3}/middleware/prometheus/options.go (100%) rename internal/handler/envoyextauth/{grpc => grpcv3}/middleware/prometheus/options_test.go (100%) rename internal/handler/envoyextauth/{grpc/v3 => grpcv3}/module.go (99%) rename internal/handler/envoyextauth/{grpc/v3 => grpcv3}/request_context.go (99%) rename internal/handler/envoyextauth/{grpc/v3 => grpcv3}/service.go (83%) diff --git a/cmd/serve/decision.go b/cmd/serve/decision.go index eee56a4e0..a67a33acd 100644 --- a/cmd/serve/decision.go +++ b/cmd/serve/decision.go @@ -23,7 +23,7 @@ import ( "github.com/dadrus/heimdall/internal" "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/handler/decision" - envoy_extauth "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/v3" + envoy_extauth "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3" ) // NewDecisionCommand represents the "serve decision" command. diff --git a/internal/handler/envoyextauth/grpc/middleware/errorhandler/defaults.go b/internal/handler/envoyextauth/grpc/middleware/errorhandler/defaults.go deleted file mode 100644 index 50640e58d..000000000 --- a/internal/handler/envoyextauth/grpc/middleware/errorhandler/defaults.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2023 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package errorhandler - -import ( - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -var defaultOptions = opts{ //nolint:gochecknoglobals - authenticationError: func(err error, verbose bool) error { - return status.Error(codes.Unauthenticated, messageFrom(err, verbose)) - }, - authorizationError: func(err error, verbose bool) error { - return status.Error(codes.PermissionDenied, messageFrom(err, verbose)) - }, - communicationError: func(err error, verbose bool) error { - return status.Error(codes.Aborted, messageFrom(err, verbose)) - }, - preconditionError: func(err error, verbose bool) error { - return status.Error(codes.FailedPrecondition, messageFrom(err, verbose)) - }, - badMethodError: func(err error, verbose bool) error { - return status.Error(codes.FailedPrecondition, messageFrom(err, verbose)) - }, - noRuleError: func(err error, verbose bool) error { - return status.Error(codes.NotFound, messageFrom(err, verbose)) - }, - internalError: func(err error, verbose bool) error { - return status.Error(codes.Internal, messageFrom(err, verbose)) - }, -} diff --git a/internal/handler/envoyextauth/grpc/v3/handler.go b/internal/handler/envoyextauth/grpcv3/handler.go similarity index 99% rename from internal/handler/envoyextauth/grpc/v3/handler.go rename to internal/handler/envoyextauth/grpcv3/handler.go index 1b9a93eca..be9bc3c40 100644 --- a/internal/handler/envoyextauth/grpc/v3/handler.go +++ b/internal/handler/envoyextauth/grpcv3/handler.go @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package v3 +package grpcv3 import ( "context" diff --git a/internal/handler/envoyextauth/grpc/middleware/accesslog/accesslog_handler.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go similarity index 100% rename from internal/handler/envoyextauth/grpc/middleware/accesslog/accesslog_handler.go rename to internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go diff --git a/internal/handler/envoyextauth/grpc/middleware/cache/cache.go b/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go similarity index 100% rename from internal/handler/envoyextauth/grpc/middleware/cache/cache.go rename to internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go new file mode 100644 index 000000000..e2c3696cb --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go @@ -0,0 +1,45 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package errorhandler + +import ( + "net/http" +) + +var defaultOptions = opts{ //nolint:gochecknoglobals + authenticationError: func(err error, verbose bool) (any, error) { + return createDeniedResponse(http.StatusUnauthorized, err, verbose), nil + }, + authorizationError: func(err error, verbose bool) (any, error) { + return createDeniedResponse(http.StatusForbidden, err, verbose), nil + }, + communicationError: func(err error, verbose bool) (any, error) { + return createDeniedResponse(http.StatusBadGateway, err, verbose), nil + }, + preconditionError: func(err error, verbose bool) (any, error) { + return createDeniedResponse(http.StatusBadRequest, err, verbose), nil + }, + badMethodError: func(err error, verbose bool) (any, error) { + return createDeniedResponse(http.StatusMethodNotAllowed, err, verbose), nil + }, + noRuleError: func(err error, verbose bool) (any, error) { + return createDeniedResponse(http.StatusNotFound, err, verbose), nil + }, + internalError: func(err error, verbose bool) (any, error) { + return createDeniedResponse(http.StatusInternalServerError, err, verbose), nil + }, +} diff --git a/internal/handler/envoyextauth/grpc/middleware/errorhandler/error_handler.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go similarity index 82% rename from internal/handler/envoyextauth/grpc/middleware/errorhandler/error_handler.go rename to internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go index 9d7525552..be9f94be4 100644 --- a/internal/handler/envoyextauth/grpc/middleware/errorhandler/error_handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go @@ -56,17 +56,17 @@ func (h *handler) handle(ctx context.Context, req any, info *grpc.UnaryServerInf switch { case errors.Is(err, heimdall.ErrAuthentication): - err = h.authenticationError(err, h.verboseErrors) + return h.authenticationError(err, h.verboseErrors) case errors.Is(err, heimdall.ErrAuthorization): - err = h.authorizationError(err, h.verboseErrors) + return h.authorizationError(err, h.verboseErrors) case errors.Is(err, heimdall.ErrCommunicationTimeout) || errors.Is(err, heimdall.ErrCommunication): - err = h.communicationError(err, h.verboseErrors) + return h.communicationError(err, h.verboseErrors) case errors.Is(err, heimdall.ErrArgument): - err = h.preconditionError(err, h.verboseErrors) + return h.preconditionError(err, h.verboseErrors) case errors.Is(err, heimdall.ErrMethodNotAllowed): - err = h.badMethodError(err, h.verboseErrors) + return h.badMethodError(err, h.verboseErrors) case errors.Is(err, heimdall.ErrNoRuleFound): - err = h.noRuleError(err, h.verboseErrors) + return h.noRuleError(err, h.verboseErrors) case errors.Is(err, &heimdall.RedirectError{}): var redirectError *heimdall.RedirectError @@ -74,8 +74,8 @@ func (h *handler) handle(ctx context.Context, req any, info *grpc.UnaryServerInf return &envoy_auth.CheckResponse{ Status: &status.Status{Code: int32(redirectError.Code)}, - HttpResponse: &envoy_auth.CheckResponse_OkResponse{ - OkResponse: &envoy_auth.OkHttpResponse{Headers: []*envoy_core.HeaderValueOption{ + HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{ + DeniedResponse: &envoy_auth.DeniedHttpResponse{Headers: []*envoy_core.HeaderValueOption{ { Header: &envoy_core.HeaderValue{ Key: "Location", @@ -90,8 +90,6 @@ func (h *handler) handle(ctx context.Context, req any, info *grpc.UnaryServerInf logger := zerolog.Ctx(ctx) logger.Error().Err(err).Msg("Internal error occurred") - err = h.internalError(err, h.verboseErrors) + return h.internalError(err, h.verboseErrors) } - - return req, err } diff --git a/internal/handler/envoyextauth/grpc/middleware/errorhandler/options.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go similarity index 54% rename from internal/handler/envoyextauth/grpc/middleware/errorhandler/options.go rename to internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go index 818e0d73f..5e0f980d5 100644 --- a/internal/handler/envoyextauth/grpc/middleware/errorhandler/options.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go @@ -19,19 +19,20 @@ package errorhandler import ( "fmt" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + 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) error - authorizationError func(err error, verbose bool) error - communicationError func(err error, verbose bool) error - preconditionError func(err error, verbose bool) error - badMethodError func(err error, verbose bool) error - noRuleError func(err error, verbose bool) error - internalError func(err error, verbose bool) error + 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) } type Option func(*opts) @@ -39,8 +40,8 @@ type Option func(*opts) func WithPreconditionErrorCode(code int) Option { return func(o *opts) { if code != 0 { - o.preconditionError = func(err error, verbose bool) error { - return status.Error(codes.Code(code), messageFrom(err, verbose)) + o.preconditionError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil } } } @@ -49,8 +50,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) error { - return status.Error(codes.Code(code), messageFrom(err, verbose)) + o.authenticationError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil } } } @@ -59,8 +60,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) error { - return status.Error(codes.Code(code), messageFrom(err, verbose)) + o.authorizationError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil } } } @@ -69,8 +70,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) error { - return status.Error(codes.Code(code), messageFrom(err, verbose)) + o.communicationError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil } } } @@ -79,8 +80,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) error { - return status.Error(codes.Code(code), messageFrom(err, verbose)) + o.internalError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil } } } @@ -89,8 +90,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) error { - return status.Error(codes.Code(code), messageFrom(err, verbose)) + o.badMethodError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil } } } @@ -99,8 +100,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) error { - return status.Error(codes.Code(code), messageFrom(err, verbose)) + o.noRuleError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil } } } @@ -112,6 +113,18 @@ func WithVerboseErrors(flag bool) Option { } } +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 "" diff --git a/internal/handler/envoyextauth/grpc/middleware/logger/handler.go b/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go similarity index 100% rename from internal/handler/envoyextauth/grpc/middleware/logger/handler.go rename to internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go diff --git a/internal/handler/envoyextauth/grpc/middleware/prometheus/defaults.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go similarity index 100% rename from internal/handler/envoyextauth/grpc/middleware/prometheus/defaults.go rename to internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go diff --git a/internal/handler/envoyextauth/grpc/middleware/prometheus/handler.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go similarity index 100% rename from internal/handler/envoyextauth/grpc/middleware/prometheus/handler.go rename to internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go diff --git a/internal/handler/envoyextauth/grpc/middleware/prometheus/options.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options.go similarity index 100% rename from internal/handler/envoyextauth/grpc/middleware/prometheus/options.go rename to internal/handler/envoyextauth/grpcv3/middleware/prometheus/options.go diff --git a/internal/handler/envoyextauth/grpc/middleware/prometheus/options_test.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options_test.go similarity index 100% rename from internal/handler/envoyextauth/grpc/middleware/prometheus/options_test.go rename to internal/handler/envoyextauth/grpcv3/middleware/prometheus/options_test.go diff --git a/internal/handler/envoyextauth/grpc/v3/module.go b/internal/handler/envoyextauth/grpcv3/module.go similarity index 99% rename from internal/handler/envoyextauth/grpc/v3/module.go rename to internal/handler/envoyextauth/grpcv3/module.go index 56651af36..44560d41b 100644 --- a/internal/handler/envoyextauth/grpc/v3/module.go +++ b/internal/handler/envoyextauth/grpcv3/module.go @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package v3 +package grpcv3 import ( "context" diff --git a/internal/handler/envoyextauth/grpc/v3/request_context.go b/internal/handler/envoyextauth/grpcv3/request_context.go similarity index 99% rename from internal/handler/envoyextauth/grpc/v3/request_context.go rename to internal/handler/envoyextauth/grpcv3/request_context.go index 8df79069f..d4fdc9f3b 100644 --- a/internal/handler/envoyextauth/grpc/v3/request_context.go +++ b/internal/handler/envoyextauth/grpcv3/request_context.go @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package v3 +package grpcv3 import ( "context" diff --git a/internal/handler/envoyextauth/grpc/v3/service.go b/internal/handler/envoyextauth/grpcv3/service.go similarity index 83% rename from internal/handler/envoyextauth/grpc/v3/service.go rename to internal/handler/envoyextauth/grpcv3/service.go index 022b38ce6..a4fbe7ccf 100644 --- a/internal/handler/envoyextauth/grpc/v3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -1,4 +1,4 @@ -package v3 +package grpcv3 import ( envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" @@ -10,11 +10,11 @@ import ( "github.com/dadrus/heimdall/internal/cache" "github.com/dadrus/heimdall/internal/config" - accesslogmiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/accesslog" - cachemiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/cache" - errormiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/errorhandler" - loggermiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/logger" - prometheus2 "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpc/middleware/prometheus" + accesslogmiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/accesslog" + cachemiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/cache" + errormiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/errorhandler" + loggermiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/logger" + prometheusmiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/prometheus" "github.com/dadrus/heimdall/internal/heimdall" "github.com/dadrus/heimdall/internal/rules" ) @@ -36,16 +36,14 @@ func newService( if conf.Metrics.Enabled { interceptors = append(interceptors, - prometheus2.New( - prometheus2.WithServiceName("decision"), - prometheus2.WithRegisterer(registrer), + prometheusmiddleware.New( + prometheusmiddleware.WithServiceName("decision"), + prometheusmiddleware.WithRegisterer(registrer), ), ) } interceptors = append(interceptors, - accesslogmiddleware.New(logger), - loggermiddleware.New(logger), errormiddleware.New( errormiddleware.WithVerboseErrors(service.Respond.Verbose), errormiddleware.WithPreconditionErrorCode(service.Respond.With.ArgumentError.Code), @@ -56,6 +54,8 @@ func newService( errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), ), + accesslogmiddleware.New(logger), + loggermiddleware.New(logger), cachemiddleware.New(cch), ) From 362f41c762a44419fafef0d45ac1bcfef5aee0c9 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Tue, 31 Jan 2023 14:19:31 +0100 Subject: [PATCH 05/67] small updates in descriptions --- .../fiber/middleware/prometheus/handler.go | 164 +++++++++--------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/internal/fiber/middleware/prometheus/handler.go b/internal/fiber/middleware/prometheus/handler.go index 16b5802c9..58106a284 100644 --- a/internal/fiber/middleware/prometheus/handler.go +++ b/internal/fiber/middleware/prometheus/handler.go @@ -17,107 +17,107 @@ package prometheus import ( - "errors" - "strconv" - "time" + "errors" + "strconv" + "time" - "github.com/gofiber/fiber/v2" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/gofiber/fiber/v2" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" ) type metricsHandler struct { - reqCounter *prometheus.CounterVec - reqHistogram *prometheus.HistogramVec - reqInFlight *prometheus.GaugeVec - filterOperation OperationFilter + reqCounter *prometheus.CounterVec + reqHistogram *prometheus.HistogramVec + reqInFlight *prometheus.GaugeVec + filterOperation OperationFilter } func New(opts ...Option) fiber.Handler { - options := defaultOptions - - for _, opt := range opts { - opt(&options) - } - - counter := promauto.With(options.registerer).NewCounterVec( - prometheus.CounterOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), - Help: "Count all http requests by status code, method and path.", - ConstLabels: options.labels, - }, - []string{"status_code", "method", "path"}, - ) - - histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), - Help: "Duration of all HTTP requests by status code, method and path.", - ConstLabels: options.labels, - Buckets: []float64{ - 0.00001, 0.00005, // 10, 50µs - 0.0001, 0.00025, 0.0005, 0.00075, // 100, 250, 500, 750µs - 0.001, 0.0025, 0.005, 0.0075, // 1, 2.5, 5, 7.5ms - 0.01, 0.025, 0.05, 0.075, // 10, 25, 50, 75ms - 0.1, 0.25, 0.5, // 100, 250, 500 ms - 1.0, 2.0, 5.0, 10.0, 15.0, // 1, 2, 5, 10, 20s - }, - }, - []string{"status_code", "method", "path"}, - ) - - gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), - Help: "All the requests in progress", - ConstLabels: options.labels, - }, []string{"method"}) - - handler := &metricsHandler{ - reqCounter: counter, - reqHistogram: histogram, - reqInFlight: gauge, - filterOperation: options.filterOperation, - } - - return handler.observeRequest + options := defaultOptions + + for _, opt := range opts { + opt(&options) + } + + counter := promauto.With(options.registerer).NewCounterVec( + prometheus.CounterOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), + Help: "Count all requests by status code, method and path.", + ConstLabels: options.labels, + }, + []string{"status_code", "method", "path"}, + ) + + histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), + Help: "Duration of all requests by status code, method and path.", + ConstLabels: options.labels, + Buckets: []float64{ + 0.00001, 0.00005, // 10, 50µs + 0.0001, 0.00025, 0.0005, 0.00075, // 100, 250, 500, 750µs + 0.001, 0.0025, 0.005, 0.0075, // 1, 2.5, 5, 7.5ms + 0.01, 0.025, 0.05, 0.075, // 10, 25, 50, 75ms + 0.1, 0.25, 0.5, // 100, 250, 500 ms + 1.0, 2.0, 5.0, 10.0, 15.0, // 1, 2, 5, 10, 20s + }, + }, + []string{"status_code", "method", "path"}, + ) + + gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), + Help: "All the requests in progress", + ConstLabels: options.labels, + }, []string{"method"}) + + handler := &metricsHandler{ + reqCounter: counter, + reqHistogram: histogram, + reqInFlight: gauge, + filterOperation: options.filterOperation, + } + + return handler.observeRequest } func (h *metricsHandler) observeRequest(ctx *fiber.Ctx) error { - const MagicNumber = 1e9 + const MagicNumber = 1e9 - start := time.Now() + start := time.Now() - if h.filterOperation(ctx) { - return ctx.Next() - } + if h.filterOperation(ctx) { + return ctx.Next() + } - method := ctx.Route().Method + method := ctx.Route().Method - h.reqInFlight.WithLabelValues(method).Inc() + h.reqInFlight.WithLabelValues(method).Inc() - defer func() { - h.reqInFlight.WithLabelValues(method).Dec() - }() + defer func() { + h.reqInFlight.WithLabelValues(method).Dec() + }() - err := ctx.Next() - // initialize with default error code - status := fiber.StatusInternalServerError + err := ctx.Next() + // initialize with default error code + status := fiber.StatusInternalServerError - if err != nil { - var ferr *fiber.Error + if err != nil { + var ferr *fiber.Error - if errors.As(err, &ferr) { - status = ferr.Code - } - } else { - status = ctx.Response().StatusCode() - } + if errors.As(err, &ferr) { + status = ferr.Code + } + } else { + status = ctx.Response().StatusCode() + } - path := ctx.Route().Path - statusCode := strconv.Itoa(status) - h.reqCounter.WithLabelValues(statusCode, method, path).Inc() + path := ctx.Route().Path + statusCode := strconv.Itoa(status) + h.reqCounter.WithLabelValues(statusCode, method, path).Inc() - elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber - h.reqHistogram.WithLabelValues(statusCode, method, path).Observe(elapsed) + elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber + h.reqHistogram.WithLabelValues(statusCode, method, path).Observe(elapsed) - return err + return err } From b0cf8841bd0084b099d76e7189f4f2aec37dc215 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Tue, 31 Jan 2023 14:20:25 +0100 Subject: [PATCH 06/67] metris implementation updated to take tunneled http requests from envoy into account --- .../grpcv3/middleware/prometheus/defaults.go | 2 +- .../grpcv3/middleware/prometheus/handler.go | 47 +++++++++---------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go index 118dd75d2..8674bf970 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go @@ -23,6 +23,6 @@ import ( // nolint: gochecknoglobals var defaultOptions = opts{ registerer: prometheus.DefaultRegisterer, - namespace: "grpc", + namespace: "http", labels: make(prometheus.Labels), } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go index e3961b07d..f2ebfd855 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go @@ -19,9 +19,9 @@ package prometheus import ( "context" "strconv" - "strings" "time" + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "google.golang.org/grpc" @@ -45,15 +45,15 @@ func New(opts ...Option) grpc.UnaryServerInterceptor { counter := promauto.With(options.registerer).NewCounterVec( prometheus.CounterOpts{ Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), - Help: "Count all grpc requests by status code, service and method.", + Help: "Count all requests by status code, service and method.", ConstLabels: options.labels, }, - []string{"grpc_code", "grpc_service", "grpc_method"}, + []string{"status_code", "method", "path"}, ) histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), - Help: "Duration of all grpc requests by code, service and method.", + Help: "Duration of all requests by code, service and method.", ConstLabels: options.labels, Buckets: []float64{ 0.00001, 0.000025, 0.00005, 0.000075, // 10, 25, 50, 75µs @@ -64,14 +64,14 @@ func New(opts ...Option) grpc.UnaryServerInterceptor { 1.0, 2.0, // 1, 2s }, }, - []string{"grpc_code", "grpc_service", "grpc_method"}, + []string{"status_code", "method", "path"}, ) gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), Help: "All the requests in progress", ConstLabels: options.labels, - }, []string{"grpc_service", "grpc_method"}) + }, []string{"method"}) handler := &metricsHandler{ reqCounter: counter, @@ -88,38 +88,35 @@ func (h *metricsHandler) observeRequest( const MagicNumber = 1e9 start := time.Now() - serviceName, methodName := splitMethodName(info.FullMethod) + method := "GRPC" + path := info.FullMethod + code := int(codes.OK) - h.reqInFlight.WithLabelValues(serviceName, methodName).Inc() + if cr, ok := req.(*envoy_auth.CheckRequest); ok { + method = cr.Attributes.Request.Http.Method + path = cr.Attributes.Request.Http.Path + } + + h.reqInFlight.WithLabelValues(method).Inc() defer func() { - h.reqInFlight.WithLabelValues(serviceName, methodName).Dec() + h.reqInFlight.WithLabelValues(method).Dec() }() resp, err := handler(ctx, req) - // initialize with default error code - code := codes.Internal if err != nil { s, _ := status.FromError(err) - code = s.Code() - } else { - code = codes.OK + code = int(s.Code()) + } else if cr, ok := req.(*envoy_auth.CheckResponse); ok { + code = int(cr.Status.Code) } - statusCode := strconv.Itoa(int(code)) - h.reqCounter.WithLabelValues(statusCode, serviceName, methodName).Inc() + statusCode := strconv.Itoa(code) + h.reqCounter.WithLabelValues(statusCode, method, path).Inc() elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber - h.reqHistogram.WithLabelValues(statusCode, serviceName, methodName).Observe(elapsed) + h.reqHistogram.WithLabelValues(statusCode, method, path).Observe(elapsed) return resp, err } - -func splitMethodName(fullMethodName string) (string, string) { - fullMethodName = strings.TrimPrefix(fullMethodName, "/") // remove leading slash - if i := strings.Index(fullMethodName, "/"); i >= 0 { - return fullMethodName[:i], fullMethodName[i+1:] - } - return "unknown", "unknown" -} From 602e66717d899bd1df477b2fdab251f55c49a51c Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Tue, 31 Jan 2023 14:56:57 +0100 Subject: [PATCH 07/67] type label added --- internal/handler/decision/app.go | 141 +++++++++--------- .../handler/envoyextauth/grpcv3/service.go | 1 + 2 files changed, 72 insertions(+), 70 deletions(-) diff --git a/internal/handler/decision/app.go b/internal/handler/decision/app.go index 724498ace..a56a81d28 100644 --- a/internal/handler/decision/app.go +++ b/internal/handler/decision/app.go @@ -17,90 +17,91 @@ package decision import ( - "strings" + "strings" - "github.com/goccy/go-json" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/recover" - "github.com/prometheus/client_golang/prometheus" - "github.com/rs/zerolog" - "go.opentelemetry.io/otel" - "go.uber.org/fx" + "github.com/goccy/go-json" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel" + "go.uber.org/fx" - "github.com/dadrus/heimdall/internal/cache" - "github.com/dadrus/heimdall/internal/config" - accesslogmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/accesslog" - cachemiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/cache" - errormiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/errorhandler" - loggermiddlerware "github.com/dadrus/heimdall/internal/fiber/middleware/logger" - tracingmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/opentelemetry" - prometheusmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/prometheus" - "github.com/dadrus/heimdall/internal/x" + "github.com/dadrus/heimdall/internal/cache" + "github.com/dadrus/heimdall/internal/config" + accesslogmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/accesslog" + cachemiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/cache" + errormiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/errorhandler" + loggermiddlerware "github.com/dadrus/heimdall/internal/fiber/middleware/logger" + tracingmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/opentelemetry" + prometheusmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/prometheus" + "github.com/dadrus/heimdall/internal/x" ) type appArgs struct { - fx.In + fx.In - Config *config.Configuration - Registerer prometheus.Registerer - Cache cache.Cache - Logger zerolog.Logger + Config *config.Configuration + Registerer prometheus.Registerer + Cache cache.Cache + Logger zerolog.Logger } func newApp(args appArgs) *fiber.App { - service := args.Config.Serve.Decision + service := args.Config.Serve.Decision - app := fiber.New(fiber.Config{ - AppName: "Heimdall Decision Service", - ReadTimeout: service.Timeout.Read, - WriteTimeout: service.Timeout.Write, - IdleTimeout: service.Timeout.Idle, - DisableStartupMessage: true, - EnableTrustedProxyCheck: true, - TrustedProxies: x.IfThenElseExec(service.TrustedProxies != nil, - func() []string { return *service.TrustedProxies }, - func() []string { return []string{} }), - JSONDecoder: json.Unmarshal, - JSONEncoder: json.Marshal, - }) + app := fiber.New(fiber.Config{ + AppName: "Heimdall Decision Service", + ReadTimeout: service.Timeout.Read, + WriteTimeout: service.Timeout.Write, + IdleTimeout: service.Timeout.Idle, + DisableStartupMessage: true, + EnableTrustedProxyCheck: true, + TrustedProxies: x.IfThenElseExec(service.TrustedProxies != nil, + func() []string { return *service.TrustedProxies }, + func() []string { return []string{} }), + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, + }) - app.Use(recover.New(recover.Config{EnableStackTrace: true})) - app.Use(tracingmiddleware.New( - tracingmiddleware.WithTracer(otel.GetTracerProvider().Tracer("github.com/dadrus/heimdall/decision")))) + app.Use(recover.New(recover.Config{EnableStackTrace: true})) + app.Use(tracingmiddleware.New( + tracingmiddleware.WithTracer(otel.GetTracerProvider().Tracer("github.com/dadrus/heimdall/decision")))) - if args.Config.Metrics.Enabled { - app.Use(prometheusmiddleware.New( - prometheusmiddleware.WithServiceName("decision"), - prometheusmiddleware.WithRegisterer(args.Registerer), - )) - } + if args.Config.Metrics.Enabled { + app.Use(prometheusmiddleware.New( + prometheusmiddleware.WithServiceName("decision"), + prometheusmiddleware.WithLabel("type", "http"), + prometheusmiddleware.WithRegisterer(args.Registerer), + )) + } - app.Use(accesslogmiddleware.New(args.Logger)) - app.Use(loggermiddlerware.New(args.Logger)) + app.Use(accesslogmiddleware.New(args.Logger)) + app.Use(loggermiddlerware.New(args.Logger)) - if service.CORS != nil { - app.Use(cors.New(cors.Config{ - AllowOrigins: strings.Join(service.CORS.AllowedOrigins, ","), - AllowMethods: strings.Join(service.CORS.AllowedMethods, ","), - AllowHeaders: strings.Join(service.CORS.AllowedHeaders, ","), - AllowCredentials: service.CORS.AllowCredentials, - ExposeHeaders: strings.Join(service.CORS.ExposedHeaders, ","), - MaxAge: int(service.CORS.MaxAge.Seconds()), - })) - } + if service.CORS != nil { + app.Use(cors.New(cors.Config{ + AllowOrigins: strings.Join(service.CORS.AllowedOrigins, ","), + AllowMethods: strings.Join(service.CORS.AllowedMethods, ","), + AllowHeaders: strings.Join(service.CORS.AllowedHeaders, ","), + AllowCredentials: service.CORS.AllowCredentials, + ExposeHeaders: strings.Join(service.CORS.ExposedHeaders, ","), + MaxAge: int(service.CORS.MaxAge.Seconds()), + })) + } - app.Use(errormiddleware.New( - errormiddleware.WithVerboseErrors(service.Respond.Verbose), - errormiddleware.WithPreconditionErrorCode(service.Respond.With.ArgumentError.Code), - errormiddleware.WithAuthenticationErrorCode(service.Respond.With.AuthenticationError.Code), - errormiddleware.WithAuthorizationErrorCode(service.Respond.With.AuthorizationError.Code), - errormiddleware.WithCommunicationErrorCode(service.Respond.With.CommunicationError.Code), - errormiddleware.WithMethodErrorCode(service.Respond.With.BadMethodError.Code), - errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), - errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), - )) - app.Use(cachemiddleware.New(args.Cache)) + app.Use(errormiddleware.New( + errormiddleware.WithVerboseErrors(service.Respond.Verbose), + errormiddleware.WithPreconditionErrorCode(service.Respond.With.ArgumentError.Code), + errormiddleware.WithAuthenticationErrorCode(service.Respond.With.AuthenticationError.Code), + errormiddleware.WithAuthorizationErrorCode(service.Respond.With.AuthorizationError.Code), + errormiddleware.WithCommunicationErrorCode(service.Respond.With.CommunicationError.Code), + errormiddleware.WithMethodErrorCode(service.Respond.With.BadMethodError.Code), + errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), + errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), + )) + app.Use(cachemiddleware.New(args.Cache)) - return app + return app } diff --git a/internal/handler/envoyextauth/grpcv3/service.go b/internal/handler/envoyextauth/grpcv3/service.go index a4fbe7ccf..2750a5e79 100644 --- a/internal/handler/envoyextauth/grpcv3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -38,6 +38,7 @@ func newService( interceptors = append(interceptors, prometheusmiddleware.New( prometheusmiddleware.WithServiceName("decision"), + prometheusmiddleware.WithLabel("type", "envoy-grpc-extauth"), prometheusmiddleware.WithRegisterer(registrer), ), ) From e4fc676046a499aa95cf5ffec80215065cacb719 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Tue, 31 Jan 2023 17:41:25 +0100 Subject: [PATCH 08/67] type label changed --- internal/handler/envoyextauth/grpcv3/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/service.go b/internal/handler/envoyextauth/grpcv3/service.go index 2750a5e79..97a9902fe 100644 --- a/internal/handler/envoyextauth/grpcv3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -38,7 +38,7 @@ func newService( interceptors = append(interceptors, prometheusmiddleware.New( prometheusmiddleware.WithServiceName("decision"), - prometheusmiddleware.WithLabel("type", "envoy-grpc-extauth"), + prometheusmiddleware.WithLabel("type", "envoy-grpc-extauth-v3"), prometheusmiddleware.WithRegisterer(registrer), ), ) From 94f47b06bf725d12b20501485f5df0c176bbcb0f Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Wed, 1 Feb 2023 11:01:27 +0100 Subject: [PATCH 09/67] linter warnings resolved --- cmd/serve/decision.go | 78 +-- internal/accesscontext/access_context.go | 38 +- .../middleware/accesslog/accesslog_handler.go | 220 +++---- .../accesslog/accesslog_handler_test.go | 432 +++++++------- .../middleware/errorhandler/error_handler.go | 100 ++-- .../fiber/middleware/prometheus/handler.go | 164 +++--- internal/handler/decision/app.go | 142 ++--- .../handler/envoyextauth/grpcv3/handler.go | 50 +- .../middleware/accesslog/accesslog_handler.go | 214 +++---- .../grpcv3/middleware/cache/cache.go | 12 +- .../middleware/errorhandler/defaults.go | 36 +- .../middleware/errorhandler/error_handler.go | 126 ++-- .../grpcv3/middleware/errorhandler/options.go | 165 +++--- .../grpcv3/middleware/logger/handler.go | 40 +- .../grpcv3/middleware/prometheus/defaults.go | 8 +- .../grpcv3/middleware/prometheus/handler.go | 166 +++--- .../grpcv3/middleware/prometheus/options.go | 74 +-- .../middleware/prometheus/options_test.go | 556 +++++++++--------- .../handler/envoyextauth/grpcv3/module.go | 124 ++-- .../envoyextauth/grpcv3/request_context.go | 198 ++++--- .../handler/envoyextauth/grpcv3/service.go | 122 ++-- internal/rules/composite_subject_creator.go | 48 +- internal/x/errorchain/error_chain.go | 182 +++--- 23 files changed, 1660 insertions(+), 1635 deletions(-) diff --git a/cmd/serve/decision.go b/cmd/serve/decision.go index a67a33acd..59fda395d 100644 --- a/cmd/serve/decision.go +++ b/cmd/serve/decision.go @@ -17,58 +17,58 @@ package serve import ( - "github.com/spf13/cobra" - "go.uber.org/fx" + "github.com/spf13/cobra" + "go.uber.org/fx" - "github.com/dadrus/heimdall/internal" - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/handler/decision" - envoy_extauth "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3" + "github.com/dadrus/heimdall/internal" + "github.com/dadrus/heimdall/internal/config" + "github.com/dadrus/heimdall/internal/handler/decision" + envoy_extauth "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3" ) // NewDecisionCommand represents the "serve decision" command. func NewDecisionCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "decision", - Short: "Starts heimdall in Decision operation mode", - Example: "heimdall serve decision", - Run: func(cmd *cobra.Command, _ []string) { - app, err := createDecisionApp(cmd) - if err != nil { - cmd.PrintErrf("Failed to initialize decision service: %v", err) - panic(err) - } + cmd := &cobra.Command{ + Use: "decision", + Short: "Starts heimdall in Decision operation mode", + Example: "heimdall serve decision", + Run: func(cmd *cobra.Command, _ []string) { + app, err := createDecisionApp(cmd) + if err != nil { + cmd.PrintErrf("Failed to initialize decision service: %v", err) + panic(err) + } - app.Run() - }, - } + app.Run() + }, + } - cmd.PersistentFlags().Bool("envoy-extauth", false, - "Whether to start the decision mode for integration with envoy extauth gRPC service") + cmd.PersistentFlags().Bool("envoy-extauth", false, + "Whether to start the decision mode for integration with envoy extauth gRPC service") - return cmd + return cmd } func createDecisionApp(cmd *cobra.Command) (*fx.App, error) { - configPath, _ := cmd.Flags().GetString("config") - envPrefix, _ := cmd.Flags().GetString("env-config-prefix") - useEnvoyExtAuth, _ := cmd.Flags().GetBool("envoy-extauth") + configPath, _ := cmd.Flags().GetString("config") + envPrefix, _ := cmd.Flags().GetString("env-config-prefix") + useEnvoyExtAuth, _ := cmd.Flags().GetBool("envoy-extauth") - opts := []fx.Option{ - fx.NopLogger, - fx.Supply( - config.ConfigurationPath(configPath), - config.EnvVarPrefix(envPrefix)), - internal.Module, - } + opts := []fx.Option{ + fx.NopLogger, + fx.Supply( + config.ConfigurationPath(configPath), + config.EnvVarPrefix(envPrefix)), + internal.Module, + } - if useEnvoyExtAuth { - opts = append(opts, envoy_extauth.Module) - } else { - opts = append(opts, decision.Module) - } + if useEnvoyExtAuth { + opts = append(opts, envoy_extauth.Module) + } else { + opts = append(opts, decision.Module) + } - app := fx.New(opts...) + app := fx.New(opts...) - return app, app.Err() + return app, app.Err() } diff --git a/internal/accesscontext/access_context.go b/internal/accesscontext/access_context.go index e3fac5e89..cd3364b90 100644 --- a/internal/accesscontext/access_context.go +++ b/internal/accesscontext/access_context.go @@ -16,43 +16,45 @@ package accesscontext -import "context" +import ( + "context" +) type ctxKey struct{} type accessContext struct { - err error - subject string + err error + subject string } func New(ctx context.Context) context.Context { - return context.WithValue(ctx, ctxKey{}, &accessContext{}) + return context.WithValue(ctx, ctxKey{}, &accessContext{}) } func Error(ctx context.Context) error { - if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { - return c.err - } + if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { + return c.err + } - return nil + return nil } func SetError(ctx context.Context, err error) { - if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { - c.err = err - } + if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { + c.err = err + } } func Subject(ctx context.Context) string { - if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { - return c.subject - } + if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { + return c.subject + } - return "" + return "" } func SetSubject(ctx context.Context, subject string) { - if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { - c.subject = subject - } + if c, ok := ctx.Value(ctxKey{}).(*accessContext); ok { + c.subject = subject + } } diff --git a/internal/fiber/middleware/accesslog/accesslog_handler.go b/internal/fiber/middleware/accesslog/accesslog_handler.go index b37c22b9d..b37d98d42 100644 --- a/internal/fiber/middleware/accesslog/accesslog_handler.go +++ b/internal/fiber/middleware/accesslog/accesslog_handler.go @@ -17,131 +17,131 @@ package accesslog import ( - "time" + "time" - "github.com/gofiber/fiber/v2" - "github.com/rs/zerolog" + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog" - "github.com/dadrus/heimdall/internal/accesscontext" - "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" ) func New(logger zerolog.Logger) fiber.Handler { - return func(c *fiber.Ctx) error { - start := time.Now() - traceCtx := tracecontext.Extract(c.UserContext()) - c.SetUserContext(accesscontext.New(c.UserContext())) + return func(c *fiber.Ctx) error { + start := time.Now() + traceCtx := tracecontext.Extract(c.UserContext()) + c.SetUserContext(accesscontext.New(c.UserContext())) - accLog := createAccessLogger(c, logger, start, traceCtx) - accLog.Info().Msg("TX started") + accLog := createAccessLogger(c, logger, start, traceCtx) + accLog.Info().Msg("TX started") - err := c.Next() + err := c.Next() - createAccessLogFinalizationEvent(c, accLog, err, start, traceCtx).Msg("TX finished") + createAccessLogFinalizationEvent(c, accLog, err, start, traceCtx).Msg("TX finished") - return err - } + return err + } } func createAccessLogger( - c *fiber.Ctx, - logger zerolog.Logger, - start time.Time, - traceCtx *tracecontext.TraceContext, + c *fiber.Ctx, + logger zerolog.Logger, + start time.Time, + traceCtx *tracecontext.TraceContext, ) zerolog.Logger { - startTime := start.Unix() - - logCtx := logger.Level(zerolog.InfoLevel).With(). - Int64("_tx_start", startTime). - Str("_client_ip", c.IP()). - Str("_http_method", c.Method()). - Str("_http_path", c.Path()). - Str("_http_user_agent", c.Get("User-Agent")). - Str("_http_host", string(c.Request().URI().Host())). - Str("_http_scheme", string(c.Request().URI().Scheme())) - - if traceCtx != nil { - logCtx = logCtx. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) - - if len(traceCtx.ParentID) != 0 { - logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) - } - } - - if c.IsProxyTrusted() { // nolint: nestif - if headerValue := c.Get("X-Forwarded-Proto"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_proto", headerValue) - } - - if headerValue := c.Get("X-Forwarded-Host"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_host", headerValue) - } - - if headerValue := c.Get("X-Forwarded-Path"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_path", headerValue) - } - - if headerValue := c.Get("X-Forwarded-Uri"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_uri", headerValue) - } - - if headerValue := c.Get("X-Forwarded-For"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_for", headerValue) - } - - if headerValue := c.Get("Forwarded"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_forwarded", headerValue) - } - } - - return logCtx.Logger() + startTime := start.Unix() + + logCtx := logger.Level(zerolog.InfoLevel).With(). + Int64("_tx_start", startTime). + Str("_client_ip", c.IP()). + Str("_http_method", c.Method()). + Str("_http_path", c.Path()). + Str("_http_user_agent", c.Get("User-Agent")). + Str("_http_host", string(c.Request().URI().Host())). + Str("_http_scheme", string(c.Request().URI().Scheme())) + + if traceCtx != nil { + logCtx = logCtx. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) + } + } + + if c.IsProxyTrusted() { // nolint: nestif + if headerValue := c.Get("X-Forwarded-Proto"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_proto", headerValue) + } + + if headerValue := c.Get("X-Forwarded-Host"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_host", headerValue) + } + + if headerValue := c.Get("X-Forwarded-Path"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_path", headerValue) + } + + if headerValue := c.Get("X-Forwarded-Uri"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_uri", headerValue) + } + + if headerValue := c.Get("X-Forwarded-For"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_x_forwarded_for", headerValue) + } + + if headerValue := c.Get("Forwarded"); len(headerValue) != 0 { + logCtx = logCtx.Str("_http_forwarded", headerValue) + } + } + + return logCtx.Logger() } func createAccessLogFinalizationEvent( - c *fiber.Ctx, - accessLogger zerolog.Logger, - err error, - start time.Time, - traceCtx *tracecontext.TraceContext, + c *fiber.Ctx, + accessLogger zerolog.Logger, + err error, + start time.Time, + traceCtx *tracecontext.TraceContext, ) *zerolog.Event { - end := time.Now() - duration := end.Sub(start) - subject := accesscontext.Subject(c.UserContext()) - accessErr := accesscontext.Error(c.UserContext()) - - event := accessLogger.Info(). - Int("_body_bytes_sent", len(c.Response().Body())). - Int("_http_status_code", c.Response().StatusCode()). - Int64("_tx_duration_ms", duration.Milliseconds()) - - if traceCtx != nil { - event = event. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) - - if len(traceCtx.ParentID) != 0 { - event = event.Str("_parent_id", traceCtx.ParentID) - } - } - - switch { - case err != nil: - if len(subject) != 0 { - event = event.Str("_subject", subject) - } - - event = event.Err(err).Bool("_access_granted", false) - case accessErr != nil: - if len(subject) != 0 { - event = event.Str("_subject", subject) - } - - event = event.Err(accessErr).Bool("_access_granted", false) - default: - event = event.Str("_subject", subject).Bool("_access_granted", true) - } - - return event + end := time.Now() + duration := end.Sub(start) + subject := accesscontext.Subject(c.UserContext()) + accessErr := accesscontext.Error(c.UserContext()) + + event := accessLogger.Info(). + Int("_body_bytes_sent", len(c.Response().Body())). + Int("_http_status_code", c.Response().StatusCode()). + Int64("_tx_duration_ms", duration.Milliseconds()) + + if traceCtx != nil { + event = event. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + event = event.Str("_parent_id", traceCtx.ParentID) + } + } + + switch { + case err != nil: + if len(subject) != 0 { + event = event.Str("_subject", subject) + } + + event = event.Err(err).Bool("_access_granted", false) + case accessErr != nil: + if len(subject) != 0 { + event = event.Str("_subject", subject) + } + + event = event.Err(accessErr).Bool("_access_granted", false) + default: + event = event.Str("_subject", subject).Bool("_access_granted", true) + } + + return event } diff --git a/internal/fiber/middleware/accesslog/accesslog_handler_test.go b/internal/fiber/middleware/accesslog/accesslog_handler_test.go index 3fee9f343..3ac9d9920 100644 --- a/internal/fiber/middleware/accesslog/accesslog_handler_test.go +++ b/internal/fiber/middleware/accesslog/accesslog_handler_test.go @@ -17,249 +17,249 @@ package accesslog import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" - "github.com/goccy/go-json" - "github.com/gofiber/fiber/v2" - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" + "github.com/goccy/go-json" + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" - "github.com/dadrus/heimdall/internal/accesscontext" - tracingmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/opentelemetry" - "github.com/dadrus/heimdall/internal/x/testsupport" + "github.com/dadrus/heimdall/internal/accesscontext" + tracingmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/opentelemetry" + "github.com/dadrus/heimdall/internal/x/testsupport" ) func TestLoggerHandler(t *testing.T) { - // GIVEN - otel.SetTracerProvider(sdktrace.NewTracerProvider()) - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) + // GIVEN + otel.SetTracerProvider(sdktrace.NewTracerProvider()) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) - parentCtx := trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: trace.TraceID{1}, SpanID: trace.SpanID{2}, TraceFlags: trace.FlagsSampled, - }) + parentCtx := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{1}, SpanID: trace.SpanID{2}, TraceFlags: trace.FlagsSampled, + }) - for _, tc := range []struct { - uc string - setHeader func(t *testing.T, req *http.Request) - configureHandler func(t *testing.T, ctx *fiber.Ctx) error - assert func(t *testing.T, logEvent1, logEvent2 map[string]any) - }{ - { - uc: "without tracing, x-* header and errors", - setHeader: func(t *testing.T, req *http.Request) { t.Helper() }, - configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { - t.Helper() + for _, tc := range []struct { + uc string + setHeader func(t *testing.T, req *http.Request) + configureHandler func(t *testing.T, ctx *fiber.Ctx) error + assert func(t *testing.T, logEvent1, logEvent2 map[string]any) + }{ + { + uc: "without tracing, x-* header and errors", + setHeader: func(t *testing.T, req *http.Request) { t.Helper() }, + configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { + t.Helper() - accesscontext.SetSubject(ctx.UserContext(), "foo") + accesscontext.SetSubject(ctx.UserContext(), "foo") - return nil - }, - assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { - t.Helper() + return nil + }, + assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { + t.Helper() - require.Len(t, logEvent1, 11) - assert.Equal(t, "info", logEvent1["level"]) - assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_client_ip") - assert.Contains(t, logEvent1, "_http_user_agent") - assert.Equal(t, "GET", logEvent1["_http_method"]) - assert.Equal(t, "example.com", logEvent1["_http_host"]) - assert.Equal(t, "/test", logEvent1["_http_path"]) - assert.Equal(t, "http", logEvent1["_http_scheme"]) - assert.Contains(t, logEvent1, "_trace_id") - assert.Contains(t, logEvent1, "_trace_id") - assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) - assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) - assert.Equal(t, "TX started", logEvent1["message"]) + require.Len(t, logEvent1, 11) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_client_ip") + assert.Contains(t, logEvent1, "_http_user_agent") + assert.Equal(t, "GET", logEvent1["_http_method"]) + assert.Equal(t, "example.com", logEvent1["_http_host"]) + assert.Equal(t, "/test", logEvent1["_http_path"]) + assert.Equal(t, "http", logEvent1["_http_scheme"]) + assert.Contains(t, logEvent1, "_trace_id") + assert.Contains(t, logEvent1, "_trace_id") + assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) + assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) + assert.Equal(t, "TX started", logEvent1["message"]) - require.Len(t, logEvent2, 16) - assert.Equal(t, "info", logEvent2["level"]) - assert.Contains(t, logEvent2, "_tx_start") - assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_client_ip") - assert.Equal(t, "GET", logEvent2["_http_method"]) - assert.Equal(t, "example.com", logEvent2["_http_host"]) - assert.Equal(t, "/test", logEvent2["_http_path"]) - assert.Equal(t, "http", logEvent2["_http_scheme"]) - assert.Contains(t, logEvent2, "_trace_id") - assert.Contains(t, logEvent2, "_trace_id") - assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) - assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) - assert.Contains(t, logEvent2, "_body_bytes_sent") - assert.Equal(t, float64(200), logEvent2["_http_status_code"]) - assert.Equal(t, true, logEvent2["_access_granted"]) - assert.Equal(t, "foo", logEvent2["_subject"]) - assert.Contains(t, logEvent2, "_http_user_agent") - assert.Equal(t, "TX finished", logEvent2["message"]) - }, - }, - { - uc: "with tracing, x-* header and error", - setHeader: func(t *testing.T, req *http.Request) { - t.Helper() + require.Len(t, logEvent2, 16) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, "GET", logEvent2["_http_method"]) + assert.Equal(t, "example.com", logEvent2["_http_host"]) + assert.Equal(t, "/test", logEvent2["_http_path"]) + assert.Equal(t, "http", logEvent2["_http_scheme"]) + assert.Contains(t, logEvent2, "_trace_id") + assert.Contains(t, logEvent2, "_trace_id") + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Contains(t, logEvent2, "_body_bytes_sent") + assert.Equal(t, float64(200), logEvent2["_http_status_code"]) + assert.Equal(t, true, logEvent2["_access_granted"]) + assert.Equal(t, "foo", logEvent2["_subject"]) + assert.Contains(t, logEvent2, "_http_user_agent") + assert.Equal(t, "TX finished", logEvent2["message"]) + }, + }, + { + uc: "with tracing, x-* header and error", + setHeader: func(t *testing.T, req *http.Request) { + t.Helper() - // nolint: contextcheck - otel.GetTextMapPropagator().Inject( - trace.ContextWithRemoteSpanContext(context.Background(), parentCtx), - propagation.HeaderCarrier(req.Header)) + // nolint: contextcheck + otel.GetTextMapPropagator().Inject( + trace.ContextWithRemoteSpanContext(context.Background(), parentCtx), + propagation.HeaderCarrier(req.Header)) - req.Header.Set("X-Forwarded-Proto", "https") - req.Header.Set("X-Forwarded-Host", "foobar.com") - req.Header.Set("X-Forwarded-Path", "/bar") - req.Header.Set("X-Forwarded-Uri", "https://foobar.com/bar") - req.Header.Set("X-Forwarded-For", "127.0.0.1") - req.Header.Set("Forwarded", "for=127.0.0.1") - }, - configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { - t.Helper() + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-Host", "foobar.com") + req.Header.Set("X-Forwarded-Path", "/bar") + req.Header.Set("X-Forwarded-Uri", "https://foobar.com/bar") + req.Header.Set("X-Forwarded-For", "127.0.0.1") + req.Header.Set("Forwarded", "for=127.0.0.1") + }, + configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { + t.Helper() - return fmt.Errorf("test error") // nolint: goerr113 - }, - assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { - t.Helper() + return fmt.Errorf("test error") // nolint: goerr113 + }, + assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { + t.Helper() - require.Len(t, logEvent1, 18) - assert.Equal(t, "info", logEvent1["level"]) - assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_client_ip") - assert.Contains(t, logEvent1, "_http_user_agent") - assert.Equal(t, "GET", logEvent1["_http_method"]) - assert.Equal(t, "example.com", logEvent1["_http_host"]) - assert.Equal(t, "/test", logEvent1["_http_path"]) - assert.Equal(t, "http", logEvent1["_http_scheme"]) - assert.Contains(t, logEvent1, "_span_id") - assert.Equal(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) - assert.Equal(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) - assert.Equal(t, "TX started", logEvent1["message"]) - assert.Equal(t, "https", logEvent1["_http_x_forwarded_proto"]) - assert.Equal(t, "foobar.com", logEvent1["_http_x_forwarded_host"]) - assert.Equal(t, "/bar", logEvent1["_http_x_forwarded_path"]) - assert.Equal(t, "https://foobar.com/bar", logEvent1["_http_x_forwarded_uri"]) - assert.Equal(t, "127.0.0.1", logEvent1["_http_x_forwarded_for"]) - assert.Equal(t, "for=127.0.0.1", logEvent1["_http_forwarded"]) + require.Len(t, logEvent1, 18) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_client_ip") + assert.Contains(t, logEvent1, "_http_user_agent") + assert.Equal(t, "GET", logEvent1["_http_method"]) + assert.Equal(t, "example.com", logEvent1["_http_host"]) + assert.Equal(t, "/test", logEvent1["_http_path"]) + assert.Equal(t, "http", logEvent1["_http_scheme"]) + assert.Contains(t, logEvent1, "_span_id") + assert.Equal(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) + assert.Equal(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) + assert.Equal(t, "TX started", logEvent1["message"]) + assert.Equal(t, "https", logEvent1["_http_x_forwarded_proto"]) + assert.Equal(t, "foobar.com", logEvent1["_http_x_forwarded_host"]) + assert.Equal(t, "/bar", logEvent1["_http_x_forwarded_path"]) + assert.Equal(t, "https://foobar.com/bar", logEvent1["_http_x_forwarded_uri"]) + assert.Equal(t, "127.0.0.1", logEvent1["_http_x_forwarded_for"]) + assert.Equal(t, "for=127.0.0.1", logEvent1["_http_forwarded"]) - require.Len(t, logEvent2, 23) - assert.Equal(t, "info", logEvent2["level"]) - assert.Contains(t, logEvent2, "_tx_start") - assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_client_ip") - assert.Equal(t, "GET", logEvent2["_http_method"]) - assert.Equal(t, "example.com", logEvent2["_http_host"]) - assert.Equal(t, "/test", logEvent2["_http_path"]) - assert.Equal(t, "http", logEvent2["_http_scheme"]) - assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) - assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) - assert.Equal(t, logEvent2["_span_id"], logEvent2["_span_id"]) - assert.Contains(t, logEvent2, "_body_bytes_sent") - assert.Equal(t, float64(200), logEvent2["_http_status_code"]) - assert.Equal(t, false, logEvent2["_access_granted"]) - assert.Equal(t, "test error", logEvent2["error"]) - assert.Contains(t, logEvent2, "_http_user_agent") - assert.Equal(t, "TX finished", logEvent2["message"]) - assert.Equal(t, "https", logEvent1["_http_x_forwarded_proto"]) - assert.Equal(t, "foobar.com", logEvent1["_http_x_forwarded_host"]) - assert.Equal(t, "/bar", logEvent1["_http_x_forwarded_path"]) - assert.Equal(t, "https://foobar.com/bar", logEvent1["_http_x_forwarded_uri"]) - assert.Equal(t, "127.0.0.1", logEvent1["_http_x_forwarded_for"]) - assert.Equal(t, "for=127.0.0.1", logEvent1["_http_forwarded"]) - }, - }, - { - uc: "without tracing and x-* header, but with subject and error set on context", - setHeader: func(t *testing.T, req *http.Request) { t.Helper() }, - configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { - t.Helper() + require.Len(t, logEvent2, 23) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, "GET", logEvent2["_http_method"]) + assert.Equal(t, "example.com", logEvent2["_http_host"]) + assert.Equal(t, "/test", logEvent2["_http_path"]) + assert.Equal(t, "http", logEvent2["_http_scheme"]) + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Equal(t, logEvent2["_span_id"], logEvent2["_span_id"]) + assert.Contains(t, logEvent2, "_body_bytes_sent") + assert.Equal(t, float64(200), logEvent2["_http_status_code"]) + assert.Equal(t, false, logEvent2["_access_granted"]) + assert.Equal(t, "test error", logEvent2["error"]) + assert.Contains(t, logEvent2, "_http_user_agent") + assert.Equal(t, "TX finished", logEvent2["message"]) + assert.Equal(t, "https", logEvent1["_http_x_forwarded_proto"]) + assert.Equal(t, "foobar.com", logEvent1["_http_x_forwarded_host"]) + assert.Equal(t, "/bar", logEvent1["_http_x_forwarded_path"]) + assert.Equal(t, "https://foobar.com/bar", logEvent1["_http_x_forwarded_uri"]) + assert.Equal(t, "127.0.0.1", logEvent1["_http_x_forwarded_for"]) + assert.Equal(t, "for=127.0.0.1", logEvent1["_http_forwarded"]) + }, + }, + { + uc: "without tracing and x-* header, but with subject and error set on context", + setHeader: func(t *testing.T, req *http.Request) { t.Helper() }, + configureHandler: func(t *testing.T, ctx *fiber.Ctx) error { + t.Helper() - accesscontext.SetSubject(ctx.UserContext(), "bar") - accesscontext.SetError(ctx.UserContext(), fmt.Errorf("test error")) // nolint: goerr113 + accesscontext.SetSubject(ctx.UserContext(), "bar") + accesscontext.SetError(ctx.UserContext(), fmt.Errorf("test error")) // nolint: goerr113 - return nil - }, - assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { - t.Helper() + return nil + }, + assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { + t.Helper() - require.Len(t, logEvent1, 11) - assert.Equal(t, "info", logEvent1["level"]) - assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_client_ip") - assert.Contains(t, logEvent1, "_http_user_agent") - assert.Equal(t, "GET", logEvent1["_http_method"]) - assert.Equal(t, "example.com", logEvent1["_http_host"]) - assert.Equal(t, "/test", logEvent1["_http_path"]) - assert.Equal(t, "http", logEvent1["_http_scheme"]) - assert.Contains(t, logEvent1, "_trace_id") - assert.Contains(t, logEvent1, "_trace_id") - assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) - assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) - assert.Equal(t, "TX started", logEvent1["message"]) + require.Len(t, logEvent1, 11) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_client_ip") + assert.Contains(t, logEvent1, "_http_user_agent") + assert.Equal(t, "GET", logEvent1["_http_method"]) + assert.Equal(t, "example.com", logEvent1["_http_host"]) + assert.Equal(t, "/test", logEvent1["_http_path"]) + assert.Equal(t, "http", logEvent1["_http_scheme"]) + assert.Contains(t, logEvent1, "_trace_id") + assert.Contains(t, logEvent1, "_trace_id") + assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) + assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) + assert.Equal(t, "TX started", logEvent1["message"]) - require.Len(t, logEvent2, 17) - assert.Equal(t, "info", logEvent2["level"]) - assert.Contains(t, logEvent2, "_tx_start") - assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_client_ip") - assert.Equal(t, "GET", logEvent2["_http_method"]) - assert.Equal(t, "example.com", logEvent2["_http_host"]) - assert.Equal(t, "/test", logEvent2["_http_path"]) - assert.Equal(t, "http", logEvent2["_http_scheme"]) - assert.Contains(t, logEvent2, "_trace_id") - assert.Contains(t, logEvent2, "_trace_id") - assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) - assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) - assert.Contains(t, logEvent2, "_body_bytes_sent") - assert.Equal(t, float64(200), logEvent2["_http_status_code"]) - assert.Equal(t, false, logEvent2["_access_granted"]) - assert.Equal(t, "bar", logEvent2["_subject"]) - assert.Equal(t, "test error", logEvent2["error"]) - assert.Contains(t, logEvent2, "_http_user_agent") - assert.Equal(t, "TX finished", logEvent2["message"]) - }, - }, - } { - t.Run(tc.uc, func(t *testing.T) { - // GIVEN - tb := &testsupport.TestingLog{TB: t} - logger := zerolog.New(zerolog.TestWriter{T: tb}) + require.Len(t, logEvent2, 17) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, "GET", logEvent2["_http_method"]) + assert.Equal(t, "example.com", logEvent2["_http_host"]) + assert.Equal(t, "/test", logEvent2["_http_path"]) + assert.Equal(t, "http", logEvent2["_http_scheme"]) + assert.Contains(t, logEvent2, "_trace_id") + assert.Contains(t, logEvent2, "_trace_id") + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Contains(t, logEvent2, "_body_bytes_sent") + assert.Equal(t, float64(200), logEvent2["_http_status_code"]) + assert.Equal(t, false, logEvent2["_access_granted"]) + assert.Equal(t, "bar", logEvent2["_subject"]) + assert.Equal(t, "test error", logEvent2["error"]) + assert.Contains(t, logEvent2, "_http_user_agent") + assert.Equal(t, "TX finished", logEvent2["message"]) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + tb := &testsupport.TestingLog{TB: t} + logger := zerolog.New(zerolog.TestWriter{T: tb}) - app := fiber.New() - app.Use(tracingmiddleware.New()) - app.Use(New(logger)) - app.Get("/test", func(ctx *fiber.Ctx) error { return tc.configureHandler(t, ctx) }) + app := fiber.New() + app.Use(tracingmiddleware.New()) + app.Use(New(logger)) + app.Get("/test", func(ctx *fiber.Ctx) error { return tc.configureHandler(t, ctx) }) - req := httptest.NewRequest(http.MethodGet, "/test", nil) + req := httptest.NewRequest(http.MethodGet, "/test", nil) - tc.setHeader(t, req) + tc.setHeader(t, req) - // WHEN - resp, err := app.Test(req, 1000000) - require.NoError(t, app.Shutdown()) + // WHEN + resp, err := app.Test(req, 1000000) + require.NoError(t, app.Shutdown()) - // THEN - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) + // THEN + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) - events := strings.Split(tb.CollectedLog(), "}") - require.Len(t, events, 3) + events := strings.Split(tb.CollectedLog(), "}") + require.Len(t, events, 3) - var ( - logLine1 map[string]any - logLine2 map[string]any - ) + var ( + logLine1 map[string]any + logLine2 map[string]any + ) - require.NoError(t, json.Unmarshal([]byte(events[0]+"}"), &logLine1)) - require.NoError(t, json.Unmarshal([]byte(events[1]+"}"), &logLine2)) + require.NoError(t, json.Unmarshal([]byte(events[0]+"}"), &logLine1)) + require.NoError(t, json.Unmarshal([]byte(events[1]+"}"), &logLine2)) - tc.assert(t, logLine1, logLine2) - }) - } + tc.assert(t, logLine1, logLine2) + }) + } } diff --git a/internal/fiber/middleware/errorhandler/error_handler.go b/internal/fiber/middleware/errorhandler/error_handler.go index 4ab1427f2..c35989ba2 100644 --- a/internal/fiber/middleware/errorhandler/error_handler.go +++ b/internal/fiber/middleware/errorhandler/error_handler.go @@ -17,68 +17,68 @@ package errorhandler import ( - "errors" + "errors" - "github.com/gofiber/fiber/v2" - "github.com/rs/zerolog" + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog" - "github.com/dadrus/heimdall/internal/accesscontext" - "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/heimdall" ) func New(opts ...Option) fiber.Handler { - options := defaultOptions + options := defaultOptions - for _, opt := range opts { - opt(&options) - } + for _, opt := range opts { + opt(&options) + } - h := &handler{opts: options} + h := &handler{opts: options} - return h.handle + return h.handle } type handler struct { - opts + opts } func (h *handler) handle(ctx *fiber.Ctx) error { //nolint:cyclop - err := ctx.Next() - if err == nil { - return nil - } - - accesscontext.SetError(ctx.UserContext(), err) - - switch { - case errors.Is(err, heimdall.ErrAuthentication): - h.onAuthenticationError(ctx) - case errors.Is(err, heimdall.ErrAuthorization): - h.onAuthorizationError(ctx) - case errors.Is(err, heimdall.ErrCommunicationTimeout) || errors.Is(err, heimdall.ErrCommunication): - h.onCommunicationError(ctx) - case errors.Is(err, heimdall.ErrArgument): - h.onPreconditionError(ctx) - case errors.Is(err, heimdall.ErrMethodNotAllowed): - h.onBadMethodError(ctx) - case errors.Is(err, heimdall.ErrNoRuleFound): - h.onNoRuleError(ctx) - case errors.Is(err, &heimdall.RedirectError{}): - var redirectError *heimdall.RedirectError - - errors.As(err, &redirectError) - - return ctx.Redirect(redirectError.RedirectTo, redirectError.Code) - default: - logger := zerolog.Ctx(ctx.UserContext()) - logger.Error().Err(err).Msg("Internal error occurred") - - h.onInternalError(ctx) - } - - if h.verboseErrors { - return ctx.Format(err) - } - - return nil + err := ctx.Next() + if err == nil { + return nil + } + + accesscontext.SetError(ctx.UserContext(), err) + + switch { + case errors.Is(err, heimdall.ErrAuthentication): + h.onAuthenticationError(ctx) + case errors.Is(err, heimdall.ErrAuthorization): + h.onAuthorizationError(ctx) + case errors.Is(err, heimdall.ErrCommunicationTimeout) || errors.Is(err, heimdall.ErrCommunication): + h.onCommunicationError(ctx) + case errors.Is(err, heimdall.ErrArgument): + h.onPreconditionError(ctx) + case errors.Is(err, heimdall.ErrMethodNotAllowed): + h.onBadMethodError(ctx) + case errors.Is(err, heimdall.ErrNoRuleFound): + h.onNoRuleError(ctx) + case errors.Is(err, &heimdall.RedirectError{}): + var redirectError *heimdall.RedirectError + + errors.As(err, &redirectError) + + return ctx.Redirect(redirectError.RedirectTo, redirectError.Code) + default: + logger := zerolog.Ctx(ctx.UserContext()) + logger.Error().Err(err).Msg("Internal error occurred") + + h.onInternalError(ctx) + } + + if h.verboseErrors { + return ctx.Format(err) + } + + return nil } diff --git a/internal/fiber/middleware/prometheus/handler.go b/internal/fiber/middleware/prometheus/handler.go index 58106a284..9787b3f77 100644 --- a/internal/fiber/middleware/prometheus/handler.go +++ b/internal/fiber/middleware/prometheus/handler.go @@ -17,107 +17,107 @@ package prometheus import ( - "errors" - "strconv" - "time" + "errors" + "strconv" + "time" - "github.com/gofiber/fiber/v2" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/gofiber/fiber/v2" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" ) type metricsHandler struct { - reqCounter *prometheus.CounterVec - reqHistogram *prometheus.HistogramVec - reqInFlight *prometheus.GaugeVec - filterOperation OperationFilter + reqCounter *prometheus.CounterVec + reqHistogram *prometheus.HistogramVec + reqInFlight *prometheus.GaugeVec + filterOperation OperationFilter } func New(opts ...Option) fiber.Handler { - options := defaultOptions - - for _, opt := range opts { - opt(&options) - } - - counter := promauto.With(options.registerer).NewCounterVec( - prometheus.CounterOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), - Help: "Count all requests by status code, method and path.", - ConstLabels: options.labels, - }, - []string{"status_code", "method", "path"}, - ) - - histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), - Help: "Duration of all requests by status code, method and path.", - ConstLabels: options.labels, - Buckets: []float64{ - 0.00001, 0.00005, // 10, 50µs - 0.0001, 0.00025, 0.0005, 0.00075, // 100, 250, 500, 750µs - 0.001, 0.0025, 0.005, 0.0075, // 1, 2.5, 5, 7.5ms - 0.01, 0.025, 0.05, 0.075, // 10, 25, 50, 75ms - 0.1, 0.25, 0.5, // 100, 250, 500 ms - 1.0, 2.0, 5.0, 10.0, 15.0, // 1, 2, 5, 10, 20s - }, - }, - []string{"status_code", "method", "path"}, - ) - - gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), - Help: "All the requests in progress", - ConstLabels: options.labels, - }, []string{"method"}) - - handler := &metricsHandler{ - reqCounter: counter, - reqHistogram: histogram, - reqInFlight: gauge, - filterOperation: options.filterOperation, - } - - return handler.observeRequest + options := defaultOptions + + for _, opt := range opts { + opt(&options) + } + + counter := promauto.With(options.registerer).NewCounterVec( + prometheus.CounterOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), + Help: "Count all requests by status code, method and path.", + ConstLabels: options.labels, + }, + []string{"status_code", "method", "path"}, + ) + + histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), + Help: "Duration of all requests by status code, method and path.", + ConstLabels: options.labels, + Buckets: []float64{ + 0.00001, 0.00005, // 10, 50µs + 0.0001, 0.00025, 0.0005, 0.00075, // 100, 250, 500, 750µs + 0.001, 0.0025, 0.005, 0.0075, // 1, 2.5, 5, 7.5ms + 0.01, 0.025, 0.05, 0.075, // 10, 25, 50, 75ms + 0.1, 0.25, 0.5, // 100, 250, 500 ms + 1.0, 2.0, 5.0, 10.0, 15.0, // 1, 2, 5, 10, 20s + }, + }, + []string{"status_code", "method", "path"}, + ) + + gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), + Help: "All the requests in progress", + ConstLabels: options.labels, + }, []string{"method"}) + + handler := &metricsHandler{ + reqCounter: counter, + reqHistogram: histogram, + reqInFlight: gauge, + filterOperation: options.filterOperation, + } + + return handler.observeRequest } func (h *metricsHandler) observeRequest(ctx *fiber.Ctx) error { - const MagicNumber = 1e9 + const MagicNumber = 1e9 - start := time.Now() + start := time.Now() - if h.filterOperation(ctx) { - return ctx.Next() - } + if h.filterOperation(ctx) { + return ctx.Next() + } - method := ctx.Route().Method + method := ctx.Route().Method - h.reqInFlight.WithLabelValues(method).Inc() + h.reqInFlight.WithLabelValues(method).Inc() - defer func() { - h.reqInFlight.WithLabelValues(method).Dec() - }() + defer func() { + h.reqInFlight.WithLabelValues(method).Dec() + }() - err := ctx.Next() - // initialize with default error code - status := fiber.StatusInternalServerError + err := ctx.Next() + // initialize with default error code + status := fiber.StatusInternalServerError - if err != nil { - var ferr *fiber.Error + if err != nil { + var ferr *fiber.Error - if errors.As(err, &ferr) { - status = ferr.Code - } - } else { - status = ctx.Response().StatusCode() - } + if errors.As(err, &ferr) { + status = ferr.Code + } + } else { + status = ctx.Response().StatusCode() + } - path := ctx.Route().Path - statusCode := strconv.Itoa(status) - h.reqCounter.WithLabelValues(statusCode, method, path).Inc() + path := ctx.Route().Path + statusCode := strconv.Itoa(status) + h.reqCounter.WithLabelValues(statusCode, method, path).Inc() - elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber - h.reqHistogram.WithLabelValues(statusCode, method, path).Observe(elapsed) + elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber + h.reqHistogram.WithLabelValues(statusCode, method, path).Observe(elapsed) - return err + return err } diff --git a/internal/handler/decision/app.go b/internal/handler/decision/app.go index a56a81d28..75ab84199 100644 --- a/internal/handler/decision/app.go +++ b/internal/handler/decision/app.go @@ -17,91 +17,91 @@ package decision import ( - "strings" + "strings" - "github.com/goccy/go-json" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/recover" - "github.com/prometheus/client_golang/prometheus" - "github.com/rs/zerolog" - "go.opentelemetry.io/otel" - "go.uber.org/fx" + "github.com/goccy/go-json" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel" + "go.uber.org/fx" - "github.com/dadrus/heimdall/internal/cache" - "github.com/dadrus/heimdall/internal/config" - accesslogmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/accesslog" - cachemiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/cache" - errormiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/errorhandler" - loggermiddlerware "github.com/dadrus/heimdall/internal/fiber/middleware/logger" - tracingmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/opentelemetry" - prometheusmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/prometheus" - "github.com/dadrus/heimdall/internal/x" + "github.com/dadrus/heimdall/internal/cache" + "github.com/dadrus/heimdall/internal/config" + accesslogmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/accesslog" + cachemiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/cache" + errormiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/errorhandler" + loggermiddlerware "github.com/dadrus/heimdall/internal/fiber/middleware/logger" + tracingmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/opentelemetry" + prometheusmiddleware "github.com/dadrus/heimdall/internal/fiber/middleware/prometheus" + "github.com/dadrus/heimdall/internal/x" ) type appArgs struct { - fx.In + fx.In - Config *config.Configuration - Registerer prometheus.Registerer - Cache cache.Cache - Logger zerolog.Logger + Config *config.Configuration + Registerer prometheus.Registerer + Cache cache.Cache + Logger zerolog.Logger } func newApp(args appArgs) *fiber.App { - service := args.Config.Serve.Decision + service := args.Config.Serve.Decision - app := fiber.New(fiber.Config{ - AppName: "Heimdall Decision Service", - ReadTimeout: service.Timeout.Read, - WriteTimeout: service.Timeout.Write, - IdleTimeout: service.Timeout.Idle, - DisableStartupMessage: true, - EnableTrustedProxyCheck: true, - TrustedProxies: x.IfThenElseExec(service.TrustedProxies != nil, - func() []string { return *service.TrustedProxies }, - func() []string { return []string{} }), - JSONDecoder: json.Unmarshal, - JSONEncoder: json.Marshal, - }) + app := fiber.New(fiber.Config{ + AppName: "Heimdall Decision Service", + ReadTimeout: service.Timeout.Read, + WriteTimeout: service.Timeout.Write, + IdleTimeout: service.Timeout.Idle, + DisableStartupMessage: true, + EnableTrustedProxyCheck: true, + TrustedProxies: x.IfThenElseExec(service.TrustedProxies != nil, + func() []string { return *service.TrustedProxies }, + func() []string { return []string{} }), + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, + }) - app.Use(recover.New(recover.Config{EnableStackTrace: true})) - app.Use(tracingmiddleware.New( - tracingmiddleware.WithTracer(otel.GetTracerProvider().Tracer("github.com/dadrus/heimdall/decision")))) + app.Use(recover.New(recover.Config{EnableStackTrace: true})) + app.Use(tracingmiddleware.New( + tracingmiddleware.WithTracer(otel.GetTracerProvider().Tracer("github.com/dadrus/heimdall/decision")))) - if args.Config.Metrics.Enabled { - app.Use(prometheusmiddleware.New( - prometheusmiddleware.WithServiceName("decision"), - prometheusmiddleware.WithLabel("type", "http"), - prometheusmiddleware.WithRegisterer(args.Registerer), - )) - } + if args.Config.Metrics.Enabled { + app.Use(prometheusmiddleware.New( + prometheusmiddleware.WithServiceName("decision"), + prometheusmiddleware.WithLabel("type", "http"), + prometheusmiddleware.WithRegisterer(args.Registerer), + )) + } - app.Use(accesslogmiddleware.New(args.Logger)) - app.Use(loggermiddlerware.New(args.Logger)) + app.Use(accesslogmiddleware.New(args.Logger)) + app.Use(loggermiddlerware.New(args.Logger)) - if service.CORS != nil { - app.Use(cors.New(cors.Config{ - AllowOrigins: strings.Join(service.CORS.AllowedOrigins, ","), - AllowMethods: strings.Join(service.CORS.AllowedMethods, ","), - AllowHeaders: strings.Join(service.CORS.AllowedHeaders, ","), - AllowCredentials: service.CORS.AllowCredentials, - ExposeHeaders: strings.Join(service.CORS.ExposedHeaders, ","), - MaxAge: int(service.CORS.MaxAge.Seconds()), - })) - } + if service.CORS != nil { + app.Use(cors.New(cors.Config{ + AllowOrigins: strings.Join(service.CORS.AllowedOrigins, ","), + AllowMethods: strings.Join(service.CORS.AllowedMethods, ","), + AllowHeaders: strings.Join(service.CORS.AllowedHeaders, ","), + AllowCredentials: service.CORS.AllowCredentials, + ExposeHeaders: strings.Join(service.CORS.ExposedHeaders, ","), + MaxAge: int(service.CORS.MaxAge.Seconds()), + })) + } - app.Use(errormiddleware.New( - errormiddleware.WithVerboseErrors(service.Respond.Verbose), - errormiddleware.WithPreconditionErrorCode(service.Respond.With.ArgumentError.Code), - errormiddleware.WithAuthenticationErrorCode(service.Respond.With.AuthenticationError.Code), - errormiddleware.WithAuthorizationErrorCode(service.Respond.With.AuthorizationError.Code), - errormiddleware.WithCommunicationErrorCode(service.Respond.With.CommunicationError.Code), - errormiddleware.WithMethodErrorCode(service.Respond.With.BadMethodError.Code), - errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), - errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), - )) - app.Use(cachemiddleware.New(args.Cache)) + app.Use(errormiddleware.New( + errormiddleware.WithVerboseErrors(service.Respond.Verbose), + errormiddleware.WithPreconditionErrorCode(service.Respond.With.ArgumentError.Code), + errormiddleware.WithAuthenticationErrorCode(service.Respond.With.AuthenticationError.Code), + errormiddleware.WithAuthorizationErrorCode(service.Respond.With.AuthorizationError.Code), + errormiddleware.WithCommunicationErrorCode(service.Respond.With.CommunicationError.Code), + errormiddleware.WithMethodErrorCode(service.Respond.With.BadMethodError.Code), + errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), + errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), + )) + app.Use(cachemiddleware.New(args.Cache)) - return app + return app } diff --git a/internal/handler/envoyextauth/grpcv3/handler.go b/internal/handler/envoyextauth/grpcv3/handler.go index be9bc3c40..357cf953e 100644 --- a/internal/handler/envoyextauth/grpcv3/handler.go +++ b/internal/handler/envoyextauth/grpcv3/handler.go @@ -17,43 +17,43 @@ package grpcv3 import ( - "context" + "context" - envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/rs/zerolog" + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/rs/zerolog" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules" - "github.com/dadrus/heimdall/internal/x/errorchain" + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules" + "github.com/dadrus/heimdall/internal/x/errorchain" ) type Handler struct { - r rules.Repository - s heimdall.JWTSigner + r rules.Repository + s heimdall.JWTSigner } func (h *Handler) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*envoy_auth.CheckResponse, error) { - logger := zerolog.Ctx(ctx) - logger.Debug().Msg("Decision Envoy ExtAuth endpoint called") + logger := zerolog.Ctx(ctx) + logger.Debug().Msg("Decision Envoy ExtAuth endpoint called") - reqCtx := NewRequestContext(ctx, req, h.s) + reqCtx := NewRequestContext(ctx, req, h.s) - rule, err := h.r.FindRule(reqCtx.RequestURL()) - if err != nil { - return nil, err - } + rule, err := h.r.FindRule(reqCtx.RequestURL()) + if err != nil { + return nil, err + } - if !rule.MatchesMethod(reqCtx.RequestMethod()) { - return nil, errorchain.NewWithMessagef(heimdall.ErrMethodNotAllowed, - "rule doesn't match %s method", reqCtx.RequestMethod()) - } + if !rule.MatchesMethod(reqCtx.RequestMethod()) { + return nil, errorchain.NewWithMessagef(heimdall.ErrMethodNotAllowed, + "rule doesn't match %s method", reqCtx.RequestMethod()) + } - _, err = rule.Execute(reqCtx) - if err != nil { - return nil, err - } + _, err = rule.Execute(reqCtx) + if err != nil { + return nil, err + } - logger.Debug().Msg("Finalizing request") + logger.Debug().Msg("Finalizing request") - return reqCtx.Finalize() + return reqCtx.Finalize() } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go index b68283dea..b6f457c72 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go @@ -17,127 +17,127 @@ package accesslog import ( - "context" - "time" + "context" + "time" - "github.com/rs/zerolog" - "google.golang.org/grpc" + "github.com/rs/zerolog" + "google.golang.org/grpc" - "github.com/dadrus/heimdall/internal/accesscontext" - "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" ) func New(logger zerolog.Logger) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - start := time.Now() - traceCtx := tracecontext.Extract(ctx) - ctx = accesscontext.New(ctx) + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + start := time.Now() + traceCtx := tracecontext.Extract(ctx) + ctx = accesscontext.New(ctx) - accLog := createAccessLogger(logger, start, traceCtx) - accLog.Info().Msg("TX started") + accLog := createAccessLogger(logger, start, traceCtx) + accLog.Info().Msg("TX started") - res, err := handler(ctx, req) + res, err := handler(ctx, req) - createAccessLogFinalizationEvent(ctx, accLog, err, start, traceCtx).Msg("TX finished") + createAccessLogFinalizationEvent(ctx, accLog, err, start, traceCtx).Msg("TX finished") - return res, err - } + return res, err + } } func createAccessLogger(logger zerolog.Logger, start time.Time, traceCtx *tracecontext.TraceContext) zerolog.Logger { - startTime := start.Unix() - - logCtx := logger.Level(zerolog.InfoLevel).With(). - Int64("_tx_start", startTime) - // Str("_client_ip", c.IP()). - // Str("_http_method", c.Method()). - // Str("_http_path", c.Path()). - // Str("_http_user_agent", c.Get("User-Agent")). - // Str("_http_host", string(c.Request().URI().Host())). - // Str("_http_scheme", string(c.Request().URI().Scheme())) - - if traceCtx != nil { - logCtx = logCtx. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) - - if len(traceCtx.ParentID) != 0 { - logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) - } - } - - // if c.IsProxyTrusted() { // nolint: nestif - // if headerValue := c.Get("X-Forwarded-Proto"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_proto", headerValue) - // } - // - // if headerValue := c.Get("X-Forwarded-Host"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_host", headerValue) - // } - // - // if headerValue := c.Get("X-Forwarded-Path"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_path", headerValue) - // } - // - // if headerValue := c.Get("X-Forwarded-Uri"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_uri", headerValue) - // } - // - // if headerValue := c.Get("X-Forwarded-For"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_for", headerValue) - // } - // - // if headerValue := c.Get("Forwarded"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_forwarded", headerValue) - // } - // } - - return logCtx.Logger() + startTime := start.Unix() + + logCtx := logger.Level(zerolog.InfoLevel).With(). + Int64("_tx_start", startTime) + // Str("_client_ip", c.IP()). + // Str("_http_method", c.Method()). + // Str("_http_path", c.Path()). + // Str("_http_user_agent", c.Get("User-Agent")). + // Str("_http_host", string(c.Request().URI().Host())). + // Str("_http_scheme", string(c.Request().URI().Scheme())) + + if traceCtx != nil { + logCtx = logCtx. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) + } + } + + // if c.IsProxyTrusted() { // nolint: nestif + // if headerValue := c.Get("X-Forwarded-Proto"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_proto", headerValue) + // } + // + // if headerValue := c.Get("X-Forwarded-Host"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_host", headerValue) + // } + // + // if headerValue := c.Get("X-Forwarded-Path"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_path", headerValue) + // } + // + // if headerValue := c.Get("X-Forwarded-Uri"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_uri", headerValue) + // } + // + // if headerValue := c.Get("X-Forwarded-For"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_x_forwarded_for", headerValue) + // } + // + // if headerValue := c.Get("Forwarded"); len(headerValue) != 0 { + // logCtx = logCtx.Str("_http_forwarded", headerValue) + // } + // } + + return logCtx.Logger() } func createAccessLogFinalizationEvent( - ctx context.Context, - accessLogger zerolog.Logger, - err error, - start time.Time, - traceCtx *tracecontext.TraceContext, + ctx context.Context, + accessLogger zerolog.Logger, + err error, + start time.Time, + traceCtx *tracecontext.TraceContext, ) *zerolog.Event { - end := time.Now() - duration := end.Sub(start) - subject := accesscontext.Subject(ctx) - accessErr := accesscontext.Error(ctx) - - event := accessLogger.Info(). - // Int("_body_bytes_sent", len(c.Response().Body())). - // Int("_http_status_code", c.Response().StatusCode()). - Int64("_tx_duration_ms", duration.Milliseconds()) - - if traceCtx != nil { - event = event. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) - - if len(traceCtx.ParentID) != 0 { - event = event.Str("_parent_id", traceCtx.ParentID) - } - } - - switch { - case err != nil: - if len(subject) != 0 { - event = event.Str("_subject", subject) - } - - event = event.Err(err).Bool("_access_granted", false) - case accessErr != nil: - if len(subject) != 0 { - event = event.Str("_subject", subject) - } - - event = event.Err(accessErr).Bool("_access_granted", false) - default: - event = event.Str("_subject", subject).Bool("_access_granted", true) - } - - return event + end := time.Now() + duration := end.Sub(start) + subject := accesscontext.Subject(ctx) + accessErr := accesscontext.Error(ctx) + + event := accessLogger.Info(). + // Int("_body_bytes_sent", len(c.Response().Body())). + // Int("_http_status_code", c.Response().StatusCode()). + Int64("_tx_duration_ms", duration.Milliseconds()) + + if traceCtx != nil { + event = event. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + event = event.Str("_parent_id", traceCtx.ParentID) + } + } + + switch { + case err != nil: + if len(subject) != 0 { + event = event.Str("_subject", subject) + } + + event = event.Err(err).Bool("_access_granted", false) + case accessErr != nil: + if len(subject) != 0 { + event = event.Str("_subject", subject) + } + + event = event.Err(accessErr).Bool("_access_granted", false) + default: + event = event.Str("_subject", subject).Bool("_access_granted", true) + } + + return event } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go b/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go index 43f3a74c3..360b1f2ff 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go @@ -17,15 +17,15 @@ package cache import ( - "context" + "context" - "google.golang.org/grpc" + "google.golang.org/grpc" - "github.com/dadrus/heimdall/internal/cache" + "github.com/dadrus/heimdall/internal/cache" ) func New(cch cache.Cache) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - return handler(cache.WithContext(ctx, cch), req) - } + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + return handler(cache.WithContext(ctx, cch), req) + } } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go index e2c3696cb..33f4c4055 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go @@ -17,29 +17,21 @@ package errorhandler import ( - "net/http" + "net/http" ) var defaultOptions = opts{ //nolint:gochecknoglobals - authenticationError: func(err error, verbose bool) (any, error) { - return createDeniedResponse(http.StatusUnauthorized, err, verbose), nil - }, - authorizationError: func(err error, verbose bool) (any, error) { - return createDeniedResponse(http.StatusForbidden, err, verbose), nil - }, - communicationError: func(err error, verbose bool) (any, error) { - return createDeniedResponse(http.StatusBadGateway, err, verbose), nil - }, - preconditionError: func(err error, verbose bool) (any, error) { - return createDeniedResponse(http.StatusBadRequest, err, verbose), nil - }, - badMethodError: func(err error, verbose bool) (any, error) { - return createDeniedResponse(http.StatusMethodNotAllowed, err, verbose), nil - }, - noRuleError: func(err error, verbose bool) (any, error) { - return createDeniedResponse(http.StatusNotFound, err, verbose), nil - }, - internalError: func(err error, verbose bool) (any, error) { - return createDeniedResponse(http.StatusInternalServerError, err, verbose), nil - }, + authenticationError: responseWith(http.StatusUnauthorized), + authorizationError: responseWith(http.StatusForbidden), + communicationError: responseWith(http.StatusBadGateway), + preconditionError: responseWith(http.StatusBadRequest), + badMethodError: responseWith(http.StatusMethodNotAllowed), + noRuleError: responseWith(http.StatusNotFound), + 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 + } } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go index be9f94be4..0616a9466 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go @@ -17,79 +17,81 @@ package errorhandler import ( - "context" - "errors" + "context" + "errors" - envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/rs/zerolog" - "google.golang.org/genproto/googleapis/rpc/status" - "google.golang.org/grpc" + envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/rs/zerolog" + "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc" - "github.com/dadrus/heimdall/internal/accesscontext" - "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/heimdall" ) func New(opts ...Option) grpc.UnaryServerInterceptor { - options := defaultOptions + options := defaultOptions - for _, opt := range opts { - opt(&options) - } + for _, opt := range opts { + opt(&options) + } - h := &handler{opts: options} + h := &handler{opts: options} - return h.handle + return h.handle } type handler struct { - opts + opts } -func (h *handler) handle(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { //nolint:cyclop - res, err := handler(ctx, req) - if err == nil { - return res, nil - } - - accesscontext.SetError(ctx, err) - - switch { - case errors.Is(err, heimdall.ErrAuthentication): - return h.authenticationError(err, h.verboseErrors) - case errors.Is(err, heimdall.ErrAuthorization): - return h.authorizationError(err, h.verboseErrors) - case errors.Is(err, heimdall.ErrCommunicationTimeout) || errors.Is(err, heimdall.ErrCommunication): - return h.communicationError(err, h.verboseErrors) - case errors.Is(err, heimdall.ErrArgument): - return h.preconditionError(err, h.verboseErrors) - case errors.Is(err, heimdall.ErrMethodNotAllowed): - return h.badMethodError(err, h.verboseErrors) - case errors.Is(err, heimdall.ErrNoRuleFound): - return h.noRuleError(err, h.verboseErrors) - case errors.Is(err, &heimdall.RedirectError{}): - var redirectError *heimdall.RedirectError - - errors.As(err, &redirectError) - - 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, - }, - }, - }}, - }, - }, nil - - default: - logger := zerolog.Ctx(ctx) - logger.Error().Err(err).Msg("Internal error occurred") - - return h.internalError(err, h.verboseErrors) - } +func (h *handler) handle( + ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, +) (any, error) { //nolint:cyclop + res, err := handler(ctx, req) + if err == nil { + return res, nil + } + + accesscontext.SetError(ctx, err) + + switch { + case errors.Is(err, heimdall.ErrAuthentication): + return h.authenticationError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrAuthorization): + return h.authorizationError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrCommunicationTimeout) || errors.Is(err, heimdall.ErrCommunication): + return h.communicationError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrArgument): + return h.preconditionError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrMethodNotAllowed): + return h.badMethodError(err, h.verboseErrors) + case errors.Is(err, heimdall.ErrNoRuleFound): + return h.noRuleError(err, h.verboseErrors) + case errors.Is(err, &heimdall.RedirectError{}): + var redirectError *heimdall.RedirectError + + errors.As(err, &redirectError) + + 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, + }, + }, + }}, + }, + }, nil + + default: + logger := zerolog.Ctx(ctx) + logger.Error().Err(err).Msg("Internal error occurred") + + return h.internalError(err, h.verboseErrors) + } } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go index 5e0f980d5..43d776a77 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go @@ -17,122 +17,123 @@ package errorhandler import ( - "fmt" + "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" + 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) + 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) } 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 - } - } - } + return func(o *opts) { + if code != 0 { + o.preconditionError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil + } + } + } } 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 - } - } - } + return func(o *opts) { + if code != 0 { + o.authenticationError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil + } + } + } } 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 - } - } - } + return func(o *opts) { + if code != 0 { + o.authorizationError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil + } + } + } } 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 - } - } - } + return func(o *opts) { + if code != 0 { + o.communicationError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil + } + } + } } 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 - } - } - } + return func(o *opts) { + if code != 0 { + o.internalError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil + } + } + } } 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 - } - } - } + return func(o *opts) { + if code != 0 { + o.badMethodError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil + } + } + } } 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 - } - } - } + return func(o *opts) { + if code != 0 { + o.noRuleError = func(err error, verbose bool) (any, error) { + return createDeniedResponse(code, err, verbose), nil + } + } + } } func WithVerboseErrors(flag bool) Option { - return func(o *opts) { - o.verboseErrors = flag - } + return func(o *opts) { + 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), - }, - }, - } + 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 "" - } - - if se, ok := err.(fmt.Stringer); ok { - return se.String() - } else { - return err.Error() - } + 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() } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go b/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go index dc9b252f2..8b6ea5378 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go @@ -17,29 +17,29 @@ package logger import ( - "context" + "context" - "github.com/rs/zerolog" - "google.golang.org/grpc" + "github.com/rs/zerolog" + "google.golang.org/grpc" - "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" + "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" ) func New(logger zerolog.Logger) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - logCtx := logger.With() - traceCtx := tracecontext.Extract(ctx) - - if traceCtx != nil { - logCtx = logCtx. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) - - if len(traceCtx.ParentID) != 0 { - logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) - } - } - - return handler(logCtx.Logger().WithContext(ctx), req) - } + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + logCtx := logger.With() + traceCtx := tracecontext.Extract(ctx) + + if traceCtx != nil { + logCtx = logCtx. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) + + if len(traceCtx.ParentID) != 0 { + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) + } + } + + return handler(logCtx.Logger().WithContext(ctx), req) + } } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go index 8674bf970..f8bf5f493 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go @@ -17,12 +17,12 @@ package prometheus import ( - "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus" ) // nolint: gochecknoglobals var defaultOptions = opts{ - registerer: prometheus.DefaultRegisterer, - namespace: "http", - labels: make(prometheus.Labels), + registerer: prometheus.DefaultRegisterer, + namespace: "http", + labels: make(prometheus.Labels), } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go index f2ebfd855..910a1cae3 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go @@ -17,106 +17,106 @@ package prometheus import ( - "context" - "strconv" - "time" - - envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "context" + "strconv" + "time" + + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) type metricsHandler struct { - reqCounter *prometheus.CounterVec - reqHistogram *prometheus.HistogramVec - reqInFlight *prometheus.GaugeVec + reqCounter *prometheus.CounterVec + reqHistogram *prometheus.HistogramVec + reqInFlight *prometheus.GaugeVec } func New(opts ...Option) grpc.UnaryServerInterceptor { - options := defaultOptions - - for _, opt := range opts { - opt(&options) - } - - counter := promauto.With(options.registerer).NewCounterVec( - prometheus.CounterOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), - Help: "Count all requests by status code, service and method.", - ConstLabels: options.labels, - }, - []string{"status_code", "method", "path"}, - ) - - histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), - Help: "Duration of all requests by code, service and method.", - ConstLabels: options.labels, - Buckets: []float64{ - 0.00001, 0.000025, 0.00005, 0.000075, // 10, 25, 50, 75µs - 0.0001, 0.00025, 0.0005, 0.00075, // 100, 250, 500, 750µs - 0.001, 0.0025, 0.005, 0.0075, // 1, 2.5, 5, 7.5ms - 0.01, 0.025, 0.05, 0.075, // 10, 25, 50, 75ms - 0.1, 0.25, 0.5, 0.75, // 100, 250, 500 750ms - 1.0, 2.0, // 1, 2s - }, - }, - []string{"status_code", "method", "path"}, - ) - - gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), - Help: "All the requests in progress", - ConstLabels: options.labels, - }, []string{"method"}) - - handler := &metricsHandler{ - reqCounter: counter, - reqHistogram: histogram, - reqInFlight: gauge, - } - - return handler.observeRequest + options := defaultOptions + + for _, opt := range opts { + opt(&options) + } + + counter := promauto.With(options.registerer).NewCounterVec( + prometheus.CounterOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), + Help: "Count all requests by status code, service and method.", + ConstLabels: options.labels, + }, + []string{"status_code", "method", "path"}, + ) + + histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), + Help: "Duration of all requests by code, service and method.", + ConstLabels: options.labels, + Buckets: []float64{ + 0.00001, 0.000025, 0.00005, 0.000075, // 10, 25, 50, 75µs + 0.0001, 0.00025, 0.0005, 0.00075, // 100, 250, 500, 750µs + 0.001, 0.0025, 0.005, 0.0075, // 1, 2.5, 5, 7.5ms + 0.01, 0.025, 0.05, 0.075, // 10, 25, 50, 75ms + 0.1, 0.25, 0.5, 0.75, // 100, 250, 500 750ms + 1.0, 2.0, // 1, 2s + }, + }, + []string{"status_code", "method", "path"}, + ) + + gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), + Help: "All the requests in progress", + ConstLabels: options.labels, + }, []string{"method"}) + + handler := &metricsHandler{ + reqCounter: counter, + reqHistogram: histogram, + reqInFlight: gauge, + } + + return handler.observeRequest } func (h *metricsHandler) observeRequest( - ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, + ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (any, error) { - const MagicNumber = 1e9 + const MagicNumber = 1e9 - start := time.Now() - method := "GRPC" - path := info.FullMethod - code := int(codes.OK) + start := time.Now() + method := "GRPC" + path := info.FullMethod + code := int(codes.OK) - if cr, ok := req.(*envoy_auth.CheckRequest); ok { - method = cr.Attributes.Request.Http.Method - path = cr.Attributes.Request.Http.Path - } + if cr, ok := req.(*envoy_auth.CheckRequest); ok { + method = cr.Attributes.Request.Http.Method + path = cr.Attributes.Request.Http.Path + } - h.reqInFlight.WithLabelValues(method).Inc() + h.reqInFlight.WithLabelValues(method).Inc() - defer func() { - h.reqInFlight.WithLabelValues(method).Dec() - }() + defer func() { + h.reqInFlight.WithLabelValues(method).Dec() + }() - resp, err := handler(ctx, req) + resp, err := handler(ctx, req) - if err != nil { - s, _ := status.FromError(err) - code = int(s.Code()) - } else if cr, ok := req.(*envoy_auth.CheckResponse); ok { - code = int(cr.Status.Code) - } + if err != nil { + s, _ := status.FromError(err) + code = int(s.Code()) + } else if cr, ok := req.(*envoy_auth.CheckResponse); ok { + code = int(cr.Status.Code) + } - statusCode := strconv.Itoa(code) - h.reqCounter.WithLabelValues(statusCode, method, path).Inc() + statusCode := strconv.Itoa(code) + h.reqCounter.WithLabelValues(statusCode, method, path).Inc() - elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber - h.reqHistogram.WithLabelValues(statusCode, method, path).Observe(elapsed) + elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber + h.reqHistogram.WithLabelValues(statusCode, method, path).Observe(elapsed) - return resp, err + return resp, err } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options.go index 4fcc5067f..0f295be3b 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options.go @@ -17,64 +17,64 @@ package prometheus import ( - "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus" ) type opts struct { - registerer prometheus.Registerer - labels prometheus.Labels - namespace string - subsystem string + registerer prometheus.Registerer + labels prometheus.Labels + namespace string + subsystem string } type Option func(*opts) func WithRegisterer(registerer prometheus.Registerer) Option { - return func(o *opts) { - if registerer != nil { - o.registerer = registerer - } - } + return func(o *opts) { + if registerer != nil { + o.registerer = registerer + } + } } func WithServiceName(name string) Option { - return func(o *opts) { - if len(name) != 0 { - o.labels["service"] = name - } - } + return func(o *opts) { + if len(name) != 0 { + o.labels["service"] = name + } + } } func WithNamespace(name string) Option { - return func(o *opts) { - if len(name) != 0 { - o.namespace = name - } - } + return func(o *opts) { + if len(name) != 0 { + o.namespace = name + } + } } func WithSubsystem(name string) Option { - return func(o *opts) { - if len(name) != 0 { - o.subsystem = name - } - } + return func(o *opts) { + if len(name) != 0 { + o.subsystem = name + } + } } func WithLabel(label, value string) Option { - return func(o *opts) { - if len(label) != 0 && len(value) != 0 { - o.labels[label] = value - } - } + return func(o *opts) { + if len(label) != 0 && len(value) != 0 { + o.labels[label] = value + } + } } func WithLabels(labels map[string]string) Option { - return func(o *opts) { - for label, value := range labels { - if len(label) != 0 && len(value) != 0 { - o.labels[label] = value - } - } - } + return func(o *opts) { + for label, value := range labels { + if len(label) != 0 && len(value) != 0 { + o.labels[label] = value + } + } + } } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options_test.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options_test.go index cdbecbe8f..8944ab4ed 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/options_test.go @@ -17,301 +17,301 @@ package prometheus import ( - "testing" + "testing" - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" ) func TestOptionsWithRegisterer(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - opt opts - value prometheus.Registerer - assert func(t *testing.T, opt *opts) - }{ - { - uc: "nil registerer", - opt: defaultOptions, - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Equal(t, prometheus.DefaultRegisterer, opt.registerer) - }, - }, - { - uc: "not nil registerer", - opt: defaultOptions, - value: prometheus.NewRegistry(), - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.NotNil(t, opt.registerer) - assert.NotEqual(t, prometheus.DefaultRegisterer, opt.registerer) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - apply := WithRegisterer(tc.value) - - // WHEN - apply(&tc.opt) - - // THEN - tc.assert(t, &tc.opt) - }) - } + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + value prometheus.Registerer + assert func(t *testing.T, opt *opts) + }{ + { + uc: "nil registerer", + opt: defaultOptions, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Equal(t, prometheus.DefaultRegisterer, opt.registerer) + }, + }, + { + uc: "not nil registerer", + opt: defaultOptions, + value: prometheus.NewRegistry(), + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.NotNil(t, opt.registerer) + assert.NotEqual(t, prometheus.DefaultRegisterer, opt.registerer) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithRegisterer(tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } } func TestOptionsWithServiceName(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - opt opts - value string - assert func(t *testing.T, opt *opts) - }{ - { - uc: "empty service name", - opt: opts{}, - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Empty(t, opt.labels) - }, - }, - { - uc: "not empty service name", - opt: opts{labels: make(prometheus.Labels)}, - value: "foo", - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Len(t, opt.labels, 1) - assert.Equal(t, "foo", opt.labels["service"]) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - apply := WithServiceName(tc.value) - - // WHEN - apply(&tc.opt) - - // THEN - tc.assert(t, &tc.opt) - }) - } + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + value string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty service name", + opt: opts{}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "not empty service name", + opt: opts{labels: make(prometheus.Labels)}, + value: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Len(t, opt.labels, 1) + assert.Equal(t, "foo", opt.labels["service"]) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithServiceName(tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } } func TestOptionsWithNamespace(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - opt opts - value string - assert func(t *testing.T, opt *opts) - }{ - { - uc: "empty namespace", - opt: opts{}, - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Empty(t, opt.namespace) - }, - }, - { - uc: "not empty service name", - opt: opts{}, - value: "foo", - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Equal(t, "foo", opt.namespace) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - apply := WithNamespace(tc.value) - - // WHEN - apply(&tc.opt) - - // THEN - tc.assert(t, &tc.opt) - }) - } + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + value string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty namespace", + opt: opts{}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.namespace) + }, + }, + { + uc: "not empty service name", + opt: opts{}, + value: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Equal(t, "foo", opt.namespace) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithNamespace(tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } } func TestOptionsWithSubsystem(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - opt opts - value string - assert func(t *testing.T, opt *opts) - }{ - { - uc: "empty subsystem", - opt: opts{}, - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Empty(t, opt.subsystem) - }, - }, - { - uc: "not empty subsystem", - opt: opts{}, - value: "foo", - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Equal(t, "foo", opt.subsystem) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - apply := WithSubsystem(tc.value) - - // WHEN - apply(&tc.opt) - - // THEN - tc.assert(t, &tc.opt) - }) - } + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + value string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty subsystem", + opt: opts{}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.subsystem) + }, + }, + { + uc: "not empty subsystem", + opt: opts{}, + value: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Equal(t, "foo", opt.subsystem) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithSubsystem(tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } } func TestOptionsWithLabel(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - opt opts - name string - value string - assert func(t *testing.T, opt *opts) - }{ - { - uc: "empty label name", - opt: opts{}, - value: "foo", - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Empty(t, opt.labels) - }, - }, - { - uc: "empty label value", - opt: opts{}, - name: "foo", - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Empty(t, opt.labels) - }, - }, - { - uc: "not empty label name & value", - opt: opts{labels: make(prometheus.Labels)}, - name: "foo", - value: "bar", - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Len(t, opt.labels, 1) - assert.Equal(t, "bar", opt.labels["foo"]) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - apply := WithLabel(tc.name, tc.value) - - // WHEN - apply(&tc.opt) - - // THEN - tc.assert(t, &tc.opt) - }) - } + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + name string + value string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty label name", + opt: opts{}, + value: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "empty label value", + opt: opts{}, + name: "foo", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "not empty label name & value", + opt: opts{labels: make(prometheus.Labels)}, + name: "foo", + value: "bar", + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Len(t, opt.labels, 1) + assert.Equal(t, "bar", opt.labels["foo"]) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithLabel(tc.name, tc.value) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } } func TestOptionsWithLabels(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - opt opts - labels map[string]string - assert func(t *testing.T, opt *opts) - }{ - { - uc: "empty labels map", - opt: opts{}, - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Empty(t, opt.labels) - }, - }, - { - uc: "map with empty key", - opt: opts{}, - labels: map[string]string{"": "bar"}, - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Empty(t, opt.labels) - }, - }, - { - uc: "map with empty value", - opt: opts{}, - labels: map[string]string{"foo": ""}, - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Empty(t, opt.labels) - }, - }, - { - uc: "map with multiple not empty label name & value", - opt: opts{labels: make(prometheus.Labels)}, - labels: map[string]string{ - "foo": "bar", - "baz": "zab", - }, - assert: func(t *testing.T, opt *opts) { - t.Helper() - - assert.Len(t, opt.labels, 2) - assert.Equal(t, "bar", opt.labels["foo"]) - assert.Equal(t, "zab", opt.labels["baz"]) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - apply := WithLabels(tc.labels) - - // WHEN - apply(&tc.opt) - - // THEN - tc.assert(t, &tc.opt) - }) - } + t.Parallel() + + for _, tc := range []struct { + uc string + opt opts + labels map[string]string + assert func(t *testing.T, opt *opts) + }{ + { + uc: "empty labels map", + opt: opts{}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "map with empty key", + opt: opts{}, + labels: map[string]string{"": "bar"}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "map with empty value", + opt: opts{}, + labels: map[string]string{"foo": ""}, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Empty(t, opt.labels) + }, + }, + { + uc: "map with multiple not empty label name & value", + opt: opts{labels: make(prometheus.Labels)}, + labels: map[string]string{ + "foo": "bar", + "baz": "zab", + }, + assert: func(t *testing.T, opt *opts) { + t.Helper() + + assert.Len(t, opt.labels, 2) + assert.Equal(t, "bar", opt.labels["foo"]) + assert.Equal(t, "zab", opt.labels["baz"]) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + apply := WithLabels(tc.labels) + + // WHEN + apply(&tc.opt) + + // THEN + tc.assert(t, &tc.opt) + }) + } } diff --git a/internal/handler/envoyextauth/grpcv3/module.go b/internal/handler/envoyextauth/grpcv3/module.go index 44560d41b..fe381a8ea 100644 --- a/internal/handler/envoyextauth/grpcv3/module.go +++ b/internal/handler/envoyextauth/grpcv3/module.go @@ -17,77 +17,77 @@ package grpcv3 import ( - "context" + "context" - "github.com/prometheus/client_golang/prometheus" - "github.com/rs/zerolog" - "go.uber.org/fx" + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog" + "go.uber.org/fx" - "github.com/dadrus/heimdall/internal/cache" - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/handler/listener" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules" + "github.com/dadrus/heimdall/internal/cache" + "github.com/dadrus/heimdall/internal/config" + "github.com/dadrus/heimdall/internal/handler/listener" + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules" ) var Module = fx.Options( // nolint: gochecknoglobals - fx.Invoke(registerHooks), + fx.Invoke(registerHooks), ) type hooksArgs struct { - fx.In - - Lifecycle fx.Lifecycle - Config *config.Configuration - Logger zerolog.Logger - Repository rules.Repository - Signer heimdall.JWTSigner - Registerer prometheus.Registerer - Cache cache.Cache + fx.In + + Lifecycle fx.Lifecycle + Config *config.Configuration + Logger zerolog.Logger + Repository rules.Repository + Signer heimdall.JWTSigner + Registerer prometheus.Registerer + Cache cache.Cache } func registerHooks(args hooksArgs) { - ln, err := listener.New("tcp4", args.Config.Serve.Decision) - if err != nil { - args.Logger.Fatal().Err(err).Msg("Could not create listener for the Decision Envoy ExtAuth service") - - return - } - - service := newService(args.Config, args.Registerer, args.Cache, args.Logger, args.Repository, args.Signer) - - args.Lifecycle.Append( - fx.Hook{ - OnStart: func(ctx context.Context) error { - go func() { - args.Logger.Info().Str("_address", ln.Addr().String()). - Msg("Decision Envoy ExtAuth service starts listening") - - if err = service.Serve(ln); err != nil { - args.Logger.Fatal().Err(err).Msg("Could not start Decision Envoy ExtAuth service") - } - }() - - return nil - }, - OnStop: func(ctx context.Context) error { - args.Logger.Info().Msg("Tearing down Decision Envoy ExtAuth service") - - done := make(chan struct{}) - - go func() { - service.GracefulStop() - close(done) - }() - - select { - case <-done: - case <-ctx.Done(): - service.Stop() - } - - return nil - }, - }, - ) + ln, err := listener.New("tcp4", args.Config.Serve.Decision) + if err != nil { + args.Logger.Fatal().Err(err).Msg("Could not create listener for the Decision Envoy ExtAuth service") + + return + } + + service := newService(args.Config, args.Registerer, args.Cache, args.Logger, args.Repository, args.Signer) + + args.Lifecycle.Append( + fx.Hook{ + OnStart: func(ctx context.Context) error { + go func() { + args.Logger.Info().Str("_address", ln.Addr().String()). + Msg("Decision Envoy ExtAuth service starts listening") + + if err = service.Serve(ln); err != nil { + args.Logger.Fatal().Err(err).Msg("Could not start Decision Envoy ExtAuth service") + } + }() + + return nil + }, + OnStop: func(ctx context.Context) error { + args.Logger.Info().Msg("Tearing down Decision Envoy ExtAuth service") + + done := make(chan struct{}) + + go func() { + service.GracefulStop() + close(done) + }() + + select { + case <-done: + case <-ctx.Done(): + service.Stop() + } + + return nil + }, + }, + ) } diff --git a/internal/handler/envoyextauth/grpcv3/request_context.go b/internal/handler/envoyextauth/grpcv3/request_context.go index d4fdc9f3b..fb35279e2 100644 --- a/internal/handler/envoyextauth/grpcv3/request_context.go +++ b/internal/handler/envoyextauth/grpcv3/request_context.go @@ -17,84 +17,90 @@ package grpcv3 import ( - "context" - "fmt" - "net/http" - "net/url" - "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" - "google.golang.org/genproto/googleapis/rpc/status" - - "github.com/dadrus/heimdall/internal/heimdall" + "context" + "fmt" + "net/http" + "net/url" + "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" + "google.golang.org/genproto/googleapis/rpc/status" + + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/x" ) type RequestContext struct { - ctx context.Context - req *envoy_auth.CheckRequest - reqURL *url.URL - upstreamHeaders http.Header - upstreamCookies map[string]string - jwtSigner heimdall.JWTSigner - err error + ctx context.Context // nolint: containedctx + req *envoy_auth.CheckRequest + reqURL *url.URL + upstreamHeaders http.Header + upstreamCookies map[string]string + jwtSigner heimdall.JWTSigner + err error } func NewRequestContext(ctx context.Context, req *envoy_auth.CheckRequest, signer heimdall.JWTSigner) *RequestContext { - return &RequestContext{ - ctx: ctx, - req: req, - reqURL: &url.URL{ - Scheme: req.Attributes.Request.Http.Scheme, - Host: req.Attributes.Request.Http.Host, - Path: req.Attributes.Request.Http.Path, - RawQuery: req.Attributes.Request.Http.Query, - Fragment: req.Attributes.Request.Http.Fragment, - }, - jwtSigner: signer, - upstreamHeaders: make(http.Header), - upstreamCookies: make(map[string]string), - } + return &RequestContext{ + ctx: ctx, + req: req, + reqURL: &url.URL{ + Scheme: req.Attributes.Request.Http.Scheme, + Host: req.Attributes.Request.Http.Host, + Path: req.Attributes.Request.Http.Path, + RawQuery: req.Attributes.Request.Http.Query, + Fragment: req.Attributes.Request.Http.Fragment, + }, + jwtSigner: signer, + upstreamHeaders: make(http.Header), + upstreamCookies: make(map[string]string), + } } func (s *RequestContext) RequestMethod() string { return s.req.Attributes.Request.Http.Method } + func (s *RequestContext) RequestHeaders() map[string]string { - return s.req.Attributes.Request.Http.Headers + return s.req.Attributes.Request.Http.Headers } + func (s *RequestContext) RequestHeader(name string) string { - return s.req.Attributes.Request.Http.Headers[name] + return s.req.Attributes.Request.Http.Headers[name] } + func (s *RequestContext) RequestCookie(name string) string { - values, ok := s.req.Attributes.Request.Http.Headers["cookie"] - if !ok { - return "" - } - - for _, cookie := range strings.Split(values, ";") { - if cookieName, cookieValue, ok := strings.Cut(cookie, "="); - ok && strings.TrimSpace(cookieName) == name { - return strings.TrimSpace(cookieValue) - } - } - - return "" + values, ok := s.req.Attributes.Request.Http.Headers["cookie"] + if !ok { + return "" + } + + for _, cookie := range strings.Split(values, ";") { + if cookieName, cookieValue, ok := strings.Cut(cookie, "="); ok && strings.TrimSpace(cookieName) == name { + return strings.TrimSpace(cookieValue) + } + } + + return "" } + func (s *RequestContext) RequestQueryParameter(name string) string { - return s.reqURL.Query().Get(name) + return s.reqURL.Query().Get(name) } + func (s *RequestContext) RequestFormParameter(name string) string { - if s.req.Attributes.Request.Http.Headers["content-type"] != "application/x-www-form-urlencoded" { - return "" - } + if s.req.Attributes.Request.Http.Headers["content-type"] != "application/x-www-form-urlencoded" { + return "" + } - values, err := url.ParseQuery(s.req.Attributes.Request.Http.Body) - if err != nil { - return "" - } + values, err := url.ParseQuery(s.req.Attributes.Request.Http.Body) + if err != nil { + return "" + } - return values.Get(name) + return values.Get(name) } + func (s *RequestContext) RequestBody() []byte { return s.req.Attributes.Request.Http.RawBody } func (s *RequestContext) AppContext() context.Context { return s.ctx } func (s *RequestContext) SetPipelineError(err error) { s.err = err } @@ -105,40 +111,46 @@ func (s *RequestContext) RequestURL() *url.URL { return s.req func (s *RequestContext) RequestClientIPs() []string { return nil } func (s *RequestContext) Finalize() (*envoy_auth.CheckResponse, error) { - if s.err != nil { - return nil, s.err - } - - var headers []*envoy_core.HeaderValueOption - - for k := range s.upstreamHeaders { - headers = append(headers, &envoy_core.HeaderValueOption{ - Header: &envoy_core.HeaderValue{ - Key: k, - Value: strings.Join(s.upstreamHeaders.Values(k), ","), - }, - }) - } - - if len(s.upstreamCookies) != 0 { - var cookies []string - - for k, v := range s.upstreamCookies { - cookies = append(cookies, fmt.Sprintf("%s=%s", k, v)) - } - - headers = append(headers, &envoy_core.HeaderValueOption{ - Header: &envoy_core.HeaderValue{ - Key: "Cookie", - Value: strings.Join(cookies, ";"), - }, - }) - } - - return &envoy_auth.CheckResponse{ - Status: &status.Status{Code: int32(envoy_type.StatusCode_OK)}, - HttpResponse: &envoy_auth.CheckResponse_OkResponse{ - OkResponse: &envoy_auth.OkHttpResponse{Headers: headers}, - }, - }, nil + if s.err != nil { + return nil, s.err + } + + headers := make([]*envoy_core.HeaderValueOption, + len(s.upstreamHeaders)+x.IfThenElse(len(s.upstreamCookies) == 0, 0, 1)) + hidx := 0 + + for k := range s.upstreamHeaders { + headers[hidx] = &envoy_core.HeaderValueOption{ + Header: &envoy_core.HeaderValue{ + Key: k, + Value: strings.Join(s.upstreamHeaders.Values(k), ","), + }, + } + + hidx++ + } + + if len(s.upstreamCookies) != 0 { + cookies := make([]string, len(s.upstreamCookies)) + cidx := 0 + + for k, v := range s.upstreamCookies { + cookies[cidx] = fmt.Sprintf("%s=%s", k, v) + cidx++ + } + + headers[hidx] = &envoy_core.HeaderValueOption{ + Header: &envoy_core.HeaderValue{ + Key: "Cookie", + Value: strings.Join(cookies, ";"), + }, + } + } + + return &envoy_auth.CheckResponse{ + Status: &status.Status{Code: int32(envoy_type.StatusCode_OK)}, + HttpResponse: &envoy_auth.CheckResponse_OkResponse{ + OkResponse: &envoy_auth.OkHttpResponse{Headers: headers}, + }, + }, nil } diff --git a/internal/handler/envoyextauth/grpcv3/service.go b/internal/handler/envoyextauth/grpcv3/service.go index 97a9902fe..bced67023 100644 --- a/internal/handler/envoyextauth/grpcv3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -1,68 +1,84 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + package grpcv3 import ( - envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/grpc-ecosystem/go-grpc-middleware/recovery" - "github.com/prometheus/client_golang/prometheus" - "github.com/rs/zerolog" - "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" - "google.golang.org/grpc" + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" - "github.com/dadrus/heimdall/internal/cache" - "github.com/dadrus/heimdall/internal/config" - accesslogmiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/accesslog" - cachemiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/cache" - errormiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/errorhandler" - loggermiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/logger" - prometheusmiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/prometheus" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules" + "github.com/dadrus/heimdall/internal/cache" + "github.com/dadrus/heimdall/internal/config" + accesslogmiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/accesslog" + cachemiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/cache" + errormiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/errorhandler" + loggermiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/logger" + prometheusmiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/prometheus" + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules" ) func newService( - conf *config.Configuration, - registrer prometheus.Registerer, - cch cache.Cache, - logger zerolog.Logger, - repository rules.Repository, - signer heimdall.JWTSigner, + conf *config.Configuration, + registerer prometheus.Registerer, + cch cache.Cache, + logger zerolog.Logger, + repository rules.Repository, + signer heimdall.JWTSigner, ) *grpc.Server { - service := conf.Serve.Decision + service := conf.Serve.Decision - interceptors := []grpc.UnaryServerInterceptor{ - grpc_recovery.UnaryServerInterceptor(), - otelgrpc.UnaryServerInterceptor(), - } + interceptors := []grpc.UnaryServerInterceptor{ + grpc_recovery.UnaryServerInterceptor(), + otelgrpc.UnaryServerInterceptor(), + } - if conf.Metrics.Enabled { - interceptors = append(interceptors, - prometheusmiddleware.New( - prometheusmiddleware.WithServiceName("decision"), - prometheusmiddleware.WithLabel("type", "envoy-grpc-extauth-v3"), - prometheusmiddleware.WithRegisterer(registrer), - ), - ) - } + if conf.Metrics.Enabled { + interceptors = append(interceptors, + prometheusmiddleware.New( + prometheusmiddleware.WithServiceName("decision"), + prometheusmiddleware.WithLabel("type", "envoy-grpc-extauth-v3"), + prometheusmiddleware.WithRegisterer(registerer), + ), + ) + } - interceptors = append(interceptors, - errormiddleware.New( - errormiddleware.WithVerboseErrors(service.Respond.Verbose), - errormiddleware.WithPreconditionErrorCode(service.Respond.With.ArgumentError.Code), - errormiddleware.WithAuthenticationErrorCode(service.Respond.With.AuthenticationError.Code), - errormiddleware.WithAuthorizationErrorCode(service.Respond.With.AuthorizationError.Code), - errormiddleware.WithCommunicationErrorCode(service.Respond.With.CommunicationError.Code), - errormiddleware.WithMethodErrorCode(service.Respond.With.BadMethodError.Code), - errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), - errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), - ), - accesslogmiddleware.New(logger), - loggermiddleware.New(logger), - cachemiddleware.New(cch), - ) + interceptors = append(interceptors, + errormiddleware.New( + errormiddleware.WithVerboseErrors(service.Respond.Verbose), + errormiddleware.WithPreconditionErrorCode(service.Respond.With.ArgumentError.Code), + errormiddleware.WithAuthenticationErrorCode(service.Respond.With.AuthenticationError.Code), + errormiddleware.WithAuthorizationErrorCode(service.Respond.With.AuthorizationError.Code), + errormiddleware.WithCommunicationErrorCode(service.Respond.With.CommunicationError.Code), + errormiddleware.WithMethodErrorCode(service.Respond.With.BadMethodError.Code), + errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), + errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), + ), + accesslogmiddleware.New(logger), + loggermiddleware.New(logger), + cachemiddleware.New(cch), + ) - srv := grpc.NewServer(grpc.ChainUnaryInterceptor(interceptors...)) + srv := grpc.NewServer(grpc.ChainUnaryInterceptor(interceptors...)) - envoy_auth.RegisterAuthorizationServer(srv, &Handler{r: repository, s: signer}) + envoy_auth.RegisterAuthorizationServer(srv, &Handler{r: repository, s: signer}) - return srv + return srv } diff --git a/internal/rules/composite_subject_creator.go b/internal/rules/composite_subject_creator.go index c9b8fd280..653585edd 100644 --- a/internal/rules/composite_subject_creator.go +++ b/internal/rules/composite_subject_creator.go @@ -17,43 +17,43 @@ package rules import ( - "errors" + "errors" - "github.com/rs/zerolog" + "github.com/rs/zerolog" - "github.com/dadrus/heimdall/internal/accesscontext" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) type compositeSubjectCreator []subjectCreator func (ca compositeSubjectCreator) Execute(ctx heimdall.Context) (*subject.Subject, error) { - logger := zerolog.Ctx(ctx.AppContext()) + logger := zerolog.Ctx(ctx.AppContext()) - var ( - sub *subject.Subject - err error - ) + var ( + sub *subject.Subject + err error + ) - for idx, a := range ca { - sub, err = a.Execute(ctx) - if err != nil { - logger.Info().Err(err).Msg("Pipeline step execution failed") + for idx, a := range ca { + sub, err = a.Execute(ctx) + if err != nil { + logger.Info().Err(err).Msg("Pipeline step execution failed") - if (errors.Is(err, heimdall.ErrArgument) || a.IsFallbackOnErrorAllowed()) && idx < len(ca) { - logger.Info().Msg("Falling back to next configured one.") + if (errors.Is(err, heimdall.ErrArgument) || a.IsFallbackOnErrorAllowed()) && idx < len(ca) { + logger.Info().Msg("Falling back to next configured one.") - continue - } + continue + } - break - } + break + } - accesscontext.SetSubject(ctx.AppContext(), sub.ID) + accesscontext.SetSubject(ctx.AppContext(), sub.ID) - return sub, nil - } + return sub, nil + } - return nil, err + return nil, err } diff --git a/internal/x/errorchain/error_chain.go b/internal/x/errorchain/error_chain.go index c137a4472..6a59110fd 100644 --- a/internal/x/errorchain/error_chain.go +++ b/internal/x/errorchain/error_chain.go @@ -17,172 +17,172 @@ package errorchain import ( - "encoding/xml" - "errors" - "fmt" - "reflect" - "strings" - - "github.com/goccy/go-json" - "github.com/iancoleman/strcase" + "encoding/xml" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/goccy/go-json" + "github.com/iancoleman/strcase" ) type element struct { - err error - msg string - next *element + err error + msg string + next *element } type ErrorChain struct { // nolint: errname - head *element - tail *element - context any + head *element + tail *element + context any } func New(err error) *ErrorChain { - chain := &ErrorChain{} + chain := &ErrorChain{} - return chain.causedBy(err, "") + return chain.causedBy(err, "") } func NewWithMessage(err error, message string) *ErrorChain { - chain := &ErrorChain{} + chain := &ErrorChain{} - return chain.causedBy(err, message) + return chain.causedBy(err, message) } func NewWithMessagef(err error, format string, a ...any) *ErrorChain { - chain := &ErrorChain{} + chain := &ErrorChain{} - return chain.causedBy(err, fmt.Sprintf(format, a...)) + return chain.causedBy(err, fmt.Sprintf(format, a...)) } func (ec *ErrorChain) Error() string { - var errs []string + var errs []string - for c := ec.head; c != nil; c = c.next { - if len(c.msg) == 0 { - errs = append(errs, c.err.Error()) - } else { - errs = append(errs, fmt.Sprintf("%s: %s", c.err.Error(), c.msg)) - } - } + for c := ec.head; c != nil; c = c.next { + if len(c.msg) == 0 { + errs = append(errs, c.err.Error()) + } else { + errs = append(errs, fmt.Sprintf("%s: %s", c.err.Error(), c.msg)) + } + } - return strings.Join(errs, ": ") + return strings.Join(errs, ": ") } func (ec *ErrorChain) causedBy(err error, msg string) *ErrorChain { - wrappedError := &element{err: err, msg: msg} + wrappedError := &element{err: err, msg: msg} - if ec.head == nil { - ec.head = wrappedError - ec.tail = wrappedError + if ec.head == nil { + ec.head = wrappedError + ec.tail = wrappedError - return ec - } + return ec + } - ec.tail.next = wrappedError - ec.tail = wrappedError + ec.tail.next = wrappedError + ec.tail = wrappedError - return ec + return ec } func (ec *ErrorChain) CausedBy(err error) *ErrorChain { - return ec.causedBy(err, "") + return ec.causedBy(err, "") } func (ec *ErrorChain) WithErrorContext(context any) *ErrorChain { - ec.context = context + ec.context = context - return ec + return ec } func (ec *ErrorChain) Unwrap() error { - if ec.head == nil || ec.head.next == nil { - return nil - } + if ec.head == nil || ec.head.next == nil { + return nil + } - return &ErrorChain{ - head: ec.head.next, - tail: ec.tail, - context: ec.context, - } + return &ErrorChain{ + head: ec.head.next, + tail: ec.tail, + context: ec.context, + } } func (ec *ErrorChain) Is(target error) bool { - if ec.head == nil { - return false - } + if ec.head == nil { + return false + } - return errors.Is(ec.head.err, target) + return errors.Is(ec.head.err, target) } func (ec *ErrorChain) As(target any) bool { - if ec.head == nil { - return false - } + if ec.head == nil { + return false + } - if ec.asTarget(target) { - return true - } + if ec.asTarget(target) { + return true + } - return errors.As(ec.head.err, target) + return errors.As(ec.head.err, target) } func (ec *ErrorChain) asTarget(target any) bool { - if ec.context == nil { - return false - } + if ec.context == nil { + return false + } - val := reflect.ValueOf(target) - targetType := val.Type().Elem() + val := reflect.ValueOf(target) + targetType := val.Type().Elem() - if targetType.Kind() != reflect.Interface || !reflect.TypeOf(ec.context).AssignableTo(targetType) { - return false - } + if targetType.Kind() != reflect.Interface || !reflect.TypeOf(ec.context).AssignableTo(targetType) { + return false + } - val.Elem().Set(reflect.ValueOf(ec.context)) + val.Elem().Set(reflect.ValueOf(ec.context)) - return true + return true } func (ec *ErrorChain) ErrorContext() any { - return ec.context + return ec.context } func (ec *ErrorChain) Errors() []error { - var errs []error + var errs []error - for c := ec.head; c != nil; c = c.next { - errs = append(errs, c.err) - } + for c := ec.head; c != nil; c = c.next { + errs = append(errs, c.err) + } - return errs + return errs } func (ec *ErrorChain) MarshalJSON() ([]byte, error) { - return json.Marshal( - message{ - Code: strcase.ToLowerCamel(ec.head.err.Error()), - Message: ec.head.msg, - }) + return json.Marshal( + message{ + Code: strcase.ToLowerCamel(ec.head.err.Error()), + Message: ec.head.msg, + }) } func (ec *ErrorChain) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error { - return encoder.Encode( - message{ - XMLName: xml.Name{Local: "error"}, - Code: strcase.ToLowerCamel(ec.head.err.Error()), - Message: ec.head.msg, - }) + return encoder.Encode( + message{ + XMLName: xml.Name{Local: "error"}, + Code: strcase.ToLowerCamel(ec.head.err.Error()), + Message: ec.head.msg, + }) } func (ec *ErrorChain) String() string { - return fmt.Sprintf("%s: %s", ec.head.err.Error(), ec.head.msg) + return fmt.Sprintf("%s: %s", ec.head.err.Error(), ec.head.msg) } type message struct { - XMLName xml.Name `json:"-"` - Code string `xml:"code" json:"code"` - Message string `xml:"message,omitempty" json:"message,omitempty"` + XMLName xml.Name `json:"-"` + Code string `xml:"code" json:"code"` + Message string `xml:"message,omitempty" json:"message,omitempty"` } From c9a30ad7fb5800daa8232cd2f4a16e73edabe772 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Wed, 1 Feb 2023 13:37:19 +0100 Subject: [PATCH 10/67] tests fixed --- internal/fiber/middleware/prometheus/handler_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/fiber/middleware/prometheus/handler_test.go b/internal/fiber/middleware/prometheus/handler_test.go index 8aad65cde..29315b2e4 100644 --- a/internal/fiber/middleware/prometheus/handler_test.go +++ b/internal/fiber/middleware/prometheus/handler_test.go @@ -75,7 +75,7 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx histMetric := metricForType(metrics, dto.MetricType_HISTOGRAM.Enum()) assert.Equal(t, "foo_bar_request_duration_seconds", histMetric.GetName()) - assert.Equal(t, "Duration of all HTTP requests by status code, method and path.", + assert.Equal(t, "Duration of all requests by status code, method and path.", histMetric.GetHelp()) require.Len(t, histMetric.Metric, 1) assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) @@ -96,7 +96,7 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx counterMetric := metricForType(metrics, dto.MetricType_COUNTER.Enum()) assert.Equal(t, "foo_bar_requests_total", counterMetric.GetName()) - assert.Equal(t, "Count all http requests by status code, method and path.", + assert.Equal(t, "Count all requests by status code, method and path.", counterMetric.GetHelp()) require.Len(t, counterMetric.Metric, 1) assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) @@ -117,7 +117,7 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx histMetric := metricForType(metrics, dto.MetricType_HISTOGRAM.Enum()) assert.Equal(t, "foo_bar_request_duration_seconds", histMetric.GetName()) - assert.Equal(t, "Duration of all HTTP requests by status code, method and path.", + assert.Equal(t, "Duration of all requests by status code, method and path.", histMetric.GetHelp()) require.Len(t, histMetric.Metric, 1) assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) @@ -138,7 +138,7 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx counterMetric := metricForType(metrics, dto.MetricType_COUNTER.Enum()) assert.Equal(t, "foo_bar_requests_total", counterMetric.GetName()) - assert.Equal(t, "Count all http requests by status code, method and path.", + assert.Equal(t, "Count all requests by status code, method and path.", counterMetric.GetHelp()) require.Len(t, counterMetric.Metric, 1) assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) @@ -159,7 +159,7 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx histMetric := metricForType(metrics, dto.MetricType_HISTOGRAM.Enum()) assert.Equal(t, "foo_bar_request_duration_seconds", histMetric.GetName()) - assert.Equal(t, "Duration of all HTTP requests by status code, method and path.", + assert.Equal(t, "Duration of all requests by status code, method and path.", histMetric.GetHelp()) require.Len(t, histMetric.Metric, 1) assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) @@ -180,7 +180,7 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx counterMetric := metricForType(metrics, dto.MetricType_COUNTER.Enum()) assert.Equal(t, "foo_bar_requests_total", counterMetric.GetName()) - assert.Equal(t, "Count all http requests by status code, method and path.", + assert.Equal(t, "Count all requests by status code, method and path.", counterMetric.GetHelp()) require.Len(t, counterMetric.Metric, 1) assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) From 8c40761828b22fd51f22482607688719867a05d1 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Wed, 1 Feb 2023 13:37:58 +0100 Subject: [PATCH 11/67] additional labels for metrics removed --- internal/handler/decision/app.go | 1 - internal/handler/envoyextauth/grpcv3/service.go | 1 - 2 files changed, 2 deletions(-) diff --git a/internal/handler/decision/app.go b/internal/handler/decision/app.go index 75ab84199..724498ace 100644 --- a/internal/handler/decision/app.go +++ b/internal/handler/decision/app.go @@ -72,7 +72,6 @@ func newApp(args appArgs) *fiber.App { if args.Config.Metrics.Enabled { app.Use(prometheusmiddleware.New( prometheusmiddleware.WithServiceName("decision"), - prometheusmiddleware.WithLabel("type", "http"), prometheusmiddleware.WithRegisterer(args.Registerer), )) } diff --git a/internal/handler/envoyextauth/grpcv3/service.go b/internal/handler/envoyextauth/grpcv3/service.go index bced67023..9cecb546c 100644 --- a/internal/handler/envoyextauth/grpcv3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -54,7 +54,6 @@ func newService( interceptors = append(interceptors, prometheusmiddleware.New( prometheusmiddleware.WithServiceName("decision"), - prometheusmiddleware.WithLabel("type", "envoy-grpc-extauth-v3"), prometheusmiddleware.WithRegisterer(registerer), ), ) From e35ec758cc2e81383a980df569dcecf0eb234dcd Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Wed, 1 Feb 2023 13:38:19 +0100 Subject: [PATCH 12/67] namcpace set to grpc --- .../envoyextauth/grpcv3/middleware/prometheus/defaults.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go index f8bf5f493..e0c9d4ec5 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/defaults.go @@ -23,6 +23,6 @@ import ( // nolint: gochecknoglobals var defaultOptions = opts{ registerer: prometheus.DefaultRegisterer, - namespace: "http", + namespace: "grpc", labels: make(prometheus.Labels), } From 94bdc35eab96bef0fceeb8f38396f855979090b4 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 2 Feb 2023 14:26:38 +0100 Subject: [PATCH 13/67] access logger implementation simplified --- .../middleware/accesslog/accesslog_handler.go | 137 +++++++----------- .../middleware/accesslog/accesslog_handler.go | 135 +++++++---------- 2 files changed, 105 insertions(+), 167 deletions(-) diff --git a/internal/fiber/middleware/accesslog/accesslog_handler.go b/internal/fiber/middleware/accesslog/accesslog_handler.go index b37d98d42..71be20db3 100644 --- a/internal/fiber/middleware/accesslog/accesslog_handler.go +++ b/internal/fiber/middleware/accesslog/accesslog_handler.go @@ -17,6 +17,7 @@ package accesslog import ( + "context" "time" "github.com/gofiber/fiber/v2" @@ -29,119 +30,85 @@ import ( func New(logger zerolog.Logger) fiber.Handler { return func(c *fiber.Ctx) error { start := time.Now() - traceCtx := tracecontext.Extract(c.UserContext()) + c.SetUserContext(accesscontext.New(c.UserContext())) - accLog := createAccessLogger(c, logger, start, traceCtx) + logCtx := logger.Level(zerolog.InfoLevel).With(). + Int64("_tx_start", start.Unix()). + Str("_client_ip", c.IP()). + Str("_http_method", c.Method()). + Str("_http_path", c.Path()). + Str("_http_user_agent", c.Get("User-Agent")). + Str("_http_host", string(c.Request().URI().Host())). + Str("_http_scheme", string(c.Request().URI().Scheme())) + logCtx = logTraceData(c.UserContext(), logCtx) + + if c.IsProxyTrusted() { + logCtx = logHeader(c, logCtx, "X-Forwarded-Proto", "_http_x_forwarded_proto") + logCtx = logHeader(c, logCtx, "X-Forwarded-Host", "_http_x_forwarded_host") + logCtx = logHeader(c, logCtx, "X-Forwarded-Path", "_http_x_forwarded_path") + logCtx = logHeader(c, logCtx, "X-Forwarded-Uri", "_http_x_forwarded_uri") + logCtx = logHeader(c, logCtx, "X-Forwarded-For", "_http_x_forwarded_for") + logCtx = logHeader(c, logCtx, "Forwarded", "_http_forwarded") + } + + accLog := logCtx.Logger() accLog.Info().Msg("TX started") err := c.Next() - createAccessLogFinalizationEvent(c, accLog, err, start, traceCtx).Msg("TX finished") + logAccessStatus(c.UserContext(), accLog.Info(), err). + Int("_body_bytes_sent", len(c.Response().Body())). + Int("_http_status_code", c.Response().StatusCode()). + Int64("_tx_duration_ms", time.Until(start).Milliseconds()). + Msg("TX finished") return err } } -func createAccessLogger( - c *fiber.Ctx, - logger zerolog.Logger, - start time.Time, - traceCtx *tracecontext.TraceContext, -) zerolog.Logger { - startTime := start.Unix() - - logCtx := logger.Level(zerolog.InfoLevel).With(). - Int64("_tx_start", startTime). - Str("_client_ip", c.IP()). - Str("_http_method", c.Method()). - Str("_http_path", c.Path()). - Str("_http_user_agent", c.Get("User-Agent")). - Str("_http_host", string(c.Request().URI().Host())). - Str("_http_scheme", string(c.Request().URI().Scheme())) - - if traceCtx != nil { - logCtx = logCtx. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) +func logAccessStatus(ctx context.Context, event *zerolog.Event, err error) *zerolog.Event { + subject := accesscontext.Subject(ctx) + accessErr := accesscontext.Error(ctx) - if len(traceCtx.ParentID) != 0 { - logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) - } - } - - if c.IsProxyTrusted() { // nolint: nestif - if headerValue := c.Get("X-Forwarded-Proto"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_proto", headerValue) - } - - if headerValue := c.Get("X-Forwarded-Host"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_host", headerValue) - } - - if headerValue := c.Get("X-Forwarded-Path"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_path", headerValue) - } - - if headerValue := c.Get("X-Forwarded-Uri"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_uri", headerValue) + switch { + case err != nil: + if len(subject) != 0 { + event.Str("_subject", subject) } - if headerValue := c.Get("X-Forwarded-For"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_x_forwarded_for", headerValue) + event.Err(err).Bool("_access_granted", false) + case accessErr != nil: + if len(subject) != 0 { + event.Str("_subject", subject) } - if headerValue := c.Get("Forwarded"); len(headerValue) != 0 { - logCtx = logCtx.Str("_http_forwarded", headerValue) - } + event.Err(accessErr).Bool("_access_granted", false) + default: + event.Str("_subject", subject).Bool("_access_granted", true) } - return logCtx.Logger() + return event } -func createAccessLogFinalizationEvent( - c *fiber.Ctx, - accessLogger zerolog.Logger, - err error, - start time.Time, - traceCtx *tracecontext.TraceContext, -) *zerolog.Event { - end := time.Now() - duration := end.Sub(start) - subject := accesscontext.Subject(c.UserContext()) - accessErr := accesscontext.Error(c.UserContext()) - - event := accessLogger.Info(). - Int("_body_bytes_sent", len(c.Response().Body())). - Int("_http_status_code", c.Response().StatusCode()). - Int64("_tx_duration_ms", duration.Milliseconds()) - - if traceCtx != nil { - event = event. +func logTraceData(ctx context.Context, logCtx zerolog.Context) zerolog.Context { + if traceCtx := tracecontext.Extract(ctx); traceCtx != nil { + logCtx = logCtx. Str("_trace_id", traceCtx.TraceID). Str("_span_id", traceCtx.SpanID) if len(traceCtx.ParentID) != 0 { - event = event.Str("_parent_id", traceCtx.ParentID) + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) } } - switch { - case err != nil: - if len(subject) != 0 { - event = event.Str("_subject", subject) - } - - event = event.Err(err).Bool("_access_granted", false) - case accessErr != nil: - if len(subject) != 0 { - event = event.Str("_subject", subject) - } + return logCtx +} - event = event.Err(accessErr).Bool("_access_granted", false) - default: - event = event.Str("_subject", subject).Bool("_access_granted", true) +func logHeader(c *fiber.Ctx, logCtx zerolog.Context, headerName, logKey string) zerolog.Context { + if headerValue := c.Get(headerName); len(headerValue) != 0 { + logCtx = logCtx.Str(logKey, headerValue) } - return event + return logCtx } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go index b6f457c72..2133026ac 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go @@ -18,10 +18,13 @@ package accesslog import ( "context" + "strings" "time" "github.com/rs/zerolog" "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" "github.com/dadrus/heimdall/internal/accesscontext" "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" @@ -30,114 +33,82 @@ import ( func New(logger zerolog.Logger) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { start := time.Now() - traceCtx := tracecontext.Extract(ctx) - ctx = accesscontext.New(ctx) + requestMetadata, _ := metadata.FromIncomingContext(ctx) + + logCtx := logger.Level(zerolog.InfoLevel).With(). + Int64("_tx_start", start.Unix()). + Str("_peer", peerFromCtx(ctx)). + Str("_request", info.FullMethod) + + logCtx = logTraceData(ctx, logCtx) + logCtx = md(logCtx, requestMetadata, "x-forwarded-for", "_x_forwarded_for") + logCtx = md(logCtx, requestMetadata, "forwarded", "_forwarded") - accLog := createAccessLogger(logger, start, traceCtx) + accLog := logCtx.Logger() accLog.Info().Msg("TX started") + ctx = accesscontext.New(ctx) res, err := handler(ctx, req) - createAccessLogFinalizationEvent(ctx, accLog, err, start, traceCtx).Msg("TX finished") + logAccessStatus(ctx, accLog.Info(), err). + Int64("_tx_duration_ms", time.Until(start).Milliseconds()). + Msg("TX finished") return res, err } } -func createAccessLogger(logger zerolog.Logger, start time.Time, traceCtx *tracecontext.TraceContext) zerolog.Logger { - startTime := start.Unix() - - logCtx := logger.Level(zerolog.InfoLevel).With(). - Int64("_tx_start", startTime) - // Str("_client_ip", c.IP()). - // Str("_http_method", c.Method()). - // Str("_http_path", c.Path()). - // Str("_http_user_agent", c.Get("User-Agent")). - // Str("_http_host", string(c.Request().URI().Host())). - // Str("_http_scheme", string(c.Request().URI().Scheme())) +func logAccessStatus(ctx context.Context, event *zerolog.Event, err error) *zerolog.Event { + subject := accesscontext.Subject(ctx) + accessErr := accesscontext.Error(ctx) - if traceCtx != nil { - logCtx = logCtx. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) + switch { + case err != nil: + if len(subject) != 0 { + event.Str("_subject", subject) + } - if len(traceCtx.ParentID) != 0 { - logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) + event.Err(err).Bool("_access_granted", false) + case accessErr != nil: + if len(subject) != 0 { + event.Str("_subject", subject) } + + event.Err(accessErr).Bool("_access_granted", false) + default: + event.Str("_subject", subject).Bool("_access_granted", true) } - // if c.IsProxyTrusted() { // nolint: nestif - // if headerValue := c.Get("X-Forwarded-Proto"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_proto", headerValue) - // } - // - // if headerValue := c.Get("X-Forwarded-Host"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_host", headerValue) - // } - // - // if headerValue := c.Get("X-Forwarded-Path"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_path", headerValue) - // } - // - // if headerValue := c.Get("X-Forwarded-Uri"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_uri", headerValue) - // } - // - // if headerValue := c.Get("X-Forwarded-For"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_x_forwarded_for", headerValue) - // } - // - // if headerValue := c.Get("Forwarded"); len(headerValue) != 0 { - // logCtx = logCtx.Str("_http_forwarded", headerValue) - // } - // } - - return logCtx.Logger() + return event } -func createAccessLogFinalizationEvent( - ctx context.Context, - accessLogger zerolog.Logger, - err error, - start time.Time, - traceCtx *tracecontext.TraceContext, -) *zerolog.Event { - end := time.Now() - duration := end.Sub(start) - subject := accesscontext.Subject(ctx) - accessErr := accesscontext.Error(ctx) - - event := accessLogger.Info(). - // Int("_body_bytes_sent", len(c.Response().Body())). - // Int("_http_status_code", c.Response().StatusCode()). - Int64("_tx_duration_ms", duration.Milliseconds()) - - if traceCtx != nil { - event = event. +func logTraceData(ctx context.Context, logCtx zerolog.Context) zerolog.Context { + if traceCtx := tracecontext.Extract(ctx); traceCtx != nil { + logCtx = logCtx. Str("_trace_id", traceCtx.TraceID). Str("_span_id", traceCtx.SpanID) if len(traceCtx.ParentID) != 0 { - event = event.Str("_parent_id", traceCtx.ParentID) + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) } } - switch { - case err != nil: - if len(subject) != 0 { - event = event.Str("_subject", subject) - } + return logCtx +} - event = event.Err(err).Bool("_access_granted", false) - case accessErr != nil: - if len(subject) != 0 { - event = event.Str("_subject", subject) - } +func md(logCtx zerolog.Context, rmd metadata.MD, mdKey, logKey string) zerolog.Context { + if headerValue := rmd.Get(mdKey); len(headerValue) != 0 { + logCtx = logCtx.Str(logKey, strings.Join(headerValue, ",")) + } - event = event.Err(accessErr).Bool("_access_granted", false) - default: - event = event.Str("_subject", subject).Bool("_access_granted", true) + return logCtx +} + +func peerFromCtx(ctx context.Context) string { + p, ok := peer.FromContext(ctx) + if !ok { + return "" } - return event + return p.Addr.String() } From a65c95e38808135dbbb1efea7a51b07ea8ade523 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 2 Feb 2023 14:42:01 +0100 Subject: [PATCH 14/67] further simplifications --- .../middleware/accesslog/accesslog_handler.go | 2 +- .../fiber/middleware/logger/logger_handler.go | 30 +++++++++++-------- .../grpcv3/middleware/logger/handler.go | 23 +++++++------- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/internal/fiber/middleware/accesslog/accesslog_handler.go b/internal/fiber/middleware/accesslog/accesslog_handler.go index 71be20db3..1dfeb32c4 100644 --- a/internal/fiber/middleware/accesslog/accesslog_handler.go +++ b/internal/fiber/middleware/accesslog/accesslog_handler.go @@ -1,4 +1,4 @@ -// Copyright 2022 Dimitrij Drus +// Copyright 2023 Dimitrij Drus // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/fiber/middleware/logger/logger_handler.go b/internal/fiber/middleware/logger/logger_handler.go index 11905ac72..202717495 100644 --- a/internal/fiber/middleware/logger/logger_handler.go +++ b/internal/fiber/middleware/logger/logger_handler.go @@ -1,4 +1,4 @@ -// Copyright 2022 Dimitrij Drus +// Copyright 2023 Dimitrij Drus // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ package logger import ( + "context" + "github.com/gofiber/fiber/v2" "github.com/rs/zerolog" @@ -25,22 +27,24 @@ import ( func New(logger zerolog.Logger) fiber.Handler { return func(c *fiber.Ctx) error { - logCtx := logger.With() ctx := c.UserContext() - traceCtx := tracecontext.Extract(ctx) - if traceCtx != nil { - logCtx = logCtx. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) + c.SetUserContext(withTraceData(ctx, logger.With()).Logger().WithContext(ctx)) - if len(traceCtx.ParentID) != 0 { - logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) - } - } + return c.Next() + } +} - c.SetUserContext(logCtx.Logger().WithContext(ctx)) +func withTraceData(ctx context.Context, logCtx zerolog.Context) zerolog.Context { + if traceCtx := tracecontext.Extract(ctx); traceCtx != nil { + logCtx = logCtx. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) - return c.Next() + if len(traceCtx.ParentID) != 0 { + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) + } } + + return logCtx } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go b/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go index 8b6ea5378..1c0a9fecb 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go @@ -27,19 +27,20 @@ import ( func New(logger zerolog.Logger) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - logCtx := logger.With() - traceCtx := tracecontext.Extract(ctx) + return handler(withTraceData(ctx, logger.With()).Logger().WithContext(ctx), req) + } +} - if traceCtx != nil { - logCtx = logCtx. - Str("_trace_id", traceCtx.TraceID). - Str("_span_id", traceCtx.SpanID) +func withTraceData(ctx context.Context, logCtx zerolog.Context) zerolog.Context { + if traceCtx := tracecontext.Extract(ctx); traceCtx != nil { + logCtx = logCtx. + Str("_trace_id", traceCtx.TraceID). + Str("_span_id", traceCtx.SpanID) - if len(traceCtx.ParentID) != 0 { - logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) - } + if len(traceCtx.ParentID) != 0 { + logCtx = logCtx.Str("_parent_id", traceCtx.ParentID) } - - return handler(logCtx.Logger().WithContext(ctx), req) } + + return logCtx } From 63ebc47b72e3ae58225c619a675afdd018a901ae Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 2 Feb 2023 14:43:49 +0100 Subject: [PATCH 15/67] unused parameters --- internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go b/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go index 360b1f2ff..e34ff43f0 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go @@ -25,7 +25,7 @@ import ( ) func New(cch cache.Cache) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { return handler(cache.WithContext(ctx, cch), req) } } From 8465e2625072d65f34ea22807f5feaa628931a77 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Feb 2023 11:24:14 +0100 Subject: [PATCH 16/67] log message changed --- internal/handler/envoyextauth/grpcv3/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/handler.go b/internal/handler/envoyextauth/grpcv3/handler.go index 357cf953e..90764f4a5 100644 --- a/internal/handler/envoyextauth/grpcv3/handler.go +++ b/internal/handler/envoyextauth/grpcv3/handler.go @@ -34,7 +34,7 @@ type Handler struct { func (h *Handler) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*envoy_auth.CheckResponse, error) { logger := zerolog.Ctx(ctx) - logger.Debug().Msg("Decision Envoy ExtAuth endpoint called") + logger.Debug().Msg("Decision Envoy ExtAuth called") reqCtx := NewRequestContext(ctx, req, h.s) From 9b5d5f0722caf06376ee415565f9b6051aba6f7a Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Feb 2023 11:24:59 +0100 Subject: [PATCH 17/67] grpc error handler middleware implementation enhanced --- .../middleware/errorhandler/defaults.go | 6 +- .../middleware/errorhandler/error_handler.go | 40 ++++--- .../middleware/errorhandler/error_response.go | 102 ++++++++++++++++++ .../grpcv3/middleware/errorhandler/options.go | 75 ++++--------- 4 files changed, 152 insertions(+), 71 deletions(-) create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go index 33f4c4055..ff9643cbf 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go @@ -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 } } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go index 0616a9466..998b59c8e 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go @@ -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,14 +78,17 @@ 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 @@ -92,6 +96,14 @@ func (h *handler) handle( 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 "" +} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go new file mode 100644 index 000000000..585ded564 --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go @@ -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("

%s

", 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 "" +} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go index 43d776a77..cdebe22e4 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go @@ -16,23 +16,15 @@ 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) @@ -40,8 +32,8 @@ 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() -} From 30b7565504e9cd58f052647b9364bd5f60b5cd57 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Wed, 8 Feb 2023 15:49:13 +0100 Subject: [PATCH 18/67] idle timeout configured --- internal/handler/envoyextauth/grpcv3/service.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/service.go b/internal/handler/envoyextauth/grpcv3/service.go index 9cecb546c..0f53279fa 100644 --- a/internal/handler/envoyextauth/grpcv3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -23,6 +23,7 @@ import ( "github.com/rs/zerolog" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" + "google.golang.org/grpc/keepalive" "github.com/dadrus/heimdall/internal/cache" "github.com/dadrus/heimdall/internal/config" @@ -75,7 +76,10 @@ func newService( cachemiddleware.New(cch), ) - srv := grpc.NewServer(grpc.ChainUnaryInterceptor(interceptors...)) + srv := grpc.NewServer( + grpc.KeepaliveParams(keepalive.ServerParameters{Timeout: service.Timeout.Idle}), + grpc.ChainUnaryInterceptor(interceptors...), + ) envoy_auth.RegisterAuthorizationServer(srv, &Handler{r: repository, s: signer}) From 3eb4e2cfb106880b575eb818ac13f3fc9c8d70c2 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Wed, 8 Feb 2023 16:11:06 +0100 Subject: [PATCH 19/67] method renamed --- .../grpcv3/middleware/accesslog/accesslog_handler.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go index 2133026ac..a74b160cc 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go @@ -41,8 +41,8 @@ func New(logger zerolog.Logger) grpc.UnaryServerInterceptor { Str("_request", info.FullMethod) logCtx = logTraceData(ctx, logCtx) - logCtx = md(logCtx, requestMetadata, "x-forwarded-for", "_x_forwarded_for") - logCtx = md(logCtx, requestMetadata, "forwarded", "_forwarded") + logCtx = logMetaData(logCtx, requestMetadata, "x-forwarded-for", "_x_forwarded_for") + logCtx = logMetaData(logCtx, requestMetadata, "forwarded", "_forwarded") accLog := logCtx.Logger() accLog.Info().Msg("TX started") @@ -96,7 +96,7 @@ func logTraceData(ctx context.Context, logCtx zerolog.Context) zerolog.Context { return logCtx } -func md(logCtx zerolog.Context, rmd metadata.MD, mdKey, logKey string) zerolog.Context { +func logMetaData(logCtx zerolog.Context, rmd metadata.MD, mdKey, logKey string) zerolog.Context { if headerValue := rmd.Get(mdKey); len(headerValue) != 0 { logCtx = logCtx.Str(logKey, strings.Join(headerValue, ",")) } From 81ea03c6de11b9b757154f05057ca558b19e7d0a Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Wed, 8 Feb 2023 16:28:47 +0100 Subject: [PATCH 20/67] header canonicalization and some simplifications --- .../envoyextauth/grpcv3/request_context.go | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/request_context.go b/internal/handler/envoyextauth/grpcv3/request_context.go index fb35279e2..4a12c15e7 100644 --- a/internal/handler/envoyextauth/grpcv3/request_context.go +++ b/internal/handler/envoyextauth/grpcv3/request_context.go @@ -34,8 +34,11 @@ import ( type RequestContext struct { ctx context.Context // nolint: containedctx - req *envoy_auth.CheckRequest + reqMethod string + reqHeaders map[string]string reqURL *url.URL + reqBody string + reqRawBody []byte upstreamHeaders http.Header upstreamCookies map[string]string jwtSigner heimdall.JWTSigner @@ -44,8 +47,9 @@ type RequestContext struct { func NewRequestContext(ctx context.Context, req *envoy_auth.CheckRequest, signer heimdall.JWTSigner) *RequestContext { return &RequestContext{ - ctx: ctx, - req: req, + ctx: ctx, + reqMethod: req.Attributes.Request.Http.Method, + reqHeaders: canonicalizeHeaders(req.Attributes.Request.Http.Headers), reqURL: &url.URL{ Scheme: req.Attributes.Request.Http.Scheme, Host: req.Attributes.Request.Http.Host, @@ -53,24 +57,30 @@ func NewRequestContext(ctx context.Context, req *envoy_auth.CheckRequest, signer RawQuery: req.Attributes.Request.Http.Query, Fragment: req.Attributes.Request.Http.Fragment, }, + reqBody: req.Attributes.Request.Http.Body, + reqRawBody: req.Attributes.Request.Http.RawBody, jwtSigner: signer, upstreamHeaders: make(http.Header), upstreamCookies: make(map[string]string), } } -func (s *RequestContext) RequestMethod() string { return s.req.Attributes.Request.Http.Method } +func canonicalizeHeaders(headers map[string]string) map[string]string { + result := make(map[string]string, len(headers)) -func (s *RequestContext) RequestHeaders() map[string]string { - return s.req.Attributes.Request.Http.Headers -} + for key, value := range headers { + result[http.CanonicalHeaderKey(key)] = value + } -func (s *RequestContext) RequestHeader(name string) string { - return s.req.Attributes.Request.Http.Headers[name] + return result } +func (s *RequestContext) RequestMethod() string { return s.reqMethod } +func (s *RequestContext) RequestHeaders() map[string]string { return s.reqHeaders } +func (s *RequestContext) RequestHeader(name string) string { return s.reqHeaders[name] } + func (s *RequestContext) RequestCookie(name string) string { - values, ok := s.req.Attributes.Request.Http.Headers["cookie"] + values, ok := s.reqHeaders["Cookie"] if !ok { return "" } @@ -89,11 +99,11 @@ func (s *RequestContext) RequestQueryParameter(name string) string { } func (s *RequestContext) RequestFormParameter(name string) string { - if s.req.Attributes.Request.Http.Headers["content-type"] != "application/x-www-form-urlencoded" { + if s.reqHeaders["Content-Type"] != "application/x-www-form-urlencoded" { return "" } - values, err := url.ParseQuery(s.req.Attributes.Request.Http.Body) + values, err := url.ParseQuery(s.reqBody) if err != nil { return "" } @@ -101,7 +111,7 @@ func (s *RequestContext) RequestFormParameter(name string) string { return values.Get(name) } -func (s *RequestContext) RequestBody() []byte { return s.req.Attributes.Request.Http.RawBody } +func (s *RequestContext) RequestBody() []byte { return s.reqRawBody } func (s *RequestContext) AppContext() context.Context { return s.ctx } func (s *RequestContext) SetPipelineError(err error) { s.err = err } func (s *RequestContext) AddHeaderForUpstream(name, value string) { s.upstreamHeaders.Add(name, value) } From fa361791afabe6d7f2bfdde5a9b37d0fc0fd695f Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 9 Feb 2023 08:58:45 +0100 Subject: [PATCH 21/67] tests for grpc access log interceptor --- .../accesslog/accesslog_handler_test.go | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler_test.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler_test.go new file mode 100644 index 000000000..54d12eb3a --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler_test.go @@ -0,0 +1,287 @@ +package accesslog + +import ( + "context" + "fmt" + "net" + "net/http" + "strings" + "testing" + + 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" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/test/bufconn" + + "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/x/testsupport" +) + +type mockHandler struct { + mock.Mock +} + +func (m *mockHandler) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*envoy_auth.CheckResponse, error) { + args := m.Called(ctx, req) + + if val := args.Get(0); val != nil { + // nolint: forcetypeassert + return val.(*envoy_auth.CheckResponse), nil + } + + return nil, args.Error(1) +} + +func TestAccessLogInterceptor(t *testing.T) { + otel.SetTracerProvider(sdktrace.NewTracerProvider()) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) + + parentCtx := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{1}, SpanID: trace.SpanID{2}, TraceFlags: trace.FlagsSampled, + }) + + for _, tc := range []struct { + uc string + outgoingContext func(t *testing.T) context.Context + configureMock func(t *testing.T, m *mockHandler) + assert func(t *testing.T, logEvent1, logEvent2 map[string]any) + }{ + { + uc: "without tracing, x-* header and errors", + outgoingContext: func(t *testing.T) context.Context { + t.Helper() + + return context.Background() + }, + configureMock: func(t *testing.T, m *mockHandler) { + t.Helper() + + m.On("Check", + mock.MatchedBy( + func(ctx context.Context) bool { + accesscontext.SetSubject(ctx, "foo") + + return true + }, + ), + mock.Anything, + ).Return( + &envoy_auth.CheckResponse{Status: &status.Status{Code: int32(envoy_type.StatusCode_OK)}}, + nil, + ) + }, + assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { + t.Helper() + + require.Len(t, logEvent1, 7) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_peer") + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", logEvent1["_request"]) + assert.Contains(t, logEvent1, "_trace_id") + assert.Contains(t, logEvent1, "_trace_id") + assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) + assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) + assert.Equal(t, "TX started", logEvent1["message"]) + + require.Len(t, logEvent2, 10) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_peer") + assert.Equal(t, logEvent1["_request"], logEvent2["_request"]) + assert.Contains(t, logEvent2, "_trace_id") + assert.Contains(t, logEvent2, "_trace_id") + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Equal(t, true, logEvent2["_access_granted"]) + assert.Equal(t, "foo", logEvent2["_subject"]) + assert.Equal(t, "TX finished", logEvent2["message"]) + }, + }, + { + uc: "with tracing, x-* header and error", + outgoingContext: func(t *testing.T) context.Context { + t.Helper() + + md := map[string]string{ + "x-forwarded-for": "127.0.0.1", + "forwarded": "for=127.0.0.1", + } + + otel.GetTextMapPropagator().Inject( + trace.ContextWithRemoteSpanContext(context.Background(), parentCtx), + propagation.MapCarrier(md)) + + return metadata.NewOutgoingContext(context.Background(), metadata.New(md)) + }, + configureMock: func(t *testing.T, m *mockHandler) { + t.Helper() + + m.On("Check", mock.Anything, mock.Anything). + Return(nil, fmt.Errorf("test error")) // nolint: goerr113 + }, + assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { + t.Helper() + + require.Len(t, logEvent1, 10) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_peer") + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", logEvent1["_request"]) + assert.Contains(t, logEvent1, "_span_id") + assert.Equal(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) + assert.Equal(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) + assert.Equal(t, "for=127.0.0.1", logEvent1["_forwarded"]) + assert.Equal(t, "127.0.0.1", logEvent1["_x_forwarded_for"]) + assert.Equal(t, "TX started", logEvent1["message"]) + + require.Len(t, logEvent2, 13) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_peer") + assert.Equal(t, logEvent1["_request"], logEvent2["_request"]) + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Equal(t, logEvent2["_span_id"], logEvent2["_span_id"]) + assert.Equal(t, false, logEvent2["_access_granted"]) + assert.Equal(t, "test error", logEvent2["error"]) + assert.Equal(t, "for=127.0.0.1", logEvent1["_forwarded"]) + assert.Equal(t, "127.0.0.1", logEvent1["_x_forwarded_for"]) + assert.Equal(t, "TX finished", logEvent2["message"]) + }, + }, + { + uc: "without tracing and x-* header, but with subject and error set on context", + outgoingContext: func(t *testing.T) context.Context { + t.Helper() + + return context.Background() + }, + configureMock: func(t *testing.T, m *mockHandler) { + t.Helper() + + m.On("Check", + mock.MatchedBy( + func(ctx context.Context) bool { + accesscontext.SetSubject(ctx, "bar") + accesscontext.SetError(ctx, fmt.Errorf("test error")) // nolint: goerr113 + + return true + }, + ), + mock.Anything, + ).Return( + &envoy_auth.CheckResponse{Status: &status.Status{Code: int32(envoy_type.StatusCode_Forbidden)}}, + nil, + ) + }, + assert: func(t *testing.T, logEvent1, logEvent2 map[string]any) { + t.Helper() + + require.Len(t, logEvent1, 7) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_peer") + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", logEvent1["_request"]) + assert.Contains(t, logEvent1, "_trace_id") + assert.Contains(t, logEvent1, "_trace_id") + assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) + assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) + assert.Equal(t, "TX started", logEvent1["message"]) + + require.Len(t, logEvent2, 11) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_peer") + assert.Equal(t, logEvent1["_request"], logEvent2["_request"]) + assert.Contains(t, logEvent2, "_trace_id") + assert.Contains(t, logEvent2, "_trace_id") + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Equal(t, false, logEvent2["_access_granted"]) + assert.Equal(t, "bar", logEvent2["_subject"]) + assert.Equal(t, "test error", logEvent2["error"]) + assert.Equal(t, "TX finished", logEvent2["message"]) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + var ( + logLine1 map[string]any + logLine2 map[string]any + ) + + lis := bufconn.Listen(1024 * 1024) + tb := &testsupport.TestingLog{TB: t} + logger := zerolog.New(zerolog.TestWriter{T: tb}) + handler := &mockHandler{} + bufDialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + conn, err := grpc.DialContext(context.Background(), "bufnet", + grpc.WithContextDialer(bufDialer), + grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + defer conn.Close() + + tc.configureMock(t, handler) + + srv := grpc.NewServer( + grpc.ChainUnaryInterceptor( + otelgrpc.UnaryServerInterceptor(), + New(logger), + ), + ) + envoy_auth.RegisterAuthorizationServer(srv, handler) + + go func() { + err = srv.Serve(lis) + require.NoError(t, err) + }() + + client := envoy_auth.NewAuthorizationClient(conn) + + // WHEN + client.Check(tc.outgoingContext(t), &envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Request: &envoy_auth.AttributeContext_Request{ + Http: &envoy_auth.AttributeContext_HttpRequest{ + Body: "foo", + Method: http.MethodPost, + Path: "/foobar", + }, + }, + }, + }) + + // THEN + srv.Stop() + + events := strings.Split(tb.CollectedLog(), "}") + require.Len(t, events, 3) + + require.NoError(t, json.Unmarshal([]byte(events[0]+"}"), &logLine1)) + require.NoError(t, json.Unmarshal([]byte(events[1]+"}"), &logLine2)) + + tc.assert(t, logLine1, logLine2) + handler.AssertExpectations(t) + }) + } +} From 45cc2e0d7798de911fb3b026952403c0fb5c7a67 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 9 Feb 2023 09:12:46 +0100 Subject: [PATCH 22/67] some renamings & more tests --- .../{accesslog_handler.go => interceptor.go} | 0 ...og_handler_test.go => interceptor_test.go} | 26 +-- .../cache/{cache.go => interceptor.go} | 0 .../{error_handler.go => interceptor.go} | 0 .../logger/{handler.go => interceptor.go} | 0 .../middleware/logger/interceptor_test.go | 170 ++++++++++++++++++ .../prometheus/{handler.go => interceptor.go} | 0 .../envoyextauth/grpcv3/mocks/handler.go | 23 +++ 8 files changed, 199 insertions(+), 20 deletions(-) rename internal/handler/envoyextauth/grpcv3/middleware/accesslog/{accesslog_handler.go => interceptor.go} (100%) rename internal/handler/envoyextauth/grpcv3/middleware/accesslog/{accesslog_handler_test.go => interceptor_test.go} (93%) rename internal/handler/envoyextauth/grpcv3/middleware/cache/{cache.go => interceptor.go} (100%) rename internal/handler/envoyextauth/grpcv3/middleware/errorhandler/{error_handler.go => interceptor.go} (100%) rename internal/handler/envoyextauth/grpcv3/middleware/logger/{handler.go => interceptor.go} (100%) create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go rename internal/handler/envoyextauth/grpcv3/middleware/prometheus/{handler.go => interceptor.go} (100%) create mode 100644 internal/handler/envoyextauth/grpcv3/mocks/handler.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go similarity index 100% rename from internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler.go rename to internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go similarity index 93% rename from internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler_test.go rename to internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go index 54d12eb3a..40bdfef1f 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/accesslog_handler_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go @@ -27,24 +27,10 @@ import ( "google.golang.org/grpc/test/bufconn" "github.com/dadrus/heimdall/internal/accesscontext" + "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" "github.com/dadrus/heimdall/internal/x/testsupport" ) -type mockHandler struct { - mock.Mock -} - -func (m *mockHandler) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*envoy_auth.CheckResponse, error) { - args := m.Called(ctx, req) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(*envoy_auth.CheckResponse), nil - } - - return nil, args.Error(1) -} - func TestAccessLogInterceptor(t *testing.T) { otel.SetTracerProvider(sdktrace.NewTracerProvider()) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) @@ -56,7 +42,7 @@ func TestAccessLogInterceptor(t *testing.T) { for _, tc := range []struct { uc string outgoingContext func(t *testing.T) context.Context - configureMock func(t *testing.T, m *mockHandler) + configureMock func(t *testing.T, m *mocks.MockHandler) assert func(t *testing.T, logEvent1, logEvent2 map[string]any) }{ { @@ -66,7 +52,7 @@ func TestAccessLogInterceptor(t *testing.T) { return context.Background() }, - configureMock: func(t *testing.T, m *mockHandler) { + configureMock: func(t *testing.T, m *mocks.MockHandler) { t.Helper() m.On("Check", @@ -128,7 +114,7 @@ func TestAccessLogInterceptor(t *testing.T) { return metadata.NewOutgoingContext(context.Background(), metadata.New(md)) }, - configureMock: func(t *testing.T, m *mockHandler) { + configureMock: func(t *testing.T, m *mocks.MockHandler) { t.Helper() m.On("Check", mock.Anything, mock.Anything). @@ -172,7 +158,7 @@ func TestAccessLogInterceptor(t *testing.T) { return context.Background() }, - configureMock: func(t *testing.T, m *mockHandler) { + configureMock: func(t *testing.T, m *mocks.MockHandler) { t.Helper() m.On("Check", @@ -230,7 +216,7 @@ func TestAccessLogInterceptor(t *testing.T) { lis := bufconn.Listen(1024 * 1024) tb := &testsupport.TestingLog{TB: t} logger := zerolog.New(zerolog.TestWriter{T: tb}) - handler := &mockHandler{} + handler := &mocks.MockHandler{} bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go b/internal/handler/envoyextauth/grpcv3/middleware/cache/interceptor.go similarity index 100% rename from internal/handler/envoyextauth/grpcv3/middleware/cache/cache.go rename to internal/handler/envoyextauth/grpcv3/middleware/cache/interceptor.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go similarity index 100% rename from internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_handler.go rename to internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go b/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor.go similarity index 100% rename from internal/handler/envoyextauth/grpcv3/middleware/logger/handler.go rename to internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go new file mode 100644 index 000000000..0551f91db --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go @@ -0,0 +1,170 @@ +// Copyright 2022 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package logger + +import ( + "context" + "fmt" + "net" + "net/http" + "testing" + + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/goccy/go-json" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/test/bufconn" + + "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" + "github.com/dadrus/heimdall/internal/x/testsupport" +) + +func TestLoggerInterceptor(t *testing.T) { + // GIVEN + otel.SetTracerProvider(sdktrace.NewTracerProvider()) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) + + parentCtx := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{1}, SpanID: trace.SpanID{2}, TraceFlags: trace.FlagsSampled, + }) + + for _, tc := range []struct { + uc string + outgoingContext func(t *testing.T) context.Context + assert func(t *testing.T, logstring string) + }{ + { + uc: "without tracing", + outgoingContext: func(t *testing.T) context.Context { + t.Helper() + + return context.Background() + }, + assert: func(t *testing.T, logstring string) { + t.Helper() + + assert.Contains(t, logstring, "test called") + assert.Contains(t, logstring, "_span_id") + assert.Contains(t, logstring, "_trace_id") + assert.NotContains(t, logstring, "_parent_id") + + var logData map[string]string + require.NoError(t, json.Unmarshal([]byte(logstring), &logData)) + assert.NotEqual(t, parentCtx.TraceID().String(), logData["_trace_id"]) + assert.NotEqual(t, parentCtx.SpanID().String(), logData["_parent_id"]) + }, + }, + { + uc: "with tracing", + outgoingContext: func(t *testing.T) context.Context { + t.Helper() + + md := map[string]string{} + + otel.GetTextMapPropagator().Inject( + trace.ContextWithRemoteSpanContext(context.Background(), parentCtx), + propagation.MapCarrier(md)) + + return metadata.NewOutgoingContext(context.Background(), metadata.New(md)) + }, + assert: func(t *testing.T, logstring string) { + t.Helper() + + assert.Contains(t, logstring, "test called") + assert.Contains(t, logstring, "_span_id") + assert.Contains(t, logstring, "_trace_id") + assert.Contains(t, logstring, "_parent_id") + + var logData map[string]string + require.NoError(t, json.Unmarshal([]byte(logstring), &logData)) + assert.Equal(t, parentCtx.TraceID().String(), logData["_trace_id"]) + assert.Equal(t, parentCtx.SpanID().String(), logData["_parent_id"]) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + lis := bufconn.Listen(1024 * 1024) + tb := &testsupport.TestingLog{TB: t} + logger := zerolog.New(zerolog.TestWriter{T: tb}) + handler := &mocks.MockHandler{} + bufDialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + conn, err := grpc.DialContext(context.Background(), "bufnet", + grpc.WithContextDialer(bufDialer), + grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + defer conn.Close() + + handler.On("Check", mock.MatchedBy( + func(ctx context.Context) bool { + zerolog.Ctx(ctx).Info().Msg("test called") + + return true + }, + ), mock.Anything). + Return(nil, fmt.Errorf("test error")) // nolint: goerr113 + + srv := grpc.NewServer( + grpc.ChainUnaryInterceptor( + otelgrpc.UnaryServerInterceptor(), + New(logger), + ), + ) + envoy_auth.RegisterAuthorizationServer(srv, handler) + + go func() { + err = srv.Serve(lis) + require.NoError(t, err) + }() + + client := envoy_auth.NewAuthorizationClient(conn) + + // WHEN + client.Check(tc.outgoingContext(t), &envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Request: &envoy_auth.AttributeContext_Request{ + Http: &envoy_auth.AttributeContext_HttpRequest{ + Body: "foo", + Method: http.MethodPost, + Path: "/foobar", + }, + }, + }, + }) + + // THEN + srv.Stop() + + // THEN + require.NoError(t, err) + tc.assert(t, tb.CollectedLog()) + }) + } +} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go similarity index 100% rename from internal/handler/envoyextauth/grpcv3/middleware/prometheus/handler.go rename to internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go diff --git a/internal/handler/envoyextauth/grpcv3/mocks/handler.go b/internal/handler/envoyextauth/grpcv3/mocks/handler.go new file mode 100644 index 000000000..bfee9487f --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/mocks/handler.go @@ -0,0 +1,23 @@ +package mocks + +import ( + "context" + + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/stretchr/testify/mock" +) + +type MockHandler struct { + mock.Mock +} + +func (m *MockHandler) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*envoy_auth.CheckResponse, error) { + args := m.Called(ctx, req) + + if val := args.Get(0); val != nil { + // nolint: forcetypeassert + return val.(*envoy_auth.CheckResponse), nil + } + + return nil, args.Error(1) +} From d103adb4b7bd96bd3cc27061140c634c2e051ed9 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 9 Feb 2023 09:21:21 +0100 Subject: [PATCH 23/67] linter warnings disabled for specific places as errors are irrelevant --- .../envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go | 1 + .../envoyextauth/grpcv3/middleware/logger/interceptor_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go index 40bdfef1f..87840da42 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go @@ -245,6 +245,7 @@ func TestAccessLogInterceptor(t *testing.T) { client := envoy_auth.NewAuthorizationClient(conn) // WHEN + // nolint: errcheck client.Check(tc.outgoingContext(t), &envoy_auth.CheckRequest{ Attributes: &envoy_auth.AttributeContext{ Request: &envoy_auth.AttributeContext_Request{ diff --git a/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go index 0551f91db..c48c96918 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go @@ -147,6 +147,7 @@ func TestLoggerInterceptor(t *testing.T) { client := envoy_auth.NewAuthorizationClient(conn) // WHEN + // nolint: errcheck client.Check(tc.outgoingContext(t), &envoy_auth.CheckRequest{ Attributes: &envoy_auth.AttributeContext{ Request: &envoy_auth.AttributeContext_Request{ From 2facb5f397966dc2502b0c5af4489c3544ed2637 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 9 Feb 2023 10:41:41 +0100 Subject: [PATCH 24/67] small refactorings & simplifications --- .../middleware/errorhandler/defaults.go | 6 --- .../middleware/errorhandler/error_response.go | 34 +++++++-------- .../grpcv3/middleware/errorhandler/options.go | 42 +++++++------------ 3 files changed, 29 insertions(+), 53 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go index ff9643cbf..f45143d7f 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go @@ -29,9 +29,3 @@ var defaultOptions = opts{ //nolint:gochecknoglobals noRuleError: responseWith(http.StatusNotFound), internalError: responseWith(http.StatusInternalServerError), } - -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 - } -} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go index 585ded564..1519911fb 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go @@ -12,33 +12,29 @@ import ( "google.golang.org/genproto/googleapis/rpc/status" ) +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 + } +} + func errorResponse(code int, err error, verbose bool, mimeType string) *envoy_auth.CheckResponse { + deniedResponse := &envoy_auth.DeniedHttpResponse{ + Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode(code)}, + } + 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, - }, - }, + deniedResponse.Headers = []*envoy_core.HeaderValueOption{ + {Header: &envoy_core.HeaderValue{Key: "Content-Type", Value: responseType}}, } + deniedResponse.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)}, - }, - }, + Status: &status.Status{Code: int32(code)}, + HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{DeniedResponse: deniedResponse}, } } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go index cdebe22e4..400214125 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go @@ -31,70 +31,56 @@ type Option func(*opts) func WithPreconditionErrorCode(code int) Option { return func(o *opts) { - if code != 0 { - o.preconditionError = func(err error, verbose bool, mimeType string) (any, error) { - return errorResponse(code, err, verbose, mimeType), nil - } + if code > 0 { + o.preconditionError = responseWith(code) } } } func WithAuthenticationErrorCode(code int) Option { return func(o *opts) { - if code != 0 { - o.authenticationError = func(err error, verbose bool, mimeType string) (any, error) { - return errorResponse(code, err, verbose, mimeType), nil - } + if code > 0 { + o.authenticationError = responseWith(code) } } } func WithAuthorizationErrorCode(code int) Option { return func(o *opts) { - if code != 0 { - o.authorizationError = func(err error, verbose bool, mimeType string) (any, error) { - return errorResponse(code, err, verbose, mimeType), nil - } + if code > 0 { + o.authorizationError = responseWith(code) } } } func WithCommunicationErrorCode(code int) Option { return func(o *opts) { - if code != 0 { - o.communicationError = func(err error, verbose bool, mimeType string) (any, error) { - return errorResponse(code, err, verbose, mimeType), nil - } + if code > 0 { + o.communicationError = responseWith(code) } } } func WithInternalServerErrorCode(code int) Option { return func(o *opts) { - if code != 0 { - o.internalError = func(err error, verbose bool, mimeType string) (any, error) { - return errorResponse(code, err, verbose, mimeType), nil - } + if code > 0 { + o.internalError = responseWith(code) } } } func WithMethodErrorCode(code int) Option { return func(o *opts) { - if code != 0 { - o.badMethodError = func(err error, verbose bool, mimeType string) (any, error) { - return errorResponse(code, err, verbose, mimeType), nil - } + if code > 0 { + o.badMethodError = responseWith(code) } } } func WithNoRuleErrorCode(code int) Option { return func(o *opts) { - if code != 0 { - o.noRuleError = func(err error, verbose bool, mimeType string) (any, error) { - return errorResponse(code, err, verbose, mimeType), nil - } + if code > 0 { + o.noRuleError = responseWith(code) } } } From 134f418c7785e0a1b3fab6176a8e569fa19d72e6 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 9 Feb 2023 11:11:25 +0100 Subject: [PATCH 25/67] some renaming and more tests --- .../middleware/errorhandler/interceptor.go | 8 +- .../errorhandler/interceptor_test.go | 280 ++++++++++++++++++ 2 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go index 998b59c8e..0d0a1d6cb 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go @@ -38,16 +38,16 @@ func New(opts ...Option) grpc.UnaryServerInterceptor { opt(&options) } - h := &handler{opts: options} + h := &interceptor{opts: options} - return h.handle + return h.intercept } -type handler struct { +type interceptor struct { opts } -func (h *handler) handle( +func (h *interceptor) intercept( ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (any, error) { //nolint:cyclop res, err := handler(ctx, req) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go new file mode 100644 index 000000000..1b82fea7c --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go @@ -0,0 +1,280 @@ +// Copyright 2022 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package errorhandler + +import ( + "context" + "net" + "net/http" + "testing" + + 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/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + + "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" + "github.com/dadrus/heimdall/internal/heimdall" +) + +func TestErrorInterceptor(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + interceptor grpc.UnaryServerInterceptor + err error + expCode envoy_type.StatusCode + expBody string + }{ + { + uc: "no error", + interceptor: New(), + expCode: http.StatusOK, + }, + { + uc: "authentication error default", + interceptor: New(), + err: heimdall.ErrAuthentication, + expCode: http.StatusUnauthorized, + }, + { + uc: "authentication error overridden", + interceptor: New(WithAuthenticationErrorCode(http.StatusContinue)), + err: heimdall.ErrAuthentication, + expCode: http.StatusContinue, + }, + { + uc: "authentication error verbose", + interceptor: New(WithVerboseErrors(true)), + err: heimdall.ErrAuthentication, + expCode: http.StatusUnauthorized, + expBody: "

authentication error

", + }, + { + uc: "authorization error default", + interceptor: New(), + err: heimdall.ErrAuthorization, + expCode: http.StatusForbidden, + }, + { + uc: "authorization error overridden", + interceptor: New(WithAuthorizationErrorCode(http.StatusContinue)), + err: heimdall.ErrAuthorization, + expCode: http.StatusContinue, + }, + { + uc: "authorization error verbose", + interceptor: New(WithVerboseErrors(true)), + err: heimdall.ErrAuthorization, + expCode: http.StatusForbidden, + expBody: "

authorization error

", + }, + { + uc: "communication timeout error default", + interceptor: New(), + err: heimdall.ErrCommunicationTimeout, + expCode: http.StatusBadGateway, + }, + { + uc: "communication timeout error overridden", + interceptor: New(WithCommunicationErrorCode(http.StatusContinue)), + err: heimdall.ErrCommunicationTimeout, + expCode: http.StatusContinue, + }, + { + uc: "communication timeout error verbose", + interceptor: New(WithVerboseErrors(true)), + err: heimdall.ErrCommunicationTimeout, + expCode: http.StatusBadGateway, + expBody: "

communication timeout error

", + }, + { + uc: "communication error default", + interceptor: New(), + err: heimdall.ErrCommunication, + expCode: http.StatusBadGateway, + }, + { + uc: "communication error overridden", + interceptor: New(WithCommunicationErrorCode(http.StatusContinue)), + err: heimdall.ErrCommunication, + expCode: http.StatusContinue, + }, + { + uc: "communication error verbose", + interceptor: New(WithVerboseErrors(true)), + err: heimdall.ErrCommunication, + expCode: http.StatusBadGateway, + expBody: "

communication error

", + }, + { + uc: "precondition error default", + interceptor: New(), + err: heimdall.ErrArgument, + expCode: http.StatusBadRequest, + }, + { + uc: "precondition error overridden", + interceptor: New(WithPreconditionErrorCode(http.StatusContinue)), + err: heimdall.ErrArgument, + expCode: http.StatusContinue, + }, + { + uc: "precondition error verbose", + interceptor: New(WithVerboseErrors(true)), + err: heimdall.ErrArgument, + expCode: http.StatusBadRequest, + expBody: "

argument error

", + }, + { + uc: "method error default", + interceptor: New(), + err: heimdall.ErrMethodNotAllowed, + expCode: http.StatusMethodNotAllowed, + }, + { + uc: "method error overridden", + interceptor: New(WithMethodErrorCode(http.StatusContinue)), + err: heimdall.ErrMethodNotAllowed, + expCode: http.StatusContinue, + }, + { + uc: "method error verbose", + interceptor: New(WithVerboseErrors(true)), + err: heimdall.ErrMethodNotAllowed, + expCode: http.StatusMethodNotAllowed, + expBody: "

method not allowed

", + }, + { + uc: "no rule error default", + interceptor: New(), + err: heimdall.ErrNoRuleFound, + expCode: http.StatusNotFound, + }, + { + uc: "no rule error overridden", + interceptor: New(WithNoRuleErrorCode(http.StatusContinue)), + err: heimdall.ErrNoRuleFound, + expCode: http.StatusContinue, + }, + { + uc: "no rule error verbose", + interceptor: New(WithVerboseErrors(true)), + err: heimdall.ErrNoRuleFound, + expCode: http.StatusNotFound, + expBody: "

no rule found

", + }, + { + uc: "redirect error", + interceptor: New(), + err: &heimdall.RedirectError{RedirectTo: "http://foo.local", Code: http.StatusFound}, + expCode: http.StatusFound, + }, + { + uc: "redirect error verbose", + interceptor: New(WithVerboseErrors(true)), + err: &heimdall.RedirectError{RedirectTo: "http://foo.local", Code: http.StatusFound}, + expCode: http.StatusFound, + }, + { + uc: "internal error default", + interceptor: New(), + err: heimdall.ErrInternal, + expCode: http.StatusInternalServerError, + }, + { + uc: "internal error overridden", + interceptor: New(WithInternalServerErrorCode(http.StatusContinue)), + err: heimdall.ErrInternal, + expCode: http.StatusContinue, + }, + { + uc: "internal error verbose", + interceptor: New(WithVerboseErrors(true)), + err: heimdall.ErrInternal, + expCode: http.StatusInternalServerError, + expBody: "

internal error

", + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + lis := bufconn.Listen(1024 * 1024) + handler := &mocks.MockHandler{} + bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } + conn, err := grpc.DialContext(context.Background(), "bufnet", + grpc.WithContextDialer(bufDialer), + grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + defer conn.Close() + + if tc.err != nil { + handler.On("Check", mock.Anything, mock.Anything).Return(nil, tc.err) + } else { + handler.On("Check", mock.Anything, mock.Anything).Return(&envoy_auth.CheckResponse{ + Status: &status.Status{Code: int32(envoy_type.StatusCode_OK)}, + HttpResponse: &envoy_auth.CheckResponse_OkResponse{ + OkResponse: &envoy_auth.OkHttpResponse{}, + }, + }, nil) + } + + srv := grpc.NewServer(grpc.UnaryInterceptor(tc.interceptor)) + envoy_auth.RegisterAuthorizationServer(srv, handler) + + go func() { + err = srv.Serve(lis) + require.NoError(t, err) + }() + + client := envoy_auth.NewAuthorizationClient(conn) + + // WHEN + // nolint: errcheck + resp, err := client.Check(context.Background(), &envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Request: &envoy_auth.AttributeContext_Request{ + Http: &envoy_auth.AttributeContext_HttpRequest{ + Body: "foo", + Method: http.MethodPost, + Path: "/foobar", + }, + }, + }, + }) + + // THEN + srv.Stop() + require.NoError(t, err) + + assert.Equal(t, int32(tc.expCode), resp.Status.Code) + + if tc.err != nil { + deniedResp := resp.GetDeniedResponse() + require.NotNil(t, deniedResp) + assert.Equal(t, tc.expCode, deniedResp.Status.Code) + assert.Equal(t, tc.expBody, deniedResp.Body) + } + }) + } +} From 12d9fb41f5f15e094d338587204c6cc2179dfb7e Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 9 Feb 2023 11:14:36 +0100 Subject: [PATCH 26/67] functionality related to format negotiation on verbose errors moved to a separate file --- .../middleware/errorhandler/error_response.go | 64 ----------------- .../errorhandler/format_negotiator.go | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/errorhandler/format_negotiator.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go index 1519911fb..65fad5ebe 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go @@ -1,14 +1,9 @@ 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" ) @@ -37,62 +32,3 @@ func errorResponse(code int, err error, verbose bool, mimeType string) *envoy_au HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{DeniedResponse: deniedResponse}, } } - -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("

%s

", 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 "" -} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/format_negotiator.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/format_negotiator.go new file mode 100644 index 000000000..0fc5e1a74 --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/format_negotiator.go @@ -0,0 +1,68 @@ +package errorhandler + +import ( + "encoding/xml" + "fmt" + "strings" + + "github.com/goccy/go-json" +) + +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("

%s

", 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 "" +} From 66dffebbe2206b8cc525839e22b4d3a2235f9f0d Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 9 Feb 2023 11:15:48 +0100 Subject: [PATCH 27/67] comment added --- .../envoyextauth/grpcv3/middleware/errorhandler/interceptor.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go index 0d0a1d6cb..6c58dace0 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go @@ -105,5 +105,6 @@ func acceptType(req any) string { return req.Attributes.Request.Http.Headers["accept"] } + // This should never happen as the API is typed return "" } From 9fb141d4ffffade97e3f48057b9d5e705d989703 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 9 Feb 2023 11:16:31 +0100 Subject: [PATCH 28/67] linter enabled again --- .../envoyextauth/grpcv3/middleware/errorhandler/interceptor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go index 6c58dace0..76b71d6ed 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go @@ -49,7 +49,7 @@ type interceptor struct { func (h *interceptor) intercept( ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler, -) (any, error) { //nolint:cyclop +) (any, error) { res, err := handler(ctx, req) if err == nil { return res, nil From 485891b57bbee316647ef049c73e31b56f1ea5fe Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 9 Feb 2023 11:18:02 +0100 Subject: [PATCH 29/67] file renamed --- .../{format_negotiator.go => content_type_negotiator.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/handler/envoyextauth/grpcv3/middleware/errorhandler/{format_negotiator.go => content_type_negotiator.go} (100%) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/format_negotiator.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/content_type_negotiator.go similarity index 100% rename from internal/handler/envoyextauth/grpcv3/middleware/errorhandler/format_negotiator.go rename to internal/handler/envoyextauth/grpcv3/middleware/errorhandler/content_type_negotiator.go From 6472ea49a9274ce7d10f96f0c1c481b9b626b810 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 10 Feb 2023 10:33:26 +0100 Subject: [PATCH 30/67] error content negotiation simplified & more tests --- .../errorhandler/content_type_negotiator.go | 68 --------------- .../middleware/errorhandler/error_response.go | 43 +++++++++- .../errorhandler/error_response_test.go | 83 +++++++++++++++++++ 3 files changed, 123 insertions(+), 71 deletions(-) delete mode 100644 internal/handler/envoyextauth/grpcv3/middleware/errorhandler/content_type_negotiator.go create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/content_type_negotiator.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/content_type_negotiator.go deleted file mode 100644 index 0fc5e1a74..000000000 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/content_type_negotiator.go +++ /dev/null @@ -1,68 +0,0 @@ -package errorhandler - -import ( - "encoding/xml" - "fmt" - "strings" - - "github.com/goccy/go-json" -) - -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("

%s

", 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 "" -} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go index 65fad5ebe..de731bc57 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go @@ -1,9 +1,14 @@ package errorhandler import ( + "encoding/xml" + "fmt" + + "github.com/elnormous/contenttype" 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" ) @@ -13,16 +18,29 @@ func responseWith(code int) func(err error, verbose bool, mimeType string) (any, } } -func errorResponse(code int, err error, verbose bool, mimeType string) *envoy_auth.CheckResponse { +func errorResponse(code int, decErr error, verbose bool, mimeType string) *envoy_auth.CheckResponse { deniedResponse := &envoy_auth.DeniedHttpResponse{ Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode(code)}, } if verbose { - body, responseType, _ := format(mimeType, err) + contentType := "text/html" + + mt, _, err := contenttype.GetAcceptableMediaTypeFromHeader( + mimeType, []contenttype.MediaType{ + {Type: "application", Subtype: "json"}, + {Type: "application", Subtype: "xml"}, + {Type: "text", Subtype: "html"}, + {Type: "text", Subtype: "plain"}, + }) + if err == nil { + contentType = mt.MIME() + } + + body, _ := format(contentType, decErr) deniedResponse.Headers = []*envoy_core.HeaderValueOption{ - {Header: &envoy_core.HeaderValue{Key: "Content-Type", Value: responseType}}, + {Header: &envoy_core.HeaderValue{Key: "Content-Type", Value: contentType}}, } deniedResponse.Body = body } @@ -32,3 +50,22 @@ func errorResponse(code int, err error, verbose bool, mimeType string) *envoy_au HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{DeniedResponse: deniedResponse}, } } + +func format(mimeType string, body any) (string, error) { + switch mimeType { + case "text/html": + return fmt.Sprintf("

%s

", body), nil + case "application/json": + res, err := json.Marshal(body) + + return string(res), err + case "application/xml": + res, err := xml.Marshal(body) + + return string(res), err + case "test/plain": + fallthrough + default: + return fmt.Sprintf("%s", body), nil + } +} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go new file mode 100644 index 000000000..a6e836aa4 --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go @@ -0,0 +1,83 @@ +package errorhandler + +import ( + "net/http" + "testing" + + envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/x/errorchain" +) + +func TestErrorResponse(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + code int + err error + offeredType string + expectedType string + expBody string + }{ + { + uc: "select text/plain from multiple offered", + code: http.StatusForbidden, + err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), + offeredType: "application/json;q=0.3,text/html;q=0.5,text/plain", + expectedType: "text/plain", + expBody: "authorization error: test", + }, + { + uc: "select text/html doe to unknown offered type", + code: http.StatusForbidden, + err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), + offeredType: "foo/bar;q=0.5,bar/foo;q=0.6", + expectedType: "text/html", + expBody: "

authorization error: test

", + }, + { + uc: "select text/html from multiple offered", + code: http.StatusForbidden, + err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), + offeredType: "application/json;q=0.3,text/html;q=0.5,text/html;q=0.8,*/*;q=0.2", + expectedType: "text/html", + expBody: "

authorization error: test

", + }, + { + uc: "select appliction/xml from multiple offered", + code: http.StatusForbidden, + err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), + offeredType: "application/json;q=0.3,text/html;q=0.5,text/plain;q=0.2,application/xml;q=0.8", + expectedType: "application/xml", + expBody: "authorizationErrortest", + }, + { + uc: "select appliction/json from multiple offered", + code: http.StatusForbidden, + err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), + offeredType: "application/xml;q=0.3,text/html;q=0.5,text/plain;q=0.2,application/json;q=0.8", + expectedType: "application/json", + expBody: "{\"code\":\"authorizationError\",\"message\":\"test\"}", + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // WHEN + resp := errorResponse(tc.code, tc.err, true, tc.offeredType) + + // THEN + require.NotNil(t, resp) + + assert.Equal(t, int32(tc.code), resp.Status.Code) + + deniedResp := resp.GetDeniedResponse() + assert.Equal(t, envoy_type.StatusCode(tc.code), deniedResp.Status.Code) + assert.Equal(t, "Content-Type", deniedResp.Headers[0].Header.Key) + assert.Equal(t, tc.expectedType, deniedResp.Headers[0].Header.Value) + assert.Equal(t, tc.expBody, deniedResp.Body) + }) + } +} From 494e2cb1c26a8b4468b5c536c2b872d10460e817 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 10 Feb 2023 10:39:18 +0100 Subject: [PATCH 31/67] forgotten go.mod & sum --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index adc0ee0c1..610871cb5 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/dlclark/regexp2 v1.8.0 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 + github.com/elnormous/contenttype v1.0.3 github.com/envoyproxy/go-control-plane v0.10.3 github.com/fsnotify/fsnotify v1.6.0 github.com/go-co-op/gocron v1.18.0 diff --git a/go.sum b/go.sum index 924fb6243..8f8050efe 100644 --- a/go.sum +++ b/go.sum @@ -833,6 +833,8 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elnormous/contenttype v1.0.3 h1:5DrD4LGO3ohab+jPplwE/LlY9JqmkYdssz4Zu7xl8Cs= +github.com/elnormous/contenttype v1.0.3/go.mod h1:ngVcyGGU8pnn4QJ5sL4StrNgc/wmXZXy5IQSBuHOFPg= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= From 0f4adcfbe3ac020f27a8b4de271bcdc7646f5741 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 10 Feb 2023 20:04:10 +0100 Subject: [PATCH 32/67] http label names updated to be more meaningfull especially, when the envoy grpc metrics are used as well --- .../fiber/middleware/prometheus/handler.go | 10 ++-- .../middleware/prometheus/handler_test.go | 50 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/internal/fiber/middleware/prometheus/handler.go b/internal/fiber/middleware/prometheus/handler.go index 9787b3f77..7f4fc9345 100644 --- a/internal/fiber/middleware/prometheus/handler.go +++ b/internal/fiber/middleware/prometheus/handler.go @@ -46,7 +46,7 @@ func New(opts ...Option) fiber.Handler { Help: "Count all requests by status code, method and path.", ConstLabels: options.labels, }, - []string{"status_code", "method", "path"}, + []string{"http_code", "http_method", "http_path"}, ) histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ @@ -58,18 +58,18 @@ func New(opts ...Option) fiber.Handler { 0.0001, 0.00025, 0.0005, 0.00075, // 100, 250, 500, 750µs 0.001, 0.0025, 0.005, 0.0075, // 1, 2.5, 5, 7.5ms 0.01, 0.025, 0.05, 0.075, // 10, 25, 50, 75ms - 0.1, 0.25, 0.5, // 100, 250, 500 ms - 1.0, 2.0, 5.0, 10.0, 15.0, // 1, 2, 5, 10, 20s + 0.1, 0.25, 0.5, 0.75, // 100, 250, 500 750 ms + 1.0, 2.0, 5.0, // 1, 2, 5 }, }, - []string{"status_code", "method", "path"}, + []string{"http_code", "http_method", "http_path"}, ) gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), Help: "All the requests in progress", ConstLabels: options.labels, - }, []string{"method"}) + }, []string{"http_method"}) handler := &metricsHandler{ reqCounter: counter, diff --git a/internal/fiber/middleware/prometheus/handler_test.go b/internal/fiber/middleware/prometheus/handler_test.go index 29315b2e4..b6fa2cdb7 100644 --- a/internal/fiber/middleware/prometheus/handler_test.go +++ b/internal/fiber/middleware/prometheus/handler_test.go @@ -79,18 +79,18 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx histMetric.GetHelp()) require.Len(t, histMetric.Metric, 1) assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) - assert.Equal(t, "GET", getLabel(histMetric.Metric[0].Label, "method")) - assert.Equal(t, "/test", getLabel(histMetric.Metric[0].Label, "path")) + assert.Equal(t, "GET", getLabel(histMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(histMetric.Metric[0].Label, "http_path")) assert.Equal(t, "foobar", getLabel(histMetric.Metric[0].Label, "service")) - assert.Equal(t, "200", getLabel(histMetric.Metric[0].Label, "status_code")) - require.Len(t, histMetric.Metric[0].Histogram.Bucket, 22) + assert.Equal(t, "200", getLabel(histMetric.Metric[0].Label, "http_code")) + require.Len(t, histMetric.Metric[0].Histogram.Bucket, 21) gaugeMetric := metricForType(metrics, dto.MetricType_GAUGE.Enum()) assert.Equal(t, "foo_bar_requests_in_progress_total", gaugeMetric.GetName()) assert.Equal(t, "All the requests in progress", gaugeMetric.GetHelp()) require.Len(t, gaugeMetric.Metric, 1) assert.Equal(t, "zab", getLabel(gaugeMetric.Metric[0].Label, "baz")) - assert.Equal(t, "GET", getLabel(gaugeMetric.Metric[0].Label, "method")) + assert.Equal(t, "GET", getLabel(gaugeMetric.Metric[0].Label, "http_method")) assert.Equal(t, "foobar", getLabel(gaugeMetric.Metric[0].Label, "service")) require.Equal(t, 0.0, gaugeMetric.Metric[0].Gauge.GetValue()) @@ -100,10 +100,10 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx counterMetric.GetHelp()) require.Len(t, counterMetric.Metric, 1) assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) - assert.Equal(t, "GET", getLabel(counterMetric.Metric[0].Label, "method")) - assert.Equal(t, "/test", getLabel(counterMetric.Metric[0].Label, "path")) + assert.Equal(t, "GET", getLabel(counterMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(counterMetric.Metric[0].Label, "http_path")) assert.Equal(t, "foobar", getLabel(counterMetric.Metric[0].Label, "service")) - assert.Equal(t, "200", getLabel(counterMetric.Metric[0].Label, "status_code")) + assert.Equal(t, "200", getLabel(counterMetric.Metric[0].Label, "http_code")) require.Equal(t, 1.0, counterMetric.Metric[0].Counter.GetValue()) }, }, @@ -121,18 +121,18 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx histMetric.GetHelp()) require.Len(t, histMetric.Metric, 1) assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) - assert.Equal(t, "POST", getLabel(histMetric.Metric[0].Label, "method")) - assert.Equal(t, "/test", getLabel(histMetric.Metric[0].Label, "path")) + assert.Equal(t, "POST", getLabel(histMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(histMetric.Metric[0].Label, "http_path")) assert.Equal(t, "foobar", getLabel(histMetric.Metric[0].Label, "service")) - assert.Equal(t, "500", getLabel(histMetric.Metric[0].Label, "status_code")) - require.Len(t, histMetric.Metric[0].Histogram.Bucket, 22) + assert.Equal(t, "500", getLabel(histMetric.Metric[0].Label, "http_code")) + require.Len(t, histMetric.Metric[0].Histogram.Bucket, 21) gaugeMetric := metricForType(metrics, dto.MetricType_GAUGE.Enum()) assert.Equal(t, "foo_bar_requests_in_progress_total", gaugeMetric.GetName()) assert.Equal(t, "All the requests in progress", gaugeMetric.GetHelp()) require.Len(t, gaugeMetric.Metric, 1) assert.Equal(t, "zab", getLabel(gaugeMetric.Metric[0].Label, "baz")) - assert.Equal(t, "POST", getLabel(gaugeMetric.Metric[0].Label, "method")) + assert.Equal(t, "POST", getLabel(gaugeMetric.Metric[0].Label, "http_method")) assert.Equal(t, "foobar", getLabel(gaugeMetric.Metric[0].Label, "service")) require.Equal(t, 0.0, gaugeMetric.Metric[0].Gauge.GetValue()) @@ -142,15 +142,15 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx counterMetric.GetHelp()) require.Len(t, counterMetric.Metric, 1) assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) - assert.Equal(t, "POST", getLabel(counterMetric.Metric[0].Label, "method")) - assert.Equal(t, "/test", getLabel(counterMetric.Metric[0].Label, "path")) + assert.Equal(t, "POST", getLabel(counterMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(counterMetric.Metric[0].Label, "http_path")) assert.Equal(t, "foobar", getLabel(counterMetric.Metric[0].Label, "service")) - assert.Equal(t, "500", getLabel(counterMetric.Metric[0].Label, "status_code")) + assert.Equal(t, "500", getLabel(counterMetric.Metric[0].Label, "http_code")) require.Equal(t, 1.0, counterMetric.Metric[0].Counter.GetValue()) }, }, { - uc: "metrics for request which server raising an error", + uc: "metrics for request with server raising an error", req: httptest.NewRequest(http.MethodPatch, "/error", nil), assert: func(t *testing.T, metrics []*dto.MetricFamily) { t.Helper() @@ -163,18 +163,18 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx histMetric.GetHelp()) require.Len(t, histMetric.Metric, 1) assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) - assert.Equal(t, "PATCH", getLabel(histMetric.Metric[0].Label, "method")) - assert.Equal(t, "/error", getLabel(histMetric.Metric[0].Label, "path")) + assert.Equal(t, "PATCH", getLabel(histMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/error", getLabel(histMetric.Metric[0].Label, "http_path")) assert.Equal(t, "foobar", getLabel(histMetric.Metric[0].Label, "service")) - assert.Equal(t, "410", getLabel(histMetric.Metric[0].Label, "status_code")) - require.Len(t, histMetric.Metric[0].Histogram.Bucket, 22) + assert.Equal(t, "410", getLabel(histMetric.Metric[0].Label, "http_code")) + require.Len(t, histMetric.Metric[0].Histogram.Bucket, 21) gaugeMetric := metricForType(metrics, dto.MetricType_GAUGE.Enum()) assert.Equal(t, "foo_bar_requests_in_progress_total", gaugeMetric.GetName()) assert.Equal(t, "All the requests in progress", gaugeMetric.GetHelp()) require.Len(t, gaugeMetric.Metric, 1) assert.Equal(t, "zab", getLabel(gaugeMetric.Metric[0].Label, "baz")) - assert.Equal(t, "PATCH", getLabel(gaugeMetric.Metric[0].Label, "method")) + assert.Equal(t, "PATCH", getLabel(gaugeMetric.Metric[0].Label, "http_method")) assert.Equal(t, "foobar", getLabel(gaugeMetric.Metric[0].Label, "service")) require.Equal(t, 0.0, gaugeMetric.Metric[0].Gauge.GetValue()) @@ -184,10 +184,10 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx counterMetric.GetHelp()) require.Len(t, counterMetric.Metric, 1) assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) - assert.Equal(t, "PATCH", getLabel(counterMetric.Metric[0].Label, "method")) - assert.Equal(t, "/error", getLabel(counterMetric.Metric[0].Label, "path")) + assert.Equal(t, "PATCH", getLabel(counterMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/error", getLabel(counterMetric.Metric[0].Label, "http_path")) assert.Equal(t, "foobar", getLabel(counterMetric.Metric[0].Label, "service")) - assert.Equal(t, "410", getLabel(counterMetric.Metric[0].Label, "status_code")) + assert.Equal(t, "410", getLabel(counterMetric.Metric[0].Label, "http_code")) require.Equal(t, 1.0, counterMetric.Metric[0].Counter.GetValue()) }, }, From 3ada333eb5d465f048c5f4e05d1a425079fc056d Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 10 Feb 2023 20:05:07 +0100 Subject: [PATCH 33/67] fixes and more tests --- .../middleware/prometheus/interceptor.go | 57 ++-- .../middleware/prometheus/interceptor_test.go | 290 ++++++++++++++++++ 2 files changed, 321 insertions(+), 26 deletions(-) create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go index 910a1cae3..a9c1bed21 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go @@ -25,7 +25,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "google.golang.org/grpc" - "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -44,34 +43,36 @@ func New(opts ...Option) grpc.UnaryServerInterceptor { counter := promauto.With(options.registerer).NewCounterVec( prometheus.CounterOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), - Help: "Count all requests by status code, service and method.", + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_total"), + Help: "Count all requests by tunneled HTTP status code, service and method, as well as by" + + " GRPC method and status code.", ConstLabels: options.labels, }, - []string{"status_code", "method", "path"}, + []string{"http_code", "http_method", "http_path", "grpc_method", "grpc_code"}, ) histogram := promauto.With(options.registerer).NewHistogramVec(prometheus.HistogramOpts{ - Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), - Help: "Duration of all requests by code, service and method.", + Name: prometheus.BuildFQName(options.namespace, options.subsystem, "request_duration_seconds"), + Help: "Duration of all requests by tunneled HTTP status code, service and method, as well as by" + + " GRPC method and status code.", ConstLabels: options.labels, Buckets: []float64{ - 0.00001, 0.000025, 0.00005, 0.000075, // 10, 25, 50, 75µs + 0.00001, 0.00005, // 10, 50µs 0.0001, 0.00025, 0.0005, 0.00075, // 100, 250, 500, 750µs 0.001, 0.0025, 0.005, 0.0075, // 1, 2.5, 5, 7.5ms 0.01, 0.025, 0.05, 0.075, // 10, 25, 50, 75ms - 0.1, 0.25, 0.5, 0.75, // 100, 250, 500 750ms - 1.0, 2.0, // 1, 2s + 0.1, 0.25, 0.5, 0.75, // 100, 250, 500 750 ms + 1.0, 2.0, 5.0, // 1, 2, 5 }, }, - []string{"status_code", "method", "path"}, + []string{"http_code", "http_method", "http_path", "grpc_method", "grpc_code"}, ) gauge := promauto.With(options.registerer).NewGaugeVec(prometheus.GaugeOpts{ Name: prometheus.BuildFQName(options.namespace, options.subsystem, "requests_in_progress_total"), - Help: "All the requests in progress", + Help: "All the requests in progress by tunneled HTTP method, as well as by GRPC method.", ConstLabels: options.labels, - }, []string{"method"}) + }, []string{"http_method", "grpc_method"}) handler := &metricsHandler{ reqCounter: counter, @@ -85,38 +86,42 @@ func New(opts ...Option) grpc.UnaryServerInterceptor { func (h *metricsHandler) observeRequest( ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (any, error) { - const MagicNumber = 1e9 + const ( + MagicNumber = 1e9 + Unknown = "unknown" + ) start := time.Now() - method := "GRPC" - path := info.FullMethod - code := int(codes.OK) + grpcMethod := info.FullMethod + grpcCode := "0" + httpMethod := Unknown + httpPath := Unknown + httpCode := Unknown if cr, ok := req.(*envoy_auth.CheckRequest); ok { - method = cr.Attributes.Request.Http.Method - path = cr.Attributes.Request.Http.Path + httpMethod = cr.Attributes.Request.Http.Method + httpPath = cr.Attributes.Request.Http.Path } - h.reqInFlight.WithLabelValues(method).Inc() + h.reqInFlight.WithLabelValues(httpMethod, grpcMethod).Inc() defer func() { - h.reqInFlight.WithLabelValues(method).Dec() + h.reqInFlight.WithLabelValues(httpMethod, grpcMethod).Dec() }() resp, err := handler(ctx, req) if err != nil { s, _ := status.FromError(err) - code = int(s.Code()) - } else if cr, ok := req.(*envoy_auth.CheckResponse); ok { - code = int(cr.Status.Code) + grpcCode = strconv.Itoa(int(s.Code())) + } else if cr, ok := resp.(*envoy_auth.CheckResponse); ok { + httpCode = strconv.Itoa(int(cr.Status.Code)) } - statusCode := strconv.Itoa(code) - h.reqCounter.WithLabelValues(statusCode, method, path).Inc() + h.reqCounter.WithLabelValues(httpCode, httpMethod, httpPath, grpcMethod, grpcCode).Inc() elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber - h.reqHistogram.WithLabelValues(statusCode, method, path).Observe(elapsed) + h.reqHistogram.WithLabelValues(httpCode, httpMethod, httpPath, grpcMethod, grpcCode).Observe(elapsed) return resp, err } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go new file mode 100644 index 000000000..af2b508ee --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go @@ -0,0 +1,290 @@ +package prometheus + +import ( + "context" + "net" + "net/http" + "testing" + + 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/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + rpc_status "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + + "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" +) + +func metricForType(metrics []*dto.MetricFamily, metricType *dto.MetricType) *dto.MetricFamily { + for _, m := range metrics { + if *m.Type == *metricType { + return m + } + } + + return nil +} + +func getLabel(labels []*dto.LabelPair, name string) string { + for _, label := range labels { + if label.GetName() == name { + return label.GetValue() + } + } + + return "" +} + +func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx + t.Parallel() + + for _, tc := range []struct { + uc string + configureMock func(t *testing.T, srv *mocks.MockHandler) + assert func(t *testing.T, metrics []*dto.MetricFamily) + }{ + { + uc: "metrics for successful request", + configureMock: func(t *testing.T, handler *mocks.MockHandler) { + t.Helper() + + handler.On("Check", mock.Anything, mock.Anything).Return(&envoy_auth.CheckResponse{ + Status: &rpc_status.Status{Code: int32(envoy_type.StatusCode_OK)}, + HttpResponse: &envoy_auth.CheckResponse_OkResponse{ + OkResponse: &envoy_auth.OkHttpResponse{}, + }, + }, nil) + }, + assert: func(t *testing.T, metrics []*dto.MetricFamily) { + t.Helper() + + assert.Len(t, metrics, 3) + + histMetric := metricForType(metrics, dto.MetricType_HISTOGRAM.Enum()) + assert.Equal(t, "foo_bar_request_duration_seconds", histMetric.GetName()) + assert.Equal(t, "Duration of all requests by tunneled HTTP status code, service and method, "+ + "as well as by GRPC method and status code.", + histMetric.GetHelp()) + require.Len(t, histMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(histMetric.Metric[0].Label, "service")) + assert.Equal(t, "0", getLabel(histMetric.Metric[0].Label, "grpc_code")) + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "POST", getLabel(histMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(histMetric.Metric[0].Label, "http_path")) + assert.Equal(t, "200", getLabel(histMetric.Metric[0].Label, "http_code")) + require.Len(t, histMetric.Metric[0].Histogram.Bucket, 21) + + gaugeMetric := metricForType(metrics, dto.MetricType_GAUGE.Enum()) + assert.Equal(t, "foo_bar_requests_in_progress_total", gaugeMetric.GetName()) + assert.Equal(t, "All the requests in progress by tunneled HTTP method, "+ + "as well as by GRPC method.", gaugeMetric.GetHelp()) + require.Len(t, gaugeMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(gaugeMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(gaugeMetric.Metric[0].Label, "service")) + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", + getLabel(gaugeMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "POST", getLabel(gaugeMetric.Metric[0].Label, "http_method")) + require.Equal(t, 0.0, gaugeMetric.Metric[0].Gauge.GetValue()) + + counterMetric := metricForType(metrics, dto.MetricType_COUNTER.Enum()) + assert.Equal(t, "foo_bar_requests_total", counterMetric.GetName()) + assert.Equal(t, "Count all requests by tunneled HTTP status code, service and method,"+ + " as well as by GRPC method and status code.", + counterMetric.GetHelp()) + require.Len(t, counterMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(counterMetric.Metric[0].Label, "service")) + assert.Equal(t, "0", getLabel(histMetric.Metric[0].Label, "grpc_code")) + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "POST", getLabel(counterMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(counterMetric.Metric[0].Label, "http_path")) + assert.Equal(t, "200", getLabel(counterMetric.Metric[0].Label, "http_code")) + require.Equal(t, 1.0, counterMetric.Metric[0].Counter.GetValue()) + }, + }, + { + uc: "metrics for request which failed with 403", + configureMock: func(t *testing.T, handler *mocks.MockHandler) { + t.Helper() + + handler.On("Check", mock.Anything, mock.Anything).Return(&envoy_auth.CheckResponse{ + Status: &rpc_status.Status{Code: int32(envoy_type.StatusCode_Forbidden)}, + HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{ + DeniedResponse: &envoy_auth.DeniedHttpResponse{ + Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode_Forbidden}, + }, + }, + }, nil) + }, + assert: func(t *testing.T, metrics []*dto.MetricFamily) { + t.Helper() + + assert.Len(t, metrics, 3) + + histMetric := metricForType(metrics, dto.MetricType_HISTOGRAM.Enum()) + assert.Equal(t, "foo_bar_request_duration_seconds", histMetric.GetName()) + assert.Equal(t, "Duration of all requests by tunneled HTTP status code, service and method, "+ + "as well as by GRPC method and status code.", histMetric.GetHelp()) + require.Len(t, histMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(histMetric.Metric[0].Label, "service")) + assert.Equal(t, "0", getLabel(histMetric.Metric[0].Label, "grpc_code")) + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "POST", getLabel(histMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(histMetric.Metric[0].Label, "http_path")) + assert.Equal(t, "403", getLabel(histMetric.Metric[0].Label, "http_code")) + require.Len(t, histMetric.Metric[0].Histogram.Bucket, 21) + + gaugeMetric := metricForType(metrics, dto.MetricType_GAUGE.Enum()) + assert.Equal(t, "foo_bar_requests_in_progress_total", gaugeMetric.GetName()) + assert.Equal(t, "All the requests in progress by tunneled HTTP method, "+ + "as well as by GRPC method.", gaugeMetric.GetHelp()) + require.Len(t, gaugeMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(gaugeMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(gaugeMetric.Metric[0].Label, "service")) + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "POST", getLabel(gaugeMetric.Metric[0].Label, "http_method")) + require.Equal(t, 0.0, gaugeMetric.Metric[0].Gauge.GetValue()) + + counterMetric := metricForType(metrics, dto.MetricType_COUNTER.Enum()) + assert.Equal(t, "foo_bar_requests_total", counterMetric.GetName()) + assert.Equal(t, "Count all requests by tunneled HTTP status code, service and method, "+ + "as well as by GRPC method and status code.", counterMetric.GetHelp()) + require.Len(t, counterMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(counterMetric.Metric[0].Label, "service")) + assert.Equal(t, "0", getLabel(histMetric.Metric[0].Label, "grpc_code")) + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "POST", getLabel(counterMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(counterMetric.Metric[0].Label, "http_path")) + assert.Equal(t, "403", getLabel(counterMetric.Metric[0].Label, "http_code")) + require.Equal(t, 1.0, counterMetric.Metric[0].Counter.GetValue()) + }, + }, + { + uc: "metrics for request with server raising an error", + configureMock: func(t *testing.T, handler *mocks.MockHandler) { + t.Helper() + + handler.On("Check", mock.Anything, mock.Anything). + Return(nil, status.Error(codes.FailedPrecondition, "test error")) + }, + assert: func(t *testing.T, metrics []*dto.MetricFamily) { + t.Helper() + + assert.Len(t, metrics, 3) + + histMetric := metricForType(metrics, dto.MetricType_HISTOGRAM.Enum()) + assert.Equal(t, "foo_bar_request_duration_seconds", histMetric.GetName()) + assert.Equal(t, "Duration of all requests by tunneled HTTP status code, service and method, "+ + "as well as by GRPC method and status code.", histMetric.GetHelp()) + require.Len(t, histMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(histMetric.Metric[0].Label, "service")) + assert.Equal(t, "9", getLabel(histMetric.Metric[0].Label, "grpc_code")) + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "POST", getLabel(histMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(histMetric.Metric[0].Label, "http_path")) + assert.Equal(t, "unknown", getLabel(histMetric.Metric[0].Label, "http_code")) + require.Len(t, histMetric.Metric[0].Histogram.Bucket, 21) + + gaugeMetric := metricForType(metrics, dto.MetricType_GAUGE.Enum()) + assert.Equal(t, "foo_bar_requests_in_progress_total", gaugeMetric.GetName()) + assert.Equal(t, "All the requests in progress by tunneled HTTP method, "+ + "as well as by GRPC method.", gaugeMetric.GetHelp()) + assert.Equal(t, "zab", getLabel(gaugeMetric.Metric[0].Label, "baz")) + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "POST", getLabel(gaugeMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "foobar", getLabel(gaugeMetric.Metric[0].Label, "service")) + require.Equal(t, 0.0, gaugeMetric.Metric[0].Gauge.GetValue()) + + counterMetric := metricForType(metrics, dto.MetricType_COUNTER.Enum()) + assert.Equal(t, "foo_bar_requests_total", counterMetric.GetName()) + assert.Equal(t, "Count all requests by tunneled HTTP status code, service and method, "+ + "as well as by GRPC method and status code.", counterMetric.GetHelp()) + require.Len(t, counterMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(counterMetric.Metric[0].Label, "service")) + assert.Equal(t, "9", getLabel(histMetric.Metric[0].Label, "grpc_code")) + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "POST", getLabel(counterMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "/test", getLabel(counterMetric.Metric[0].Label, "http_path")) + assert.Equal(t, "unknown", getLabel(counterMetric.Metric[0].Label, "http_code")) + require.Equal(t, 1.0, counterMetric.Metric[0].Counter.GetValue()) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + registry := prometheus.NewRegistry() + lis := bufconn.Listen(1024 * 1024) + handler := &mocks.MockHandler{} + bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } + conn, err := grpc.DialContext(context.Background(), "bufnet", + grpc.WithContextDialer(bufDialer), + grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + defer conn.Close() + + tc.configureMock(t, handler) + + srv := grpc.NewServer(grpc.UnaryInterceptor(New( + WithRegisterer(registry), + WithNamespace("foo"), + WithSubsystem("bar"), + WithLabel("baz", "zab"), + WithServiceName("foobar"), + ))) + envoy_auth.RegisterAuthorizationServer(srv, handler) + + go func() { + err = srv.Serve(lis) + require.NoError(t, err) + }() + + client := envoy_auth.NewAuthorizationClient(conn) + + // WHEN + // we're not interested in the response and the error + // nolint: errcheck + client.Check(context.Background(), &envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Request: &envoy_auth.AttributeContext_Request{ + Http: &envoy_auth.AttributeContext_HttpRequest{ + Body: "foo", + Method: http.MethodPost, + Path: "/test", + }, + }, + }, + }) + + // THEN + srv.Stop() + + metrics, err := registry.Gather() + require.NoError(t, err) + + tc.assert(t, metrics) + handler.AssertExpectations(t) + }) + } +} From 6254ab600d6ac0e43898fae1f32b866904f924a2 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sat, 11 Feb 2023 20:43:34 +0100 Subject: [PATCH 34/67] license header added --- .../middleware/accesslog/interceptor_test.go | 16 ++++++++++++++++ .../middleware/errorhandler/error_response.go | 16 ++++++++++++++++ .../errorhandler/error_response_test.go | 16 ++++++++++++++++ .../middleware/errorhandler/interceptor_test.go | 2 +- .../grpcv3/middleware/logger/interceptor_test.go | 2 +- .../middleware/prometheus/interceptor_test.go | 16 ++++++++++++++++ .../handler/envoyextauth/grpcv3/mocks/handler.go | 16 ++++++++++++++++ 7 files changed, 82 insertions(+), 2 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go index 87840da42..561e77a04 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go @@ -1,3 +1,19 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + package accesslog import ( diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go index de731bc57..d7b4ce2ec 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go @@ -1,3 +1,19 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + package errorhandler import ( diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go index a6e836aa4..b749e36e3 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go @@ -1,3 +1,19 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + package errorhandler import ( diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go index 1b82fea7c..a8e8f55cc 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 Dimitrij Drus +// Copyright 2023 Dimitrij Drus // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go index c48c96918..8a4979828 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 Dimitrij Drus +// Copyright 2023 Dimitrij Drus // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go index af2b508ee..e02d9ec81 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go @@ -1,3 +1,19 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + package prometheus import ( diff --git a/internal/handler/envoyextauth/grpcv3/mocks/handler.go b/internal/handler/envoyextauth/grpcv3/mocks/handler.go index bfee9487f..ae49a634e 100644 --- a/internal/handler/envoyextauth/grpcv3/mocks/handler.go +++ b/internal/handler/envoyextauth/grpcv3/mocks/handler.go @@ -1,3 +1,19 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + package mocks import ( From 5a16c5b012ce76ed3edb831879cc368403e9319c Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 18:25:58 +0100 Subject: [PATCH 35/67] more tests and handling of unknown services/methods in access log and metrics --- .../middleware/accesslog/interceptor.go | 73 +++++-- .../middleware/accesslog/interceptor_test.go | 105 +++++++++- .../grpcv3/middleware/mocks/generate.go | 4 + .../middleware/mocks/test_service.pb.go | 190 ++++++++++++++++++ .../middleware/mocks/test_service.proto | 13 ++ .../middleware/mocks/test_service_grpc.pb.go | 110 ++++++++++ .../middleware/prometheus/interceptor.go | 50 ++++- .../middleware/prometheus/interceptor_test.go | 99 ++++++++- .../handler/envoyextauth/grpcv3/service.go | 32 ++- 9 files changed, 633 insertions(+), 43 deletions(-) create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/mocks/generate.go create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service.pb.go create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service.proto create mode 100644 internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service_grpc.pb.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go index a74b160cc..43a09ec99 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go @@ -30,34 +30,75 @@ import ( "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" ) -func New(logger zerolog.Logger) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - start := time.Now() - requestMetadata, _ := metadata.FromIncomingContext(ctx) +type ServerInterceptor interface { + Unary() grpc.UnaryServerInterceptor + Stream() grpc.StreamServerInterceptor +} - logCtx := logger.Level(zerolog.InfoLevel).With(). - Int64("_tx_start", start.Unix()). - Str("_peer", peerFromCtx(ctx)). - Str("_request", info.FullMethod) +func New(logger zerolog.Logger) ServerInterceptor { + return &accessLogInterceptor{l: logger} +} - logCtx = logTraceData(ctx, logCtx) - logCtx = logMetaData(logCtx, requestMetadata, "x-forwarded-for", "_x_forwarded_for") - logCtx = logMetaData(logCtx, requestMetadata, "forwarded", "_forwarded") +type accessLogInterceptor struct { + l zerolog.Logger +} - accLog := logCtx.Logger() - accLog.Info().Msg("TX started") +func (i *accessLogInterceptor) Unary() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + start, accLog := i.startTransaction(ctx, info.FullMethod) ctx = accesscontext.New(ctx) res, err := handler(ctx, req) - logAccessStatus(ctx, accLog.Info(), err). - Int64("_tx_duration_ms", time.Until(start).Milliseconds()). - Msg("TX finished") + i.finalizeTransaction(ctx, accLog, start, err) return res, err } } +func (i *accessLogInterceptor) Stream() grpc.StreamServerInterceptor { + return func( + srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler, + ) error { + ctx := stream.Context() + start, accLog := i.startTransaction(ctx, info.FullMethod) + + ctx = accesscontext.New(ctx) + err := handler(srv, stream) + + i.finalizeTransaction(ctx, accLog, start, err) + + return err + } +} + +func (i *accessLogInterceptor) startTransaction(ctx context.Context, fullMethod string) (time.Time, zerolog.Logger) { + start := time.Now() + requestMetadata, _ := metadata.FromIncomingContext(ctx) + + logCtx := i.l.Level(zerolog.InfoLevel).With(). + Int64("_tx_start", start.Unix()). + Str("_peer", peerFromCtx(ctx)). + Str("_request", fullMethod) + + logCtx = logTraceData(ctx, logCtx) + logCtx = logMetaData(logCtx, requestMetadata, "x-forwarded-for", "_x_forwarded_for") + logCtx = logMetaData(logCtx, requestMetadata, "forwarded", "_forwarded") + + accLog := logCtx.Logger() + accLog.Info().Msg("TX started") + + return start, accLog +} + +func (i *accessLogInterceptor) finalizeTransaction( + ctx context.Context, accLog zerolog.Logger, start time.Time, err error, +) { + logAccessStatus(ctx, accLog.Info(), err). + Int64("_tx_duration_ms", time.Until(start).Milliseconds()). + Msg("TX finished") +} + func logAccessStatus(ctx context.Context, event *zerolog.Event, err error) *zerolog.Event { subject := accesscontext.Subject(ctx) accessErr := accesscontext.Error(ctx) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go index 561e77a04..295dafc03 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go @@ -36,18 +36,21 @@ import ( "go.opentelemetry.io/otel/propagation" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" - "google.golang.org/genproto/googleapis/rpc/status" + rpc_status "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" "github.com/dadrus/heimdall/internal/accesscontext" - "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" + grpc_mocks "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/mocks" + service_mocks "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" "github.com/dadrus/heimdall/internal/x/testsupport" ) -func TestAccessLogInterceptor(t *testing.T) { +func TestAccessLogInterceptorForKnownService(t *testing.T) { otel.SetTracerProvider(sdktrace.NewTracerProvider()) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) @@ -58,7 +61,7 @@ func TestAccessLogInterceptor(t *testing.T) { for _, tc := range []struct { uc string outgoingContext func(t *testing.T) context.Context - configureMock func(t *testing.T, m *mocks.MockHandler) + configureMock func(t *testing.T, m *service_mocks.MockHandler) assert func(t *testing.T, logEvent1, logEvent2 map[string]any) }{ { @@ -68,7 +71,7 @@ func TestAccessLogInterceptor(t *testing.T) { return context.Background() }, - configureMock: func(t *testing.T, m *mocks.MockHandler) { + configureMock: func(t *testing.T, m *service_mocks.MockHandler) { t.Helper() m.On("Check", @@ -81,7 +84,7 @@ func TestAccessLogInterceptor(t *testing.T) { ), mock.Anything, ).Return( - &envoy_auth.CheckResponse{Status: &status.Status{Code: int32(envoy_type.StatusCode_OK)}}, + &envoy_auth.CheckResponse{Status: &rpc_status.Status{Code: int32(envoy_type.StatusCode_OK)}}, nil, ) }, @@ -130,7 +133,7 @@ func TestAccessLogInterceptor(t *testing.T) { return metadata.NewOutgoingContext(context.Background(), metadata.New(md)) }, - configureMock: func(t *testing.T, m *mocks.MockHandler) { + configureMock: func(t *testing.T, m *service_mocks.MockHandler) { t.Helper() m.On("Check", mock.Anything, mock.Anything). @@ -174,7 +177,7 @@ func TestAccessLogInterceptor(t *testing.T) { return context.Background() }, - configureMock: func(t *testing.T, m *mocks.MockHandler) { + configureMock: func(t *testing.T, m *service_mocks.MockHandler) { t.Helper() m.On("Check", @@ -188,7 +191,7 @@ func TestAccessLogInterceptor(t *testing.T) { ), mock.Anything, ).Return( - &envoy_auth.CheckResponse{Status: &status.Status{Code: int32(envoy_type.StatusCode_Forbidden)}}, + &envoy_auth.CheckResponse{Status: &rpc_status.Status{Code: int32(envoy_type.StatusCode_Forbidden)}}, nil, ) }, @@ -232,7 +235,7 @@ func TestAccessLogInterceptor(t *testing.T) { lis := bufconn.Listen(1024 * 1024) tb := &testsupport.TestingLog{TB: t} logger := zerolog.New(zerolog.TestWriter{T: tb}) - handler := &mocks.MockHandler{} + handler := &service_mocks.MockHandler{} bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } @@ -248,7 +251,7 @@ func TestAccessLogInterceptor(t *testing.T) { srv := grpc.NewServer( grpc.ChainUnaryInterceptor( otelgrpc.UnaryServerInterceptor(), - New(logger), + New(logger).Unary(), ), ) envoy_auth.RegisterAuthorizationServer(srv, handler) @@ -288,3 +291,83 @@ func TestAccessLogInterceptor(t *testing.T) { }) } } + +func TestAccessLogInterceptorForUnknownService(t *testing.T) { + otel.SetTracerProvider(sdktrace.NewTracerProvider()) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) + + var ( + logEvent1 map[string]any + logEvent2 map[string]any + ) + + lis := bufconn.Listen(1024 * 1024) + tb := &testsupport.TestingLog{TB: t} + logger := zerolog.New(zerolog.TestWriter{T: tb}) + handler := &service_mocks.MockHandler{} + bufDialer := func(context.Context, string) (net.Conn, error) { + return lis.Dial() + } + conn, err := grpc.DialContext(context.Background(), "bufnet", + grpc.WithContextDialer(bufDialer), + grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + defer conn.Close() + + srv := grpc.NewServer( + grpc.UnknownServiceHandler(func(srv interface{}, stream grpc.ServerStream) error { + return status.Error(codes.Unknown, "unknown service or method") + }), + grpc.ChainStreamInterceptor( + otelgrpc.StreamServerInterceptor(), + New(logger).Stream(), + ), + ) + envoy_auth.RegisterAuthorizationServer(srv, handler) + + go func() { + err = srv.Serve(lis) + require.NoError(t, err) + }() + + client := grpc_mocks.NewTestClient(conn) + + // WHEN + // nolint: errcheck + _, err = client.Test(context.Background(), &grpc_mocks.TestRequest{}) + + // THEN + require.Error(t, err) + srv.Stop() + handler.AssertExpectations(t) + + events := strings.Split(tb.CollectedLog(), "}") + require.Len(t, events, 3) + + require.NoError(t, json.Unmarshal([]byte(events[0]+"}"), &logEvent1)) + require.NoError(t, json.Unmarshal([]byte(events[1]+"}"), &logEvent2)) + + require.Len(t, logEvent1, 7) + assert.Equal(t, "info", logEvent1["level"]) + assert.Contains(t, logEvent1, "_tx_start") + assert.Contains(t, logEvent1, "_peer") + assert.Equal(t, "/test.Test/Test", logEvent1["_request"]) + assert.Contains(t, logEvent1, "_trace_id") + assert.Contains(t, logEvent1, "_trace_id") + assert.Equal(t, "TX started", logEvent1["message"]) + + require.Len(t, logEvent2, 10) + assert.Equal(t, "info", logEvent2["level"]) + assert.Contains(t, logEvent2, "_tx_start") + assert.Contains(t, logEvent2, "_tx_duration_ms") + assert.Contains(t, logEvent2, "_peer") + assert.Equal(t, logEvent1["_request"], logEvent2["_request"]) + assert.Contains(t, logEvent2, "_trace_id") + assert.Contains(t, logEvent2, "_trace_id") + assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) + assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) + assert.Equal(t, false, logEvent2["_access_granted"]) + assert.Contains(t, logEvent2["error"], "unknown service or method") + assert.Equal(t, "TX finished", logEvent2["message"]) +} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/mocks/generate.go b/internal/handler/envoyextauth/grpcv3/middleware/mocks/generate.go new file mode 100644 index 000000000..34baa52c1 --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/mocks/generate.go @@ -0,0 +1,4 @@ +package mocks + +//go:generate protoc --go_out=. --go_opt=paths=source_relative test_service.proto +//go:generate protoc --go-grpc_out=. --go-grpc_opt=paths=source_relative test_service.proto diff --git a/internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service.pb.go b/internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service.pb.go new file mode 100644 index 000000000..cbf893540 --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service.pb.go @@ -0,0 +1,190 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.12.4 +// source: test_service.proto + +package mocks + +import ( + "reflect" + "sync" + + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TestRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TestRequest) Reset() { + *x = TestRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_test_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestRequest) ProtoMessage() {} + +func (x *TestRequest) ProtoReflect() protoreflect.Message { + mi := &file_test_service_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestRequest.ProtoReflect.Descriptor instead. +func (*TestRequest) Descriptor() ([]byte, []int) { + return file_test_service_proto_rawDescGZIP(), []int{0} +} + +type TestResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TestResponse) Reset() { + *x = TestResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_test_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestResponse) ProtoMessage() {} + +func (x *TestResponse) ProtoReflect() protoreflect.Message { + mi := &file_test_service_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestResponse.ProtoReflect.Descriptor instead. +func (*TestResponse) Descriptor() ([]byte, []int) { + return file_test_service_proto_rawDescGZIP(), []int{1} +} + +var File_test_service_proto protoreflect.FileDescriptor + +var file_test_service_proto_rawDesc = []byte{ + 0x0a, 0x12, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x74, 0x65, 0x73, 0x74, 0x22, 0x0d, 0x0a, 0x0b, 0x54, 0x65, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x54, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x35, 0x0a, 0x04, 0x54, 0x65, 0x73, + 0x74, 0x12, 0x2d, 0x0a, 0x04, 0x54, 0x65, 0x73, 0x74, 0x12, 0x11, 0x2e, 0x74, 0x65, 0x73, 0x74, + 0x2e, 0x54, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x74, + 0x65, 0x73, 0x74, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_test_service_proto_rawDescOnce sync.Once + file_test_service_proto_rawDescData = file_test_service_proto_rawDesc +) + +func file_test_service_proto_rawDescGZIP() []byte { + file_test_service_proto_rawDescOnce.Do(func() { + file_test_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_service_proto_rawDescData) + }) + return file_test_service_proto_rawDescData +} + +var file_test_service_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_test_service_proto_goTypes = []interface{}{ + (*TestRequest)(nil), // 0: test.TestRequest + (*TestResponse)(nil), // 1: test.TestResponse +} +var file_test_service_proto_depIdxs = []int32{ + 0, // 0: test.Test.Test:input_type -> test.TestRequest + 1, // 1: test.Test.Test:output_type -> test.TestResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_test_service_proto_init() } +func file_test_service_proto_init() { + if File_test_service_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_test_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TestRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TestResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_test_service_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_test_service_proto_goTypes, + DependencyIndexes: file_test_service_proto_depIdxs, + MessageInfos: file_test_service_proto_msgTypes, + }.Build() + File_test_service_proto = out.File + file_test_service_proto_rawDesc = nil + file_test_service_proto_goTypes = nil + file_test_service_proto_depIdxs = nil +} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service.proto b/internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service.proto new file mode 100644 index 000000000..c10fd6a51 --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service.proto @@ -0,0 +1,13 @@ +syntax = 'proto3'; + +option go_package = "./testservice"; + +package test; + +message TestRequest {} + +message TestResponse {} + +service Test { + rpc Test(TestRequest) returns (TestResponse); +} \ No newline at end of file diff --git a/internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service_grpc.pb.go b/internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service_grpc.pb.go new file mode 100644 index 000000000..26aca434b --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/middleware/mocks/test_service_grpc.pb.go @@ -0,0 +1,110 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v3.12.4 +// source: test_service.proto + +package mocks + +import ( + "context" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Test_Test_FullMethodName = "/test.Test/Test" +) + +// TestClient is the client API for Test service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type TestClient interface { + Test(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error) +} + +type testClient struct { + cc grpc.ClientConnInterface +} + +func NewTestClient(cc grpc.ClientConnInterface) TestClient { + return &testClient{cc} +} + +func (c *testClient) Test(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error) { + out := new(TestResponse) + err := c.cc.Invoke(ctx, Test_Test_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TestServer is the server API for Test service. +// All implementations must embed UnimplementedTestServer +// for forward compatibility +type TestServer interface { + Test(context.Context, *TestRequest) (*TestResponse, error) + mustEmbedUnimplementedTestServer() +} + +// UnimplementedTestServer must be embedded to have forward compatible implementations. +type UnimplementedTestServer struct { +} + +func (UnimplementedTestServer) Test(context.Context, *TestRequest) (*TestResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Test not implemented") +} +func (UnimplementedTestServer) mustEmbedUnimplementedTestServer() {} + +// UnsafeTestServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TestServer will +// result in compilation errors. +type UnsafeTestServer interface { + mustEmbedUnimplementedTestServer() +} + +func RegisterTestServer(s grpc.ServiceRegistrar, srv TestServer) { + s.RegisterService(&Test_ServiceDesc, srv) +} + +func _Test_Test_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TestRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TestServer).Test(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Test_Test_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TestServer).Test(ctx, req.(*TestRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Test_ServiceDesc is the grpc.ServiceDesc for Test service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Test_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "test.Test", + HandlerType: (*TestServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Test", + Handler: _Test_Test_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "test_service.proto", +} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go index a9c1bed21..2ee0db75e 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go @@ -28,13 +28,21 @@ import ( "google.golang.org/grpc/status" ) +type ServerInterceptor interface { + Unary() grpc.UnaryServerInterceptor + Stream() grpc.StreamServerInterceptor +} + type metricsHandler struct { reqCounter *prometheus.CounterVec reqHistogram *prometheus.HistogramVec reqInFlight *prometheus.GaugeVec } -func New(opts ...Option) grpc.UnaryServerInterceptor { +func (h *metricsHandler) Unary() grpc.UnaryServerInterceptor { return h.observeUnaryRequest } +func (h *metricsHandler) Stream() grpc.StreamServerInterceptor { return h.observeStreamRequest } + +func New(opts ...Option) ServerInterceptor { options := defaultOptions for _, opt := range opts { @@ -80,10 +88,10 @@ func New(opts ...Option) grpc.UnaryServerInterceptor { reqInFlight: gauge, } - return handler.observeRequest + return handler } -func (h *metricsHandler) observeRequest( +func (h *metricsHandler) observeUnaryRequest( ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (any, error) { const ( @@ -125,3 +133,39 @@ func (h *metricsHandler) observeRequest( return resp, err } + +func (h *metricsHandler) observeStreamRequest( + srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler, +) error { + const ( + MagicNumber = 1e9 + Unknown = "unknown" + ) + + start := time.Now() + grpcMethod := info.FullMethod + grpcCode := "0" + httpMethod := Unknown + httpPath := Unknown + httpCode := Unknown + + h.reqInFlight.WithLabelValues(httpMethod, grpcMethod).Inc() + + defer func() { + h.reqInFlight.WithLabelValues(httpMethod, grpcMethod).Dec() + }() + + err := handler(srv, stream) + + if err != nil { + s, _ := status.FromError(err) + grpcCode = strconv.Itoa(int(s.Code())) + } + + h.reqCounter.WithLabelValues(httpCode, httpMethod, httpPath, grpcMethod, grpcCode).Inc() + + elapsed := float64(time.Since(start).Nanoseconds()) / MagicNumber + h.reqHistogram.WithLabelValues(httpCode, httpMethod, httpPath, grpcMethod, grpcCode).Observe(elapsed) + + return err +} diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go index e02d9ec81..630e0bdc5 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go @@ -36,6 +36,7 @@ import ( "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" + testservice2 "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/mocks" "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" ) @@ -59,9 +60,7 @@ func getLabel(labels []*dto.LabelPair, name string) string { return "" } -func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx - t.Parallel() - +func TestHandlerObserveKnownRequests(t *testing.T) { //nolint:maintidx for _, tc := range []struct { uc string configureMock func(t *testing.T, srv *mocks.MockHandler) @@ -268,7 +267,7 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx WithSubsystem("bar"), WithLabel("baz", "zab"), WithServiceName("foobar"), - ))) + ).Unary())) envoy_auth.RegisterAuthorizationServer(srv, handler) go func() { @@ -304,3 +303,95 @@ func TestHandlerObserveRequests(t *testing.T) { //nolint:maintidx }) } } + +func TestHandlerObserveUnknownRequests(t *testing.T) { + // GIVEN + registry := prometheus.NewRegistry() + lis := bufconn.Listen(1024 * 1024) + handler := &mocks.MockHandler{} + bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } + conn, err := grpc.DialContext(context.Background(), "bufnet", + grpc.WithContextDialer(bufDialer), + grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + defer conn.Close() + + metricsIntercepter := New( + WithRegisterer(registry), + WithNamespace("foo"), + WithSubsystem("bar"), + WithLabel("baz", "zab"), + WithServiceName("foobar"), + ) + srv := grpc.NewServer( + grpc.UnknownServiceHandler(func(srv interface{}, stream grpc.ServerStream) error { + return status.Error(codes.Unknown, "unknown service or method") + }), + grpc.StreamInterceptor(metricsIntercepter.Stream())) + + envoy_auth.RegisterAuthorizationServer(srv, handler) + + go func() { + err = srv.Serve(lis) + require.NoError(t, err) + }() + + client := testservice2.NewTestClient(conn) + + // WHEN + // we're not interested in the response and the error + // nolint: errcheck + _, err = client.Test(context.Background(), &testservice2.TestRequest{}) + + // THEN + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown service or method") + srv.Stop() + + metrics, err := registry.Gather() + require.NoError(t, err) + handler.AssertExpectations(t) + + assert.Len(t, metrics, 3) + histMetric := metricForType(metrics, dto.MetricType_HISTOGRAM.Enum()) + assert.Equal(t, "foo_bar_request_duration_seconds", histMetric.GetName()) + assert.Equal(t, "Duration of all requests by tunneled HTTP status code, service and method, "+ + "as well as by GRPC method and status code.", histMetric.GetHelp()) + require.Len(t, histMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(histMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(histMetric.Metric[0].Label, "service")) + assert.Equal(t, "2", getLabel(histMetric.Metric[0].Label, "grpc_code")) + assert.Equal(t, "/test.Test/Test", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "unknown", getLabel(histMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "unknown", getLabel(histMetric.Metric[0].Label, "http_path")) + assert.Equal(t, "unknown", getLabel(histMetric.Metric[0].Label, "http_code")) + require.Len(t, histMetric.Metric[0].Histogram.Bucket, 21) + + gaugeMetric := metricForType(metrics, dto.MetricType_GAUGE.Enum()) + assert.Equal(t, "foo_bar_requests_in_progress_total", gaugeMetric.GetName()) + assert.Equal(t, "All the requests in progress by tunneled HTTP method, "+ + "as well as by GRPC method.", gaugeMetric.GetHelp()) + assert.Equal(t, "zab", getLabel(gaugeMetric.Metric[0].Label, "baz")) + assert.Equal(t, "/test.Test/Test", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "unknown", getLabel(gaugeMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "foobar", getLabel(gaugeMetric.Metric[0].Label, "service")) + require.Equal(t, 0.0, gaugeMetric.Metric[0].Gauge.GetValue()) + + counterMetric := metricForType(metrics, dto.MetricType_COUNTER.Enum()) + assert.Equal(t, "foo_bar_requests_total", counterMetric.GetName()) + assert.Equal(t, "Count all requests by tunneled HTTP status code, service and method, "+ + "as well as by GRPC method and status code.", counterMetric.GetHelp()) + require.Len(t, counterMetric.Metric, 1) + assert.Equal(t, "zab", getLabel(counterMetric.Metric[0].Label, "baz")) + assert.Equal(t, "foobar", getLabel(counterMetric.Metric[0].Label, "service")) + assert.Equal(t, "2", getLabel(histMetric.Metric[0].Label, "grpc_code")) + assert.Equal(t, "/test.Test/Test", + getLabel(histMetric.Metric[0].Label, "grpc_method")) + assert.Equal(t, "unknown", getLabel(counterMetric.Metric[0].Label, "http_method")) + assert.Equal(t, "unknown", getLabel(counterMetric.Metric[0].Label, "http_path")) + assert.Equal(t, "unknown", getLabel(counterMetric.Metric[0].Label, "http_code")) + require.Equal(t, 1.0, counterMetric.Metric[0].Counter.GetValue()) +} diff --git a/internal/handler/envoyextauth/grpcv3/service.go b/internal/handler/envoyextauth/grpcv3/service.go index 0f53279fa..a6fdd85f7 100644 --- a/internal/handler/envoyextauth/grpcv3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -23,7 +23,9 @@ import ( "github.com/rs/zerolog" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/keepalive" + "google.golang.org/grpc/status" "github.com/dadrus/heimdall/internal/cache" "github.com/dadrus/heimdall/internal/config" @@ -45,22 +47,28 @@ func newService( signer heimdall.JWTSigner, ) *grpc.Server { service := conf.Serve.Decision + accessLogger := accesslogmiddleware.New(logger) - interceptors := []grpc.UnaryServerInterceptor{ + streamInterceptors := []grpc.StreamServerInterceptor{ + grpc_recovery.StreamServerInterceptor(), + otelgrpc.StreamServerInterceptor(), + } + + unaryInterceptors := []grpc.UnaryServerInterceptor{ grpc_recovery.UnaryServerInterceptor(), otelgrpc.UnaryServerInterceptor(), } if conf.Metrics.Enabled { - interceptors = append(interceptors, - prometheusmiddleware.New( - prometheusmiddleware.WithServiceName("decision"), - prometheusmiddleware.WithRegisterer(registerer), - ), + metrics := prometheusmiddleware.New( + prometheusmiddleware.WithServiceName("decision"), + prometheusmiddleware.WithRegisterer(registerer), ) + unaryInterceptors = append(unaryInterceptors, metrics.Unary()) + streamInterceptors = append(streamInterceptors, metrics.Stream()) } - interceptors = append(interceptors, + unaryInterceptors = append(unaryInterceptors, errormiddleware.New( errormiddleware.WithVerboseErrors(service.Respond.Verbose), errormiddleware.WithPreconditionErrorCode(service.Respond.With.ArgumentError.Code), @@ -71,14 +79,20 @@ func newService( errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), ), - accesslogmiddleware.New(logger), + accessLogger.Unary(), loggermiddleware.New(logger), cachemiddleware.New(cch), ) + streamInterceptors = append(streamInterceptors, accessLogger.Stream()) + srv := grpc.NewServer( grpc.KeepaliveParams(keepalive.ServerParameters{Timeout: service.Timeout.Idle}), - grpc.ChainUnaryInterceptor(interceptors...), + grpc.UnknownServiceHandler(func(srv interface{}, stream grpc.ServerStream) error { + return status.Error(codes.Unknown, "unknown service or method") + }), + grpc.ChainUnaryInterceptor(unaryInterceptors...), + grpc.ChainStreamInterceptor(streamInterceptors...), ) envoy_auth.RegisterAuthorizationServer(srv, &Handler{r: repository, s: signer}) From 1969e4179182cf7018d1c293e5b48024ed61a55b Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 18:51:19 +0100 Subject: [PATCH 36/67] linter warnings resolved --- .../envoyextauth/grpcv3/middleware/prometheus/interceptor.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go index 2ee0db75e..bc0dae0f8 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor.go @@ -156,7 +156,6 @@ func (h *metricsHandler) observeStreamRequest( }() err := handler(srv, stream) - if err != nil { s, _ := status.FromError(err) grpcCode = strconv.Itoa(int(s.Code())) From d2906d8dd9c909e9e1aa6ae15edeff9efced099e Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 18:52:40 +0100 Subject: [PATCH 37/67] license header added --- .../grpcv3/middleware/mocks/generate.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/mocks/generate.go b/internal/handler/envoyextauth/grpcv3/middleware/mocks/generate.go index 34baa52c1..b46aaf691 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/mocks/generate.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/mocks/generate.go @@ -1,3 +1,19 @@ +// Copyright 2023 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + package mocks //go:generate protoc --go_out=. --go_opt=paths=source_relative test_service.proto From 7664ff9ac666de3f67bf097b469f3f6cfd4f9e78 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 19:40:48 +0100 Subject: [PATCH 38/67] some updates to old tests --- internal/handler/decision/handler_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/handler/decision/handler_test.go b/internal/handler/decision/handler_test.go index d5a6df7c6..51af6da14 100644 --- a/internal/handler/decision/handler_test.go +++ b/internal/handler/decision/handler_test.go @@ -31,7 +31,6 @@ import ( "github.com/dadrus/heimdall/internal/cache/mocks" "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/handler/requestcontext" "github.com/dadrus/heimdall/internal/heimdall" mocks2 "github.com/dadrus/heimdall/internal/rules/mocks" mocks4 "github.com/dadrus/heimdall/internal/rules/rule/mocks" @@ -123,7 +122,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { }, }, { - uc: "rule execution fails with pipeline authorization error", + uc: "rule execution fails with authorization error", createRequest: func(t *testing.T) *http.Request { t.Helper() @@ -133,7 +132,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { + rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { ctx.SetPipelineError(heimdall.ErrAuthorization) return true @@ -174,7 +173,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { + rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { ctx.AddHeaderForUpstream("X-Foo-Bar", "baz") ctx.AddCookieForUpstream("X-Bar-Foo", "zab") @@ -226,7 +225,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { + rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { ctx.AddHeaderForUpstream("X-Foo-Bar", "baz") ctx.AddCookieForUpstream("X-Bar-Foo", "zab") @@ -279,7 +278,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { + rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { ctx.AddHeaderForUpstream("X-Foo-Bar", "baz") ctx.AddCookieForUpstream("X-Bar-Foo", "zab") From 8aba38a82d820d9dd99063c40bcd4430f0c6411e Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 19:41:31 +0100 Subject: [PATCH 39/67] small test refactorings to simplify them --- .../grpcv3/middleware/accesslog/interceptor_test.go | 5 +---- .../grpcv3/middleware/prometheus/interceptor_test.go | 6 ++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go index 295dafc03..a1e13a9c2 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go @@ -236,11 +236,8 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { tb := &testsupport.TestingLog{TB: t} logger := zerolog.New(zerolog.TestWriter{T: tb}) handler := &service_mocks.MockHandler{} - bufDialer := func(context.Context, string) (net.Conn, error) { - return lis.Dial() - } conn, err := grpc.DialContext(context.Background(), "bufnet", - grpc.WithContextDialer(bufDialer), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }), grpc.WithTransportCredentials(insecure.NewCredentials())) require.NoError(t, err) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go index 630e0bdc5..40c734785 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go @@ -251,9 +251,8 @@ func TestHandlerObserveKnownRequests(t *testing.T) { //nolint:maintidx registry := prometheus.NewRegistry() lis := bufconn.Listen(1024 * 1024) handler := &mocks.MockHandler{} - bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } conn, err := grpc.DialContext(context.Background(), "bufnet", - grpc.WithContextDialer(bufDialer), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }), grpc.WithTransportCredentials(insecure.NewCredentials())) require.NoError(t, err) @@ -309,9 +308,8 @@ func TestHandlerObserveUnknownRequests(t *testing.T) { registry := prometheus.NewRegistry() lis := bufconn.Listen(1024 * 1024) handler := &mocks.MockHandler{} - bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } conn, err := grpc.DialContext(context.Background(), "bufnet", - grpc.WithContextDialer(bufDialer), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }), grpc.WithTransportCredentials(insecure.NewCredentials())) require.NoError(t, err) From ebd76325d5c206d5b6847397d479e088ce0f8904 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 19:42:06 +0100 Subject: [PATCH 40/67] new tests and fixes for found bugs --- .../envoyextauth/grpcv3/handler_test.go | 222 ++++++++++++++++++ .../handler/envoyextauth/grpcv3/service.go | 7 +- 2 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 internal/handler/envoyextauth/grpcv3/handler_test.go diff --git a/internal/handler/envoyextauth/grpcv3/handler_test.go b/internal/handler/envoyextauth/grpcv3/handler_test.go new file mode 100644 index 000000000..15d991683 --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/handler_test.go @@ -0,0 +1,222 @@ +package grpcv3 + +import ( + "context" + "net" + "net/http" + "testing" + + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + + "github.com/dadrus/heimdall/internal/cache/mocks" + "github.com/dadrus/heimdall/internal/config" + "github.com/dadrus/heimdall/internal/heimdall" + mocks2 "github.com/dadrus/heimdall/internal/rules/mocks" + mocks4 "github.com/dadrus/heimdall/internal/rules/rule/mocks" +) + +func TestHandleDecisionEndpointRequest(t *testing.T) { + for _, tc := range []struct { + uc string + serviceConf config.ServiceConfig + configureMocks func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) + assertResponse func(t *testing.T, err error, response *envoy_auth.CheckResponse) + }{ + { + uc: "no rules configured", + configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + t.Helper() + + repository.On("FindRule", mock.Anything).Return(nil, heimdall.ErrNoRuleFound) + }, + assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.NoError(t, err) + assert.Equal(t, int32(http.StatusNotFound), response.Status.Code) + + deniedResponse := response.GetDeniedResponse() + require.NotNil(t, deniedResponse) + assert.Equal(t, typev3.StatusCode(http.StatusNotFound), deniedResponse.Status.Code) + assert.Len(t, deniedResponse.Body, 0) + assert.Empty(t, deniedResponse.Headers) + }, + }, + { + uc: "rule doesn't match method", + configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + t.Helper() + + rule.On("MatchesMethod", http.MethodPost).Return(false) + + repository.On("FindRule", mock.Anything).Return(rule, nil) + }, + assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.NoError(t, err) + assert.Equal(t, int32(http.StatusMethodNotAllowed), response.Status.Code) + + deniedResponse := response.GetDeniedResponse() + require.NotNil(t, deniedResponse) + assert.Equal(t, typev3.StatusCode(http.StatusMethodNotAllowed), deniedResponse.Status.Code) + assert.Len(t, deniedResponse.Body, 0) + assert.Empty(t, deniedResponse.Headers) + }, + }, + { + uc: "rule execution fails with authentication error", + configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + t.Helper() + + rule.On("MatchesMethod", http.MethodPost).Return(true) + rule.On("Execute", mock.Anything).Return(nil, heimdall.ErrAuthentication) + + repository.On("FindRule", mock.Anything).Return(rule, nil) + }, + assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.NoError(t, err) + assert.Equal(t, int32(http.StatusUnauthorized), response.Status.Code) + + deniedResponse := response.GetDeniedResponse() + require.NotNil(t, deniedResponse) + assert.Equal(t, typev3.StatusCode(http.StatusUnauthorized), deniedResponse.Status.Code) + assert.Len(t, deniedResponse.Body, 0) + assert.Empty(t, deniedResponse.Headers) + }, + }, + { + uc: "rule execution fails with authorization error", + configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + t.Helper() + + rule.On("MatchesMethod", http.MethodPost).Return(true) + rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { + ctx.SetPipelineError(heimdall.ErrAuthorization) + + return true + })).Return(nil, nil) + + repository.On("FindRule", mock.Anything).Return(rule, nil) + }, + assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.NoError(t, err) + assert.Equal(t, int32(http.StatusForbidden), response.Status.Code) + + deniedResponse := response.GetDeniedResponse() + require.NotNil(t, deniedResponse) + assert.Equal(t, typev3.StatusCode(http.StatusForbidden), deniedResponse.Status.Code) + assert.Len(t, deniedResponse.Body, 0) + assert.Empty(t, deniedResponse.Headers) + }, + }, + { + uc: "rule execution fails with a redirect", + configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + t.Helper() + + rule.On("MatchesMethod", http.MethodPost).Return(true) + rule.On("Execute", mock.Anything).Return(nil, &heimdall.RedirectError{ + Message: "test redirect", + Code: http.StatusFound, + RedirectTo: "http://foo.bar", + }) + + repository.On("FindRule", mock.Anything).Return(rule, nil) + }, + assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.NoError(t, err) + assert.Equal(t, int32(http.StatusFound), response.Status.Code) + + deniedResponse := response.GetDeniedResponse() + require.NotNil(t, deniedResponse) + assert.Equal(t, typev3.StatusCode(http.StatusFound), deniedResponse.Status.Code) + assert.Len(t, deniedResponse.Body, 0) + assert.Len(t, deniedResponse.Headers, 1) + assert.Equal(t, "Location", deniedResponse.Headers[0].Header.Key) + assert.Equal(t, "http://foo.bar", deniedResponse.Headers[0].Header.Value) + }, + }, + { + uc: "rule execution succeeds", + configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + t.Helper() + + rule.On("MatchesMethod", http.MethodPost).Return(true) + rule.On("Execute", mock.Anything).Return(nil, nil) + + repository.On("FindRule", mock.Anything).Return(rule, nil) + }, + assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.NoError(t, err) + assert.Equal(t, int32(http.StatusOK), response.Status.Code) + + okResponse := response.GetOkResponse() + require.NotNil(t, okResponse) + assert.Empty(t, okResponse.Headers) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + lis := bufconn.Listen(1024 * 1024) + conn, err := grpc.DialContext(context.Background(), "bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + conf := &config.Configuration{Serve: config.ServeConfig{Decision: tc.serviceConf}} + cch := &mocks.MockCache{} + repo := &mocks2.MockRepository{} + rule := &mocks4.MockRule{} + + tc.configureMocks(t, repo, rule) + + srv := newService(conf, prometheus.NewRegistry(), cch, log.Logger, repo, nil) + + defer srv.Stop() + + go func() { + err := srv.Serve(lis) + require.NoError(t, err) + }() + + client := envoy_auth.NewAuthorizationClient(conn) + + // WHEN + resp, err := client.Check(context.Background(), &envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Request: &envoy_auth.AttributeContext_Request{ + Http: &envoy_auth.AttributeContext_HttpRequest{ + Body: "foo", + Method: http.MethodPost, + Path: "/test", + }, + }, + }, + }) + + // THEN + require.NoError(t, err) + tc.assertResponse(t, err, resp) + repo.AssertExpectations(t) + rule.AssertExpectations(t) + }) + } +} diff --git a/internal/handler/envoyextauth/grpcv3/service.go b/internal/handler/envoyextauth/grpcv3/service.go index a6fdd85f7..8f0f8c429 100644 --- a/internal/handler/envoyextauth/grpcv3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -48,14 +48,17 @@ func newService( ) *grpc.Server { service := conf.Serve.Decision accessLogger := accesslogmiddleware.New(logger) + recoveryHandler := grpc_recovery.WithRecoveryHandler(func(any) error { + return status.Error(codes.Internal, "internal error") + }) streamInterceptors := []grpc.StreamServerInterceptor{ - grpc_recovery.StreamServerInterceptor(), + grpc_recovery.StreamServerInterceptor(recoveryHandler), otelgrpc.StreamServerInterceptor(), } unaryInterceptors := []grpc.UnaryServerInterceptor{ - grpc_recovery.UnaryServerInterceptor(), + grpc_recovery.UnaryServerInterceptor(recoveryHandler), otelgrpc.UnaryServerInterceptor(), } From 438f18dca66923bff1c59e577c87cb476ea400cc Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 19:45:41 +0100 Subject: [PATCH 41/67] more tests --- .../handler/envoyextauth/grpcv3/handler_test.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/handler_test.go b/internal/handler/envoyextauth/grpcv3/handler_test.go index 15d991683..04f4b1d01 100644 --- a/internal/handler/envoyextauth/grpcv3/handler_test.go +++ b/internal/handler/envoyextauth/grpcv3/handler_test.go @@ -173,6 +173,18 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { assert.Empty(t, okResponse.Headers) }, }, + { + uc: "server panics and error does not contain traces", + configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + t.Helper() + }, + assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.Error(t, err) + assert.Equal(t, "rpc error: code = Internal desc = internal error", err.Error()) + }, + }, } { t.Run("case="+tc.uc, func(t *testing.T) { // GIVEN @@ -213,7 +225,6 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { }) // THEN - require.NoError(t, err) tc.assertResponse(t, err, resp) repo.AssertExpectations(t) rule.AssertExpectations(t) From 42ec80992579beb27d892ca680f3df2f8efdebd0 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 23:13:25 +0100 Subject: [PATCH 42/67] new tests and fixes for identified bugs --- .../envoyextauth/grpcv3/request_context.go | 4 +- .../grpcv3/request_context_test.go | 235 ++++++++++++++++++ 2 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 internal/handler/envoyextauth/grpcv3/request_context_test.go diff --git a/internal/handler/envoyextauth/grpcv3/request_context.go b/internal/handler/envoyextauth/grpcv3/request_context.go index 4a12c15e7..2da72ae89 100644 --- a/internal/handler/envoyextauth/grpcv3/request_context.go +++ b/internal/handler/envoyextauth/grpcv3/request_context.go @@ -34,6 +34,7 @@ import ( type RequestContext struct { ctx context.Context // nolint: containedctx + address string reqMethod string reqHeaders map[string]string reqURL *url.URL @@ -48,6 +49,7 @@ type RequestContext struct { func NewRequestContext(ctx context.Context, req *envoy_auth.CheckRequest, signer heimdall.JWTSigner) *RequestContext { return &RequestContext{ ctx: ctx, + address: req.Attributes.Source.GetAddress().GetSocketAddress().GetAddress(), reqMethod: req.Attributes.Request.Http.Method, reqHeaders: canonicalizeHeaders(req.Attributes.Request.Http.Headers), reqURL: &url.URL{ @@ -118,7 +120,7 @@ func (s *RequestContext) AddHeaderForUpstream(name, value string) { s.upstreamHe func (s *RequestContext) AddCookieForUpstream(name, value string) { s.upstreamCookies[name] = value } func (s *RequestContext) Signer() heimdall.JWTSigner { return s.jwtSigner } func (s *RequestContext) RequestURL() *url.URL { return s.reqURL } -func (s *RequestContext) RequestClientIPs() []string { return nil } +func (s *RequestContext) RequestClientIPs() []string { return []string{s.address} } func (s *RequestContext) Finalize() (*envoy_auth.CheckResponse, error) { if s.err != nil { diff --git a/internal/handler/envoyextauth/grpcv3/request_context_test.go b/internal/handler/envoyextauth/grpcv3/request_context_test.go new file mode 100644 index 000000000..5714fc795 --- /dev/null +++ b/internal/handler/envoyextauth/grpcv3/request_context_test.go @@ -0,0 +1,235 @@ +package grpcv3 + +import ( + "context" + "errors" + "net/http" + "strings" + "testing" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/heimdall/mocks" +) + +func TestNewRequestContext(t *testing.T) { + t.Parallel() + + // GIVEN + httpReq := &envoy_auth.AttributeContext_HttpRequest{ + Method: http.MethodPatch, + Scheme: "https", + Host: "foo.bar:8080", + Path: "/test", + Query: "bar=moo", + Fragment: "foobar", + Body: "content=heimdall", + RawBody: []byte("content=heimdall"), + Headers: map[string]string{ + "x-foo-bar": "barfoo", + "cookie": "bar=foo;foo=baz", + "content-type": "application/x-www-form-urlencoded", + }, + } + checkReq := &envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Source: &envoy_auth.AttributeContext_Peer{ + Address: &corev3.Address{ + Address: &corev3.Address_SocketAddress{ + SocketAddress: &corev3.SocketAddress{ + Address: "0.0.0.0", + }, + }, + }, + }, + Request: &envoy_auth.AttributeContext_Request{ + Http: httpReq, + }, + }, + } + ctx := NewRequestContext(context.Background(), checkReq, &mocks.MockJWTSigner{}) + + // THEN + require.Equal(t, httpReq.Method, ctx.RequestMethod()) + require.Equal(t, httpReq.Scheme, ctx.RequestURL().Scheme) + require.Equal(t, httpReq.Host, ctx.RequestURL().Host) + require.Equal(t, httpReq.Path, ctx.RequestURL().Path) + require.Equal(t, httpReq.Fragment, ctx.RequestURL().Fragment) + require.Equal(t, httpReq.Query, ctx.RequestURL().RawQuery) + require.Equal(t, "moo", ctx.RequestQueryParameter("bar")) + require.Equal(t, httpReq.RawBody, ctx.RequestBody()) + require.Empty(t, ctx.RequestFormParameter("foo")) + require.Equal(t, "heimdall", ctx.RequestFormParameter("content")) + require.Len(t, ctx.RequestHeaders(), 3) + require.Equal(t, "barfoo", ctx.RequestHeader("X-Foo-Bar")) + require.Equal(t, "foo", ctx.RequestCookie("bar")) + require.Equal(t, "baz", ctx.RequestCookie("foo")) + require.Empty(t, ctx.RequestCookie("baz")) + require.NotNil(t, ctx.AppContext()) + require.NotNil(t, ctx.Signer()) + require.Len(t, ctx.RequestClientIPs(), 1) + assert.Contains(t, ctx.RequestClientIPs(), "0.0.0.0") +} + +func TestFinalizeRequestContext(t *testing.T) { + t.Parallel() + + findHeader := func(headers []*corev3.HeaderValueOption, name string) *corev3.HeaderValue { + for _, header := range headers { + if header.Header.Key == name { + return header.Header + } + } + + return nil + } + + for _, tc := range []struct { + uc string + updateContext func(t *testing.T, ctx heimdall.Context) + assert func(t *testing.T, err error, response *envoy_auth.CheckResponse) + }{ + { + uc: "successful with some header", + updateContext: func(t *testing.T, ctx heimdall.Context) { + t.Helper() + + ctx.AddHeaderForUpstream("x-for-upstream-1", "some-value-1") + ctx.AddHeaderForUpstream("x-for-upstream-2", "some-value-2") + ctx.AddHeaderForUpstream("x-for-upstream-1", "some-value-3") + }, + assert: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.NoError(t, err) + require.NotNil(t, response) + + assert.Equal(t, int32(http.StatusOK), response.Status.Code) + + okResponse := response.GetOkResponse() + require.NotNil(t, okResponse) + + require.Len(t, okResponse.Headers, 2) + + header := findHeader(okResponse.Headers, "X-For-Upstream-1") + require.NotNil(t, header) + assert.Equal(t, "some-value-1,some-value-3", header.Value) + header = findHeader(okResponse.Headers, "X-For-Upstream-2") + require.NotNil(t, header) + assert.Equal(t, "some-value-2", header.Value) + }, + }, + { + uc: "successful with some cookies", + updateContext: func(t *testing.T, ctx heimdall.Context) { + t.Helper() + + ctx.AddCookieForUpstream("some-cookie", "value-1") + ctx.AddCookieForUpstream("some-other-cookie", "value-2") + }, + assert: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.NoError(t, err) + require.NotNil(t, response) + + assert.Equal(t, int32(http.StatusOK), response.Status.Code) + + okResponse := response.GetOkResponse() + require.NotNil(t, okResponse) + + require.Len(t, okResponse.Headers, 1) + assert.Equal(t, "Cookie", okResponse.Headers[0].Header.Key) + values := strings.Split(okResponse.Headers[0].Header.Value, ";") + assert.Len(t, values, 2) + assert.Contains(t, okResponse.Headers[0].Header.Value, "some-cookie=value-1") + assert.Contains(t, okResponse.Headers[0].Header.Value, "some-other-cookie=value-2") + }, + }, + { + uc: "successful with header and cookie", + updateContext: func(t *testing.T, ctx heimdall.Context) { + t.Helper() + + ctx.AddHeaderForUpstream("x-for-upstream", "some-value") + ctx.AddCookieForUpstream("some-cookie", "value-1") + }, + assert: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.NoError(t, err) + require.NotNil(t, response) + + assert.Equal(t, int32(http.StatusOK), response.Status.Code) + + okResponse := response.GetOkResponse() + require.NotNil(t, okResponse) + + require.Len(t, okResponse.Headers, 2) + header := findHeader(okResponse.Headers, "X-For-Upstream") + require.NotNil(t, header) + assert.Equal(t, "some-value", header.Value) + header = findHeader(okResponse.Headers, "Cookie") + require.NotNil(t, header) + assert.Equal(t, "some-cookie=value-1", header.Value) + }, + }, + { + uc: "erroneous with header and cookie", + updateContext: func(t *testing.T, ctx heimdall.Context) { + t.Helper() + + ctx.SetPipelineError(errors.New("test error")) // nolint: goerr113 + ctx.AddHeaderForUpstream("x-for-upstream", "some-value") + ctx.AddCookieForUpstream("some-cookie", "value-1") + ctx.AddCookieForUpstream("some-other-cookie", "value-2") + }, + assert: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { + t.Helper() + + require.Error(t, err) + assert.Equal(t, err.Error(), "test error") + require.Nil(t, response) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + httpReq := &envoy_auth.AttributeContext_HttpRequest{ + Method: http.MethodPatch, + Scheme: "https", + Host: "foo.bar:8080", + Path: "/test", + Query: "bar=moo", + Fragment: "foobar", + Body: "content=heimdall", + RawBody: []byte("content=heimdall"), + Headers: map[string]string{ + "x-foo-bar": "barfoo", + "cookie": "bar=foo;foo=baz", + "content-type": "application/x-www-form-urlencoded", + }, + } + checkReq := &envoy_auth.CheckRequest{ + Attributes: &envoy_auth.AttributeContext{ + Request: &envoy_auth.AttributeContext_Request{ + Http: httpReq, + }, + }, + } + ctx := NewRequestContext(context.Background(), checkReq, nil) + + tc.updateContext(t, ctx) + + // WHEN + resp, err := ctx.Finalize() + + // THEN + tc.assert(t, err, resp) + }) + } +} From b9691c2212ac48d32335003e498c4805f4bdecc7 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 23:55:09 +0100 Subject: [PATCH 43/67] test enhanced --- cmd/serve/proxy_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/serve/proxy_test.go b/cmd/serve/proxy_test.go index 7c6b13932..de9a1c08c 100644 --- a/cmd/serve/proxy_test.go +++ b/cmd/serve/proxy_test.go @@ -20,7 +20,6 @@ import ( "strconv" "testing" - "github.com/spf13/cobra" "github.com/stretchr/testify/require" "github.com/dadrus/heimdall/internal/x/testsupport" @@ -38,6 +37,6 @@ func TestCreateProxyApp(t *testing.T) { t.Setenv("SERVE_PROXY_PORT", strconv.Itoa(port1)) t.Setenv("SERVE_MANAGEMENT_PORT", strconv.Itoa(port2)) - _, err = createProxyApp(&cobra.Command{}) + _, err = createProxyApp(NewProxyCommand()) require.NoError(t, err) } From 0b408260090b01f84eb552c689345320a0725037 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 23:55:46 +0100 Subject: [PATCH 44/67] new test to bootstrap envoyproxy decision service --- cmd/serve/decision_test.go | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/cmd/serve/decision_test.go b/cmd/serve/decision_test.go index c5778d8cd..dd39a2bf8 100644 --- a/cmd/serve/decision_test.go +++ b/cmd/serve/decision_test.go @@ -20,13 +20,12 @@ import ( "strconv" "testing" - "github.com/spf13/cobra" "github.com/stretchr/testify/require" "github.com/dadrus/heimdall/internal/x/testsupport" ) -func TestCreateDecisionApp(t *testing.T) { +func TestCreateDecisionAppForHTTPRequests(t *testing.T) { // this test verifies that all dependencies are resolved // and nothing has been forgotten port1, err := testsupport.GetFreePort() @@ -38,6 +37,26 @@ func TestCreateDecisionApp(t *testing.T) { t.Setenv("SERVE_DECISION_PORT", strconv.Itoa(port1)) t.Setenv("SERVE_MANAGEMENT_PORT", strconv.Itoa(port2)) - _, err = createDecisionApp(&cobra.Command{}) + _, err = createDecisionApp(NewDecisionCommand()) + require.NoError(t, err) +} + +func TestCreateDecisionAppForEnvoyGRPCRequests(t *testing.T) { + // this test verifies that all dependencies are resolved + // and nothing has been forgotten + port1, err := testsupport.GetFreePort() + require.NoError(t, err) + + port2, err := testsupport.GetFreePort() + require.NoError(t, err) + + t.Setenv("SERVE_DECISION_PORT", strconv.Itoa(port1)) + t.Setenv("SERVE_MANAGEMENT_PORT", strconv.Itoa(port2)) + + cmd := NewDecisionCommand() + err = cmd.ParseFlags([]string{"--envoy-extauth=true"}) + require.NoError(t, err) + + _, err = createDecisionApp(cmd) require.NoError(t, err) } From fd8bfd32962eb6ff822745be411a1355d5cfa5ee Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 23:56:24 +0100 Subject: [PATCH 45/67] metrics configured in test --- internal/handler/envoyextauth/grpcv3/handler_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/handler_test.go b/internal/handler/envoyextauth/grpcv3/handler_test.go index 04f4b1d01..8e1e22b9c 100644 --- a/internal/handler/envoyextauth/grpcv3/handler_test.go +++ b/internal/handler/envoyextauth/grpcv3/handler_test.go @@ -27,7 +27,6 @@ import ( func TestHandleDecisionEndpointRequest(t *testing.T) { for _, tc := range []struct { uc string - serviceConf config.ServiceConfig configureMocks func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) assertResponse func(t *testing.T, err error, response *envoy_auth.CheckResponse) }{ @@ -193,7 +192,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }), grpc.WithTransportCredentials(insecure.NewCredentials())) require.NoError(t, err) - conf := &config.Configuration{Serve: config.ServeConfig{Decision: tc.serviceConf}} + conf := &config.Configuration{Metrics: config.MetricsConfig{Enabled: true}} cch := &mocks.MockCache{} repo := &mocks2.MockRepository{} rule := &mocks4.MockRule{} From 96c5a5b052b4a02ee60214778a4aed7ed9b46f29 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 3 Mar 2023 23:58:47 +0100 Subject: [PATCH 46/67] mock moved --- .../middleware/accesslog/interceptor_test.go | 19 +++++++++---------- .../errorhandler/interceptor_test.go | 2 +- .../middleware/logger/interceptor_test.go | 2 +- .../grpcv3/{ => middleware}/mocks/handler.go | 0 .../middleware/prometheus/interceptor_test.go | 7 +++---- 5 files changed, 14 insertions(+), 16 deletions(-) rename internal/handler/envoyextauth/grpcv3/{ => middleware}/mocks/handler.go (100%) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go index a1e13a9c2..6c48558c1 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go @@ -45,8 +45,7 @@ import ( "google.golang.org/grpc/test/bufconn" "github.com/dadrus/heimdall/internal/accesscontext" - grpc_mocks "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/mocks" - service_mocks "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" + "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/mocks" "github.com/dadrus/heimdall/internal/x/testsupport" ) @@ -61,7 +60,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { for _, tc := range []struct { uc string outgoingContext func(t *testing.T) context.Context - configureMock func(t *testing.T, m *service_mocks.MockHandler) + configureMock func(t *testing.T, m *mocks.MockHandler) assert func(t *testing.T, logEvent1, logEvent2 map[string]any) }{ { @@ -71,7 +70,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { return context.Background() }, - configureMock: func(t *testing.T, m *service_mocks.MockHandler) { + configureMock: func(t *testing.T, m *mocks.MockHandler) { t.Helper() m.On("Check", @@ -133,7 +132,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { return metadata.NewOutgoingContext(context.Background(), metadata.New(md)) }, - configureMock: func(t *testing.T, m *service_mocks.MockHandler) { + configureMock: func(t *testing.T, m *mocks.MockHandler) { t.Helper() m.On("Check", mock.Anything, mock.Anything). @@ -177,7 +176,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { return context.Background() }, - configureMock: func(t *testing.T, m *service_mocks.MockHandler) { + configureMock: func(t *testing.T, m *mocks.MockHandler) { t.Helper() m.On("Check", @@ -235,7 +234,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { lis := bufconn.Listen(1024 * 1024) tb := &testsupport.TestingLog{TB: t} logger := zerolog.New(zerolog.TestWriter{T: tb}) - handler := &service_mocks.MockHandler{} + handler := &mocks.MockHandler{} conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }), grpc.WithTransportCredentials(insecure.NewCredentials())) @@ -301,7 +300,7 @@ func TestAccessLogInterceptorForUnknownService(t *testing.T) { lis := bufconn.Listen(1024 * 1024) tb := &testsupport.TestingLog{TB: t} logger := zerolog.New(zerolog.TestWriter{T: tb}) - handler := &service_mocks.MockHandler{} + handler := &mocks.MockHandler{} bufDialer := func(context.Context, string) (net.Conn, error) { return lis.Dial() } @@ -328,11 +327,11 @@ func TestAccessLogInterceptorForUnknownService(t *testing.T) { require.NoError(t, err) }() - client := grpc_mocks.NewTestClient(conn) + client := mocks.NewTestClient(conn) // WHEN // nolint: errcheck - _, err = client.Test(context.Background(), &grpc_mocks.TestRequest{}) + _, err = client.Test(context.Background(), &mocks.TestRequest{}) // THEN require.Error(t, err) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go index a8e8f55cc..a2dcd2bdf 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go @@ -32,7 +32,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" - "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" + "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/mocks" "github.com/dadrus/heimdall/internal/heimdall" ) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go index 8a4979828..855312956 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/logger/interceptor_test.go @@ -39,7 +39,7 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/grpc/test/bufconn" - "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" + "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/mocks" "github.com/dadrus/heimdall/internal/x/testsupport" ) diff --git a/internal/handler/envoyextauth/grpcv3/mocks/handler.go b/internal/handler/envoyextauth/grpcv3/middleware/mocks/handler.go similarity index 100% rename from internal/handler/envoyextauth/grpcv3/mocks/handler.go rename to internal/handler/envoyextauth/grpcv3/middleware/mocks/handler.go diff --git a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go index 40c734785..c76040a92 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/prometheus/interceptor_test.go @@ -36,8 +36,7 @@ import ( "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" - testservice2 "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/mocks" - "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/mocks" + "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/mocks" ) func metricForType(metrics []*dto.MetricFamily, metricType *dto.MetricType) *dto.MetricFamily { @@ -335,12 +334,12 @@ func TestHandlerObserveUnknownRequests(t *testing.T) { require.NoError(t, err) }() - client := testservice2.NewTestClient(conn) + client := mocks.NewTestClient(conn) // WHEN // we're not interested in the response and the error // nolint: errcheck - _, err = client.Test(context.Background(), &testservice2.TestRequest{}) + _, err = client.Test(context.Background(), &mocks.TestRequest{}) // THEN assert.Error(t, err) From 6d2328d112344329fdb4ca43a7109c042b552647 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sat, 4 Mar 2023 00:14:10 +0100 Subject: [PATCH 47/67] new test --- internal/x/errorchain/error_chain_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/x/errorchain/error_chain_test.go b/internal/x/errorchain/error_chain_test.go index 9e8f10567..e5cb24078 100644 --- a/internal/x/errorchain/error_chain_test.go +++ b/internal/x/errorchain/error_chain_test.go @@ -206,6 +206,19 @@ func TestErrorChainAsUsedWithNotAssignableInterface(t *testing.T) { assert.False(t, res) } +func TestErrorChainString(t *testing.T) { + t.Parallel() + + // GIVEN + testErr := errorchain.NewWithMessage(errTest1, "foo").CausedBy(errTest2) + + // WHEN + value := testErr.String() + + // THEN + assert.Equal(t, "test error 1: foo", value) +} + func TestErrorChainJSONMarshal(t *testing.T) { t.Parallel() From fb319b8de5743c39d7945928c7d4f0c2514c5412 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sat, 4 Mar 2023 20:53:33 +0100 Subject: [PATCH 48/67] logged accesslog events updated --- .../middleware/accesslog/interceptor.go | 9 ++++-- .../middleware/accesslog/interceptor_test.go | 32 +++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go index 43a09ec99..37b9ca3da 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go @@ -18,6 +18,7 @@ package accesslog import ( "context" + "net" "strings" "time" @@ -78,8 +79,8 @@ func (i *accessLogInterceptor) startTransaction(ctx context.Context, fullMethod logCtx := i.l.Level(zerolog.InfoLevel).With(). Int64("_tx_start", start.Unix()). - Str("_peer", peerFromCtx(ctx)). - Str("_request", fullMethod) + Str("_client_ip", peerFromCtx(ctx)). + Str("_grpc_method", fullMethod) logCtx = logTraceData(ctx, logCtx) logCtx = logMetaData(logCtx, requestMetadata, "x-forwarded-for", "_x_forwarded_for") @@ -151,5 +152,9 @@ func peerFromCtx(ctx context.Context) string { return "" } + if tcpAddr, ok := p.Addr.(*net.TCPAddr); ok { + return tcpAddr.IP.String() + } + return p.Addr.String() } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go index 6c48558c1..ae019d8fd 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go @@ -93,8 +93,8 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { require.Len(t, logEvent1, 7) assert.Equal(t, "info", logEvent1["level"]) assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_peer") - assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", logEvent1["_request"]) + assert.Contains(t, logEvent1, "_client_ip") + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", logEvent1["_grpc_method"]) assert.Contains(t, logEvent1, "_trace_id") assert.Contains(t, logEvent1, "_trace_id") assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) @@ -105,8 +105,8 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { assert.Equal(t, "info", logEvent2["level"]) assert.Contains(t, logEvent2, "_tx_start") assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_peer") - assert.Equal(t, logEvent1["_request"], logEvent2["_request"]) + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, logEvent1["_grpc_method"], logEvent2["_grpc_method"]) assert.Contains(t, logEvent2, "_trace_id") assert.Contains(t, logEvent2, "_trace_id") assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) @@ -144,8 +144,8 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { require.Len(t, logEvent1, 10) assert.Equal(t, "info", logEvent1["level"]) assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_peer") - assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", logEvent1["_request"]) + assert.Contains(t, logEvent1, "_client_ip") + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", logEvent1["_grpc_method"]) assert.Contains(t, logEvent1, "_span_id") assert.Equal(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) assert.Equal(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) @@ -157,8 +157,8 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { assert.Equal(t, "info", logEvent2["level"]) assert.Contains(t, logEvent2, "_tx_start") assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_peer") - assert.Equal(t, logEvent1["_request"], logEvent2["_request"]) + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, logEvent1["_grpc_method"], logEvent2["_grpc_method"]) assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) assert.Equal(t, logEvent2["_span_id"], logEvent2["_span_id"]) @@ -200,8 +200,8 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { require.Len(t, logEvent1, 7) assert.Equal(t, "info", logEvent1["level"]) assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_peer") - assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", logEvent1["_request"]) + assert.Contains(t, logEvent1, "_client_ip") + assert.Equal(t, "/envoy.service.auth.v3.Authorization/Check", logEvent1["_grpc_method"]) assert.Contains(t, logEvent1, "_trace_id") assert.Contains(t, logEvent1, "_trace_id") assert.NotEqual(t, parentCtx.TraceID().String(), logEvent1["_trace_id"]) @@ -212,8 +212,8 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { assert.Equal(t, "info", logEvent2["level"]) assert.Contains(t, logEvent2, "_tx_start") assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_peer") - assert.Equal(t, logEvent1["_request"], logEvent2["_request"]) + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, logEvent1["_grpc_method"], logEvent2["_grpc_method"]) assert.Contains(t, logEvent2, "_trace_id") assert.Contains(t, logEvent2, "_trace_id") assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) @@ -347,8 +347,8 @@ func TestAccessLogInterceptorForUnknownService(t *testing.T) { require.Len(t, logEvent1, 7) assert.Equal(t, "info", logEvent1["level"]) assert.Contains(t, logEvent1, "_tx_start") - assert.Contains(t, logEvent1, "_peer") - assert.Equal(t, "/test.Test/Test", logEvent1["_request"]) + assert.Contains(t, logEvent1, "_client_ip") + assert.Equal(t, "/test.Test/Test", logEvent1["_grpc_method"]) assert.Contains(t, logEvent1, "_trace_id") assert.Contains(t, logEvent1, "_trace_id") assert.Equal(t, "TX started", logEvent1["message"]) @@ -357,8 +357,8 @@ func TestAccessLogInterceptorForUnknownService(t *testing.T) { assert.Equal(t, "info", logEvent2["level"]) assert.Contains(t, logEvent2, "_tx_start") assert.Contains(t, logEvent2, "_tx_duration_ms") - assert.Contains(t, logEvent2, "_peer") - assert.Equal(t, logEvent1["_request"], logEvent2["_request"]) + assert.Contains(t, logEvent2, "_client_ip") + assert.Equal(t, logEvent1["_grpc_method"], logEvent2["_grpc_method"]) assert.Contains(t, logEvent2, "_trace_id") assert.Contains(t, logEvent2, "_trace_id") assert.Equal(t, logEvent2["_trace_id"], logEvent2["_trace_id"]) From 4bc7a8aa4432bd7915473ac6a62e790423a58305 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sat, 4 Mar 2023 21:29:45 +0100 Subject: [PATCH 49/67] additional check to avoid nil map access --- .../envoyextauth/grpcv3/middleware/accesslog/interceptor.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go index 37b9ca3da..b4c23cc0b 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go @@ -139,6 +139,10 @@ func logTraceData(ctx context.Context, logCtx zerolog.Context) zerolog.Context { } func logMetaData(logCtx zerolog.Context, rmd metadata.MD, mdKey, logKey string) zerolog.Context { + if len(rmd) == 0 { + return logCtx + } + if headerValue := rmd.Get(mdKey); len(headerValue) != 0 { logCtx = logCtx.Str(logKey, strings.Join(headerValue, ",")) } From dee4c868e973b9fadb50e5d79dd1b330efdf31b6 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sat, 4 Mar 2023 21:30:34 +0100 Subject: [PATCH 50/67] using x-forwarded-for set by envoy to retrieve ips of the hops --- .../envoyextauth/grpcv3/request_context.go | 16 +++++++++--- .../grpcv3/request_context_test.go | 25 ++++++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/request_context.go b/internal/handler/envoyextauth/grpcv3/request_context.go index 2da72ae89..c809839de 100644 --- a/internal/handler/envoyextauth/grpcv3/request_context.go +++ b/internal/handler/envoyextauth/grpcv3/request_context.go @@ -27,6 +27,7 @@ import ( 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" + "google.golang.org/grpc/metadata" "github.com/dadrus/heimdall/internal/heimdall" "github.com/dadrus/heimdall/internal/x" @@ -34,7 +35,7 @@ import ( type RequestContext struct { ctx context.Context // nolint: containedctx - address string + ips []string reqMethod string reqHeaders map[string]string reqURL *url.URL @@ -47,9 +48,18 @@ type RequestContext struct { } func NewRequestContext(ctx context.Context, req *envoy_auth.CheckRequest, signer heimdall.JWTSigner) *RequestContext { + var clientIPs []string + + if rmd, ok := metadata.FromIncomingContext(ctx); ok { + // this header is used by envoyproxy to forward the ip addresses of the hops + if headerValue := rmd.Get("x-forwarded-for"); len(headerValue) != 0 { + clientIPs = headerValue + } + } + return &RequestContext{ ctx: ctx, - address: req.Attributes.Source.GetAddress().GetSocketAddress().GetAddress(), + ips: clientIPs, reqMethod: req.Attributes.Request.Http.Method, reqHeaders: canonicalizeHeaders(req.Attributes.Request.Http.Headers), reqURL: &url.URL{ @@ -120,7 +130,7 @@ func (s *RequestContext) AddHeaderForUpstream(name, value string) { s.upstreamHe func (s *RequestContext) AddCookieForUpstream(name, value string) { s.upstreamCookies[name] = value } func (s *RequestContext) Signer() heimdall.JWTSigner { return s.jwtSigner } func (s *RequestContext) RequestURL() *url.URL { return s.reqURL } -func (s *RequestContext) RequestClientIPs() []string { return []string{s.address} } +func (s *RequestContext) RequestClientIPs() []string { return s.ips } func (s *RequestContext) Finalize() (*envoy_auth.CheckResponse, error) { if s.err != nil { diff --git a/internal/handler/envoyextauth/grpcv3/request_context_test.go b/internal/handler/envoyextauth/grpcv3/request_context_test.go index 5714fc795..afccb16b2 100644 --- a/internal/handler/envoyextauth/grpcv3/request_context_test.go +++ b/internal/handler/envoyextauth/grpcv3/request_context_test.go @@ -11,6 +11,7 @@ import ( envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" "github.com/dadrus/heimdall/internal/heimdall" "github.com/dadrus/heimdall/internal/heimdall/mocks" @@ -37,21 +38,22 @@ func TestNewRequestContext(t *testing.T) { } checkReq := &envoy_auth.CheckRequest{ Attributes: &envoy_auth.AttributeContext{ - Source: &envoy_auth.AttributeContext_Peer{ - Address: &corev3.Address{ - Address: &corev3.Address_SocketAddress{ - SocketAddress: &corev3.SocketAddress{ - Address: "0.0.0.0", - }, - }, - }, - }, Request: &envoy_auth.AttributeContext_Request{ Http: httpReq, }, }, } - ctx := NewRequestContext(context.Background(), checkReq, &mocks.MockJWTSigner{}) + md := metadata.New(nil) + md.Set("x-forwarded-for", "127.0.0.1", "192.168.1.1") + + ctx := NewRequestContext( + metadata.NewIncomingContext( + context.Background(), + md, + ), + checkReq, + &mocks.MockJWTSigner{}, + ) // THEN require.Equal(t, httpReq.Method, ctx.RequestMethod()) @@ -71,8 +73,7 @@ func TestNewRequestContext(t *testing.T) { require.Empty(t, ctx.RequestCookie("baz")) require.NotNil(t, ctx.AppContext()) require.NotNil(t, ctx.Signer()) - require.Len(t, ctx.RequestClientIPs(), 1) - assert.Contains(t, ctx.RequestClientIPs(), "0.0.0.0") + assert.Equal(t, ctx.RequestClientIPs(), []string{"127.0.0.1", "192.168.1.1"}) } func TestFinalizeRequestContext(t *testing.T) { From 25da4e165d4fee390d7d738bae58a4e0d2712e06 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 14:57:55 +0100 Subject: [PATCH 51/67] linter warning resolved --- .../envoyextauth/grpcv3/middleware/accesslog/interceptor.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go index b4c23cc0b..ce3639780 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go @@ -151,14 +151,14 @@ func logMetaData(logCtx zerolog.Context, rmd metadata.MD, mdKey, logKey string) } func peerFromCtx(ctx context.Context) string { - p, ok := peer.FromContext(ctx) + peerInfo, ok := peer.FromContext(ctx) if !ok { return "" } - if tcpAddr, ok := p.Addr.(*net.TCPAddr); ok { + if tcpAddr, ok := peerInfo.Addr.(*net.TCPAddr); ok { return tcpAddr.IP.String() } - return p.Addr.String() + return peerInfo.Addr.String() } From 78adaa17b770e0b5eb130679d06a74acf3edbf88 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 15:52:58 +0100 Subject: [PATCH 52/67] using rpc status code instead of an HTTP one as required by the API --- internal/handler/envoyextauth/grpcv3/request_context.go | 4 ++-- .../handler/envoyextauth/grpcv3/request_context_test.go | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/request_context.go b/internal/handler/envoyextauth/grpcv3/request_context.go index c809839de..e88ab419c 100644 --- a/internal/handler/envoyextauth/grpcv3/request_context.go +++ b/internal/handler/envoyextauth/grpcv3/request_context.go @@ -25,7 +25,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/gogo/googleapis/google/rpc" "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/metadata" @@ -170,7 +170,7 @@ func (s *RequestContext) Finalize() (*envoy_auth.CheckResponse, error) { } return &envoy_auth.CheckResponse{ - Status: &status.Status{Code: int32(envoy_type.StatusCode_OK)}, + Status: &status.Status{Code: int32(rpc.OK)}, HttpResponse: &envoy_auth.CheckResponse_OkResponse{ OkResponse: &envoy_auth.OkHttpResponse{Headers: headers}, }, diff --git a/internal/handler/envoyextauth/grpcv3/request_context_test.go b/internal/handler/envoyextauth/grpcv3/request_context_test.go index afccb16b2..0d5d09721 100644 --- a/internal/handler/envoyextauth/grpcv3/request_context_test.go +++ b/internal/handler/envoyextauth/grpcv3/request_context_test.go @@ -9,6 +9,7 @@ import ( corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/gogo/googleapis/google/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" @@ -109,7 +110,7 @@ func TestFinalizeRequestContext(t *testing.T) { require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, int32(http.StatusOK), response.Status.Code) + assert.Equal(t, int32(rpc.OK), response.Status.Code) okResponse := response.GetOkResponse() require.NotNil(t, okResponse) @@ -138,7 +139,7 @@ func TestFinalizeRequestContext(t *testing.T) { require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, int32(http.StatusOK), response.Status.Code) + assert.Equal(t, int32(rpc.OK), response.Status.Code) okResponse := response.GetOkResponse() require.NotNil(t, okResponse) @@ -165,7 +166,7 @@ func TestFinalizeRequestContext(t *testing.T) { require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, int32(http.StatusOK), response.Status.Code) + assert.Equal(t, int32(rpc.OK), response.Status.Code) okResponse := response.GetOkResponse() require.NotNil(t, okResponse) From af8bb4c57adeec7a598c1090748e56d1a1c647bf Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 15:54:10 +0100 Subject: [PATCH 53/67] test updated to check for rpc status code instead of HTTP one --- internal/handler/envoyextauth/grpcv3/handler_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/handler_test.go b/internal/handler/envoyextauth/grpcv3/handler_test.go index 8e1e22b9c..218c7e00e 100644 --- a/internal/handler/envoyextauth/grpcv3/handler_test.go +++ b/internal/handler/envoyextauth/grpcv3/handler_test.go @@ -8,6 +8,7 @@ import ( envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/gogo/googleapis/google/rpc" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" @@ -165,7 +166,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() require.NoError(t, err) - assert.Equal(t, int32(http.StatusOK), response.Status.Code) + assert.Equal(t, int32(rpc.OK), response.Status.Code) okResponse := response.GetOkResponse() require.NotNil(t, okResponse) From c2c6e605b8cc3bb60ab75e1f98a38badf7a9d37f Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 15:59:20 +0100 Subject: [PATCH 54/67] using codes from google package --- internal/handler/envoyextauth/grpcv3/handler_test.go | 4 ++-- internal/handler/envoyextauth/grpcv3/request_context.go | 4 ++-- .../handler/envoyextauth/grpcv3/request_context_test.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/handler_test.go b/internal/handler/envoyextauth/grpcv3/handler_test.go index 218c7e00e..d3adaf653 100644 --- a/internal/handler/envoyextauth/grpcv3/handler_test.go +++ b/internal/handler/envoyextauth/grpcv3/handler_test.go @@ -8,13 +8,13 @@ import ( envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" - "github.com/gogo/googleapis/google/rpc" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" @@ -166,7 +166,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() require.NoError(t, err) - assert.Equal(t, int32(rpc.OK), response.Status.Code) + assert.Equal(t, int32(codes.OK), response.Status.Code) okResponse := response.GetOkResponse() require.NotNil(t, okResponse) diff --git a/internal/handler/envoyextauth/grpcv3/request_context.go b/internal/handler/envoyextauth/grpcv3/request_context.go index e88ab419c..623c2ab2d 100644 --- a/internal/handler/envoyextauth/grpcv3/request_context.go +++ b/internal/handler/envoyextauth/grpcv3/request_context.go @@ -25,8 +25,8 @@ 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" - "github.com/gogo/googleapis/google/rpc" "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "github.com/dadrus/heimdall/internal/heimdall" @@ -170,7 +170,7 @@ func (s *RequestContext) Finalize() (*envoy_auth.CheckResponse, error) { } return &envoy_auth.CheckResponse{ - Status: &status.Status{Code: int32(rpc.OK)}, + Status: &status.Status{Code: int32(codes.OK)}, HttpResponse: &envoy_auth.CheckResponse_OkResponse{ OkResponse: &envoy_auth.OkHttpResponse{Headers: headers}, }, diff --git a/internal/handler/envoyextauth/grpcv3/request_context_test.go b/internal/handler/envoyextauth/grpcv3/request_context_test.go index 0d5d09721..40992cbec 100644 --- a/internal/handler/envoyextauth/grpcv3/request_context_test.go +++ b/internal/handler/envoyextauth/grpcv3/request_context_test.go @@ -9,9 +9,9 @@ import ( corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/gogo/googleapis/google/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "github.com/dadrus/heimdall/internal/heimdall" @@ -110,7 +110,7 @@ func TestFinalizeRequestContext(t *testing.T) { require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, int32(rpc.OK), response.Status.Code) + assert.Equal(t, int32(codes.OK), response.Status.Code) okResponse := response.GetOkResponse() require.NotNil(t, okResponse) @@ -139,7 +139,7 @@ func TestFinalizeRequestContext(t *testing.T) { require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, int32(rpc.OK), response.Status.Code) + assert.Equal(t, int32(codes.OK), response.Status.Code) okResponse := response.GetOkResponse() require.NotNil(t, okResponse) @@ -166,7 +166,7 @@ func TestFinalizeRequestContext(t *testing.T) { require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, int32(rpc.OK), response.Status.Code) + assert.Equal(t, int32(codes.OK), response.Status.Code) okResponse := response.GetOkResponse() require.NotNil(t, okResponse) From a77ae26ff9cf0786274915b8c329c2b12fc710c7 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 16:51:37 +0100 Subject: [PATCH 55/67] proper use of grpc error codes --- .../envoyextauth/grpcv3/handler_test.go | 10 +- .../middleware/errorhandler/defaults.go | 16 ++-- .../middleware/errorhandler/error_response.go | 15 ++- .../errorhandler/error_response_test.go | 25 +++-- .../middleware/errorhandler/interceptor.go | 3 +- .../errorhandler/interceptor_test.go | 91 ++++++++++++------- .../grpcv3/middleware/errorhandler/options.go | 16 ++-- 7 files changed, 111 insertions(+), 65 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/handler_test.go b/internal/handler/envoyextauth/grpcv3/handler_test.go index d3adaf653..8372e7997 100644 --- a/internal/handler/envoyextauth/grpcv3/handler_test.go +++ b/internal/handler/envoyextauth/grpcv3/handler_test.go @@ -42,7 +42,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() require.NoError(t, err) - assert.Equal(t, int32(http.StatusNotFound), response.Status.Code) + assert.Equal(t, int32(codes.NotFound), response.Status.Code) deniedResponse := response.GetDeniedResponse() require.NotNil(t, deniedResponse) @@ -64,7 +64,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() require.NoError(t, err) - assert.Equal(t, int32(http.StatusMethodNotAllowed), response.Status.Code) + assert.Equal(t, int32(codes.InvalidArgument), response.Status.Code) deniedResponse := response.GetDeniedResponse() require.NotNil(t, deniedResponse) @@ -87,7 +87,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() require.NoError(t, err) - assert.Equal(t, int32(http.StatusUnauthorized), response.Status.Code) + assert.Equal(t, int32(codes.Unauthenticated), response.Status.Code) deniedResponse := response.GetDeniedResponse() require.NotNil(t, deniedResponse) @@ -114,7 +114,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() require.NoError(t, err) - assert.Equal(t, int32(http.StatusForbidden), response.Status.Code) + assert.Equal(t, int32(codes.PermissionDenied), response.Status.Code) deniedResponse := response.GetDeniedResponse() require.NotNil(t, deniedResponse) @@ -141,7 +141,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { t.Helper() require.NoError(t, err) - assert.Equal(t, int32(http.StatusFound), response.Status.Code) + assert.Equal(t, int32(codes.FailedPrecondition), response.Status.Code) deniedResponse := response.GetDeniedResponse() require.NotNil(t, deniedResponse) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go index f45143d7f..39f6f56ba 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/defaults.go @@ -18,14 +18,16 @@ package errorhandler import ( "net/http" + + "google.golang.org/grpc/codes" ) var defaultOptions = opts{ //nolint:gochecknoglobals - authenticationError: responseWith(http.StatusUnauthorized), - authorizationError: responseWith(http.StatusForbidden), - communicationError: responseWith(http.StatusBadGateway), - preconditionError: responseWith(http.StatusBadRequest), - badMethodError: responseWith(http.StatusMethodNotAllowed), - noRuleError: responseWith(http.StatusNotFound), - internalError: responseWith(http.StatusInternalServerError), + authenticationError: responseWith(codes.Unauthenticated, http.StatusUnauthorized), + authorizationError: responseWith(codes.PermissionDenied, http.StatusForbidden), + communicationError: responseWith(codes.DeadlineExceeded, http.StatusBadGateway), + preconditionError: responseWith(codes.InvalidArgument, http.StatusBadRequest), + badMethodError: responseWith(codes.InvalidArgument, http.StatusMethodNotAllowed), + noRuleError: responseWith(codes.NotFound, http.StatusNotFound), + internalError: responseWith(codes.Internal, http.StatusInternalServerError), } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go index d7b4ce2ec..2a5b0596a 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response.go @@ -26,17 +26,22 @@ import ( envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" "github.com/goccy/go-json" "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc/codes" ) -func responseWith(code int) func(err error, verbose bool, mimeType string) (any, error) { +func responseWith( + grpcCode codes.Code, httpCodeOverride 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 + return errorResponse(grpcCode, httpCodeOverride, err, verbose, mimeType), nil } } -func errorResponse(code int, decErr error, verbose bool, mimeType string) *envoy_auth.CheckResponse { +func errorResponse( + grpcCode codes.Code, httpCodeOverride int, decErr error, verbose bool, mimeType string, +) *envoy_auth.CheckResponse { deniedResponse := &envoy_auth.DeniedHttpResponse{ - Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode(code)}, + Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode(httpCodeOverride)}, } if verbose { @@ -62,7 +67,7 @@ func errorResponse(code int, decErr error, verbose bool, mimeType string) *envoy } return &envoy_auth.CheckResponse{ - Status: &status.Status{Code: int32(code)}, + Status: &status.Status{Code: int32(grpcCode)}, HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{DeniedResponse: deniedResponse}, } } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go index b749e36e3..31f9e433b 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/error_response_test.go @@ -23,6 +23,7 @@ import ( envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" "github.com/dadrus/heimdall/internal/heimdall" "github.com/dadrus/heimdall/internal/x/errorchain" @@ -33,7 +34,8 @@ func TestErrorResponse(t *testing.T) { for _, tc := range []struct { uc string - code int + grpcCode codes.Code + httpCode int err error offeredType string expectedType string @@ -41,7 +43,8 @@ func TestErrorResponse(t *testing.T) { }{ { uc: "select text/plain from multiple offered", - code: http.StatusForbidden, + grpcCode: codes.NotFound, + httpCode: http.StatusForbidden, err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), offeredType: "application/json;q=0.3,text/html;q=0.5,text/plain", expectedType: "text/plain", @@ -49,7 +52,8 @@ func TestErrorResponse(t *testing.T) { }, { uc: "select text/html doe to unknown offered type", - code: http.StatusForbidden, + grpcCode: codes.Internal, + httpCode: http.StatusForbidden, err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), offeredType: "foo/bar;q=0.5,bar/foo;q=0.6", expectedType: "text/html", @@ -57,7 +61,8 @@ func TestErrorResponse(t *testing.T) { }, { uc: "select text/html from multiple offered", - code: http.StatusForbidden, + grpcCode: codes.PermissionDenied, + httpCode: http.StatusForbidden, err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), offeredType: "application/json;q=0.3,text/html;q=0.5,text/html;q=0.8,*/*;q=0.2", expectedType: "text/html", @@ -65,7 +70,8 @@ func TestErrorResponse(t *testing.T) { }, { uc: "select appliction/xml from multiple offered", - code: http.StatusForbidden, + grpcCode: codes.PermissionDenied, + httpCode: http.StatusForbidden, err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), offeredType: "application/json;q=0.3,text/html;q=0.5,text/plain;q=0.2,application/xml;q=0.8", expectedType: "application/xml", @@ -73,7 +79,8 @@ func TestErrorResponse(t *testing.T) { }, { uc: "select appliction/json from multiple offered", - code: http.StatusForbidden, + grpcCode: codes.PermissionDenied, + httpCode: http.StatusForbidden, err: errorchain.NewWithMessage(heimdall.ErrAuthorization, "test"), offeredType: "application/xml;q=0.3,text/html;q=0.5,text/plain;q=0.2,application/json;q=0.8", expectedType: "application/json", @@ -82,15 +89,15 @@ func TestErrorResponse(t *testing.T) { } { t.Run(tc.uc, func(t *testing.T) { // WHEN - resp := errorResponse(tc.code, tc.err, true, tc.offeredType) + resp := errorResponse(tc.grpcCode, tc.httpCode, tc.err, true, tc.offeredType) // THEN require.NotNil(t, resp) - assert.Equal(t, int32(tc.code), resp.Status.Code) + assert.Equal(t, int32(tc.grpcCode), resp.Status.Code) deniedResp := resp.GetDeniedResponse() - assert.Equal(t, envoy_type.StatusCode(tc.code), deniedResp.Status.Code) + assert.Equal(t, envoy_type.StatusCode(tc.httpCode), deniedResp.Status.Code) assert.Equal(t, "Content-Type", deniedResp.Headers[0].Header.Key) assert.Equal(t, tc.expectedType, deniedResp.Headers[0].Header.Value) assert.Equal(t, tc.expBody, deniedResp.Body) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go index 76b71d6ed..be2a13cf7 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor.go @@ -26,6 +26,7 @@ import ( "github.com/rs/zerolog" "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "github.com/dadrus/heimdall/internal/accesscontext" "github.com/dadrus/heimdall/internal/heimdall" @@ -76,7 +77,7 @@ func (h *interceptor) intercept( errors.As(err, &redirectError) return &envoy_auth.CheckResponse{ - Status: &status.Status{Code: int32(redirectError.Code)}, + Status: &status.Status{Code: int32(codes.FailedPrecondition)}, HttpResponse: &envoy_auth.CheckResponse_DeniedResponse{ DeniedResponse: &envoy_auth.DeniedHttpResponse{ Status: &envoy_type.HttpStatus{Code: envoy_type.StatusCode(redirectError.Code)}, diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go index a2dcd2bdf..8b9f17a8d 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go @@ -29,6 +29,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" @@ -43,176 +44,204 @@ func TestErrorInterceptor(t *testing.T) { uc string interceptor grpc.UnaryServerInterceptor err error - expCode envoy_type.StatusCode + expGrpcCode codes.Code + expHttpCode envoy_type.StatusCode expBody string }{ { uc: "no error", interceptor: New(), - expCode: http.StatusOK, + expGrpcCode: codes.OK, + expHttpCode: http.StatusOK, }, { uc: "authentication error default", interceptor: New(), err: heimdall.ErrAuthentication, - expCode: http.StatusUnauthorized, + expGrpcCode: codes.Unauthenticated, + expHttpCode: http.StatusUnauthorized, }, { uc: "authentication error overridden", interceptor: New(WithAuthenticationErrorCode(http.StatusContinue)), err: heimdall.ErrAuthentication, - expCode: http.StatusContinue, + expGrpcCode: codes.Unauthenticated, + expHttpCode: http.StatusContinue, }, { uc: "authentication error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrAuthentication, - expCode: http.StatusUnauthorized, + expGrpcCode: codes.Unauthenticated, + expHttpCode: http.StatusUnauthorized, expBody: "

authentication error

", }, { uc: "authorization error default", interceptor: New(), err: heimdall.ErrAuthorization, - expCode: http.StatusForbidden, + expGrpcCode: codes.PermissionDenied, + expHttpCode: http.StatusForbidden, }, { uc: "authorization error overridden", interceptor: New(WithAuthorizationErrorCode(http.StatusContinue)), err: heimdall.ErrAuthorization, - expCode: http.StatusContinue, + expGrpcCode: codes.PermissionDenied, + expHttpCode: http.StatusContinue, }, { uc: "authorization error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrAuthorization, - expCode: http.StatusForbidden, + expGrpcCode: codes.PermissionDenied, + expHttpCode: http.StatusForbidden, expBody: "

authorization error

", }, { uc: "communication timeout error default", interceptor: New(), err: heimdall.ErrCommunicationTimeout, - expCode: http.StatusBadGateway, + expGrpcCode: codes.DeadlineExceeded, + expHttpCode: http.StatusBadGateway, }, { uc: "communication timeout error overridden", interceptor: New(WithCommunicationErrorCode(http.StatusContinue)), err: heimdall.ErrCommunicationTimeout, - expCode: http.StatusContinue, + expGrpcCode: codes.DeadlineExceeded, + expHttpCode: http.StatusContinue, }, { uc: "communication timeout error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrCommunicationTimeout, - expCode: http.StatusBadGateway, + expGrpcCode: codes.DeadlineExceeded, + expHttpCode: http.StatusBadGateway, expBody: "

communication timeout error

", }, { uc: "communication error default", interceptor: New(), err: heimdall.ErrCommunication, - expCode: http.StatusBadGateway, + expGrpcCode: codes.DeadlineExceeded, + expHttpCode: http.StatusBadGateway, }, { uc: "communication error overridden", interceptor: New(WithCommunicationErrorCode(http.StatusContinue)), err: heimdall.ErrCommunication, - expCode: http.StatusContinue, + expGrpcCode: codes.DeadlineExceeded, + expHttpCode: http.StatusContinue, }, { uc: "communication error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrCommunication, - expCode: http.StatusBadGateway, + expGrpcCode: codes.DeadlineExceeded, + expHttpCode: http.StatusBadGateway, expBody: "

communication error

", }, { uc: "precondition error default", interceptor: New(), err: heimdall.ErrArgument, - expCode: http.StatusBadRequest, + expGrpcCode: codes.InvalidArgument, + expHttpCode: http.StatusBadRequest, }, { uc: "precondition error overridden", interceptor: New(WithPreconditionErrorCode(http.StatusContinue)), err: heimdall.ErrArgument, - expCode: http.StatusContinue, + expGrpcCode: codes.InvalidArgument, + expHttpCode: http.StatusContinue, }, { uc: "precondition error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrArgument, - expCode: http.StatusBadRequest, + expGrpcCode: codes.InvalidArgument, + expHttpCode: http.StatusBadRequest, expBody: "

argument error

", }, { uc: "method error default", interceptor: New(), err: heimdall.ErrMethodNotAllowed, - expCode: http.StatusMethodNotAllowed, + expGrpcCode: codes.InvalidArgument, + expHttpCode: http.StatusMethodNotAllowed, }, { uc: "method error overridden", interceptor: New(WithMethodErrorCode(http.StatusContinue)), err: heimdall.ErrMethodNotAllowed, - expCode: http.StatusContinue, + expGrpcCode: codes.InvalidArgument, + expHttpCode: http.StatusContinue, }, { uc: "method error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrMethodNotAllowed, - expCode: http.StatusMethodNotAllowed, + expGrpcCode: codes.InvalidArgument, + expHttpCode: http.StatusMethodNotAllowed, expBody: "

method not allowed

", }, { uc: "no rule error default", interceptor: New(), err: heimdall.ErrNoRuleFound, - expCode: http.StatusNotFound, + expGrpcCode: codes.NotFound, + expHttpCode: http.StatusNotFound, }, { uc: "no rule error overridden", interceptor: New(WithNoRuleErrorCode(http.StatusContinue)), err: heimdall.ErrNoRuleFound, - expCode: http.StatusContinue, + expGrpcCode: codes.NotFound, + expHttpCode: http.StatusContinue, }, { uc: "no rule error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrNoRuleFound, - expCode: http.StatusNotFound, + expGrpcCode: codes.NotFound, + expHttpCode: http.StatusNotFound, expBody: "

no rule found

", }, { uc: "redirect error", interceptor: New(), err: &heimdall.RedirectError{RedirectTo: "http://foo.local", Code: http.StatusFound}, - expCode: http.StatusFound, + expGrpcCode: codes.FailedPrecondition, + expHttpCode: http.StatusFound, }, { uc: "redirect error verbose", interceptor: New(WithVerboseErrors(true)), err: &heimdall.RedirectError{RedirectTo: "http://foo.local", Code: http.StatusFound}, - expCode: http.StatusFound, + expGrpcCode: codes.FailedPrecondition, + expHttpCode: http.StatusFound, }, { uc: "internal error default", interceptor: New(), err: heimdall.ErrInternal, - expCode: http.StatusInternalServerError, + expGrpcCode: codes.Internal, + expHttpCode: http.StatusInternalServerError, }, { uc: "internal error overridden", interceptor: New(WithInternalServerErrorCode(http.StatusContinue)), err: heimdall.ErrInternal, - expCode: http.StatusContinue, + expGrpcCode: codes.Internal, + expHttpCode: http.StatusContinue, }, { uc: "internal error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrInternal, - expCode: http.StatusInternalServerError, + expGrpcCode: codes.Internal, + expHttpCode: http.StatusInternalServerError, expBody: "

internal error

", }, } { @@ -232,7 +261,7 @@ func TestErrorInterceptor(t *testing.T) { handler.On("Check", mock.Anything, mock.Anything).Return(nil, tc.err) } else { handler.On("Check", mock.Anything, mock.Anything).Return(&envoy_auth.CheckResponse{ - Status: &status.Status{Code: int32(envoy_type.StatusCode_OK)}, + Status: &status.Status{Code: int32(tc.expGrpcCode)}, HttpResponse: &envoy_auth.CheckResponse_OkResponse{ OkResponse: &envoy_auth.OkHttpResponse{}, }, @@ -267,12 +296,12 @@ func TestErrorInterceptor(t *testing.T) { srv.Stop() require.NoError(t, err) - assert.Equal(t, int32(tc.expCode), resp.Status.Code) + assert.Equal(t, int32(tc.expGrpcCode), resp.Status.Code) if tc.err != nil { deniedResp := resp.GetDeniedResponse() require.NotNil(t, deniedResp) - assert.Equal(t, tc.expCode, deniedResp.Status.Code) + assert.Equal(t, tc.expHttpCode, deniedResp.Status.Code) assert.Equal(t, tc.expBody, deniedResp.Body) } }) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go index 400214125..4567a45f1 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/options.go @@ -16,6 +16,8 @@ package errorhandler +import "google.golang.org/grpc/codes" + type opts struct { verboseErrors bool authenticationError func(err error, verbose bool, mimeType string) (any, error) @@ -32,7 +34,7 @@ type Option func(*opts) func WithPreconditionErrorCode(code int) Option { return func(o *opts) { if code > 0 { - o.preconditionError = responseWith(code) + o.preconditionError = responseWith(codes.InvalidArgument, code) } } } @@ -40,7 +42,7 @@ func WithPreconditionErrorCode(code int) Option { func WithAuthenticationErrorCode(code int) Option { return func(o *opts) { if code > 0 { - o.authenticationError = responseWith(code) + o.authenticationError = responseWith(codes.Unauthenticated, code) } } } @@ -48,7 +50,7 @@ func WithAuthenticationErrorCode(code int) Option { func WithAuthorizationErrorCode(code int) Option { return func(o *opts) { if code > 0 { - o.authorizationError = responseWith(code) + o.authorizationError = responseWith(codes.PermissionDenied, code) } } } @@ -56,7 +58,7 @@ func WithAuthorizationErrorCode(code int) Option { func WithCommunicationErrorCode(code int) Option { return func(o *opts) { if code > 0 { - o.communicationError = responseWith(code) + o.communicationError = responseWith(codes.DeadlineExceeded, code) } } } @@ -64,7 +66,7 @@ func WithCommunicationErrorCode(code int) Option { func WithInternalServerErrorCode(code int) Option { return func(o *opts) { if code > 0 { - o.internalError = responseWith(code) + o.internalError = responseWith(codes.Internal, code) } } } @@ -72,7 +74,7 @@ func WithInternalServerErrorCode(code int) Option { func WithMethodErrorCode(code int) Option { return func(o *opts) { if code > 0 { - o.badMethodError = responseWith(code) + o.badMethodError = responseWith(codes.InvalidArgument, code) } } } @@ -80,7 +82,7 @@ func WithMethodErrorCode(code int) Option { func WithNoRuleErrorCode(code int) Option { return func(o *opts) { if code > 0 { - o.noRuleError = responseWith(code) + o.noRuleError = responseWith(codes.NotFound, code) } } } From 2266317e43ffb0cdb720317e9da79b23bcda4c8e Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 16:53:07 +0100 Subject: [PATCH 56/67] linter warnings resolved --- .../errorhandler/interceptor_test.go | 120 +++++++++--------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go index 8b9f17a8d..f32a6055d 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/errorhandler/interceptor_test.go @@ -37,211 +37,211 @@ import ( "github.com/dadrus/heimdall/internal/heimdall" ) -func TestErrorInterceptor(t *testing.T) { +func TestErrorInterceptor(t *testing.T) { //nolint:maintidx t.Parallel() for _, tc := range []struct { uc string interceptor grpc.UnaryServerInterceptor err error - expGrpcCode codes.Code - expHttpCode envoy_type.StatusCode + expGRPCCode codes.Code + expHTTPCode envoy_type.StatusCode expBody string }{ { uc: "no error", interceptor: New(), - expGrpcCode: codes.OK, - expHttpCode: http.StatusOK, + expGRPCCode: codes.OK, + expHTTPCode: http.StatusOK, }, { uc: "authentication error default", interceptor: New(), err: heimdall.ErrAuthentication, - expGrpcCode: codes.Unauthenticated, - expHttpCode: http.StatusUnauthorized, + expGRPCCode: codes.Unauthenticated, + expHTTPCode: http.StatusUnauthorized, }, { uc: "authentication error overridden", interceptor: New(WithAuthenticationErrorCode(http.StatusContinue)), err: heimdall.ErrAuthentication, - expGrpcCode: codes.Unauthenticated, - expHttpCode: http.StatusContinue, + expGRPCCode: codes.Unauthenticated, + expHTTPCode: http.StatusContinue, }, { uc: "authentication error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrAuthentication, - expGrpcCode: codes.Unauthenticated, - expHttpCode: http.StatusUnauthorized, + expGRPCCode: codes.Unauthenticated, + expHTTPCode: http.StatusUnauthorized, expBody: "

authentication error

", }, { uc: "authorization error default", interceptor: New(), err: heimdall.ErrAuthorization, - expGrpcCode: codes.PermissionDenied, - expHttpCode: http.StatusForbidden, + expGRPCCode: codes.PermissionDenied, + expHTTPCode: http.StatusForbidden, }, { uc: "authorization error overridden", interceptor: New(WithAuthorizationErrorCode(http.StatusContinue)), err: heimdall.ErrAuthorization, - expGrpcCode: codes.PermissionDenied, - expHttpCode: http.StatusContinue, + expGRPCCode: codes.PermissionDenied, + expHTTPCode: http.StatusContinue, }, { uc: "authorization error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrAuthorization, - expGrpcCode: codes.PermissionDenied, - expHttpCode: http.StatusForbidden, + expGRPCCode: codes.PermissionDenied, + expHTTPCode: http.StatusForbidden, expBody: "

authorization error

", }, { uc: "communication timeout error default", interceptor: New(), err: heimdall.ErrCommunicationTimeout, - expGrpcCode: codes.DeadlineExceeded, - expHttpCode: http.StatusBadGateway, + expGRPCCode: codes.DeadlineExceeded, + expHTTPCode: http.StatusBadGateway, }, { uc: "communication timeout error overridden", interceptor: New(WithCommunicationErrorCode(http.StatusContinue)), err: heimdall.ErrCommunicationTimeout, - expGrpcCode: codes.DeadlineExceeded, - expHttpCode: http.StatusContinue, + expGRPCCode: codes.DeadlineExceeded, + expHTTPCode: http.StatusContinue, }, { uc: "communication timeout error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrCommunicationTimeout, - expGrpcCode: codes.DeadlineExceeded, - expHttpCode: http.StatusBadGateway, + expGRPCCode: codes.DeadlineExceeded, + expHTTPCode: http.StatusBadGateway, expBody: "

communication timeout error

", }, { uc: "communication error default", interceptor: New(), err: heimdall.ErrCommunication, - expGrpcCode: codes.DeadlineExceeded, - expHttpCode: http.StatusBadGateway, + expGRPCCode: codes.DeadlineExceeded, + expHTTPCode: http.StatusBadGateway, }, { uc: "communication error overridden", interceptor: New(WithCommunicationErrorCode(http.StatusContinue)), err: heimdall.ErrCommunication, - expGrpcCode: codes.DeadlineExceeded, - expHttpCode: http.StatusContinue, + expGRPCCode: codes.DeadlineExceeded, + expHTTPCode: http.StatusContinue, }, { uc: "communication error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrCommunication, - expGrpcCode: codes.DeadlineExceeded, - expHttpCode: http.StatusBadGateway, + expGRPCCode: codes.DeadlineExceeded, + expHTTPCode: http.StatusBadGateway, expBody: "

communication error

", }, { uc: "precondition error default", interceptor: New(), err: heimdall.ErrArgument, - expGrpcCode: codes.InvalidArgument, - expHttpCode: http.StatusBadRequest, + expGRPCCode: codes.InvalidArgument, + expHTTPCode: http.StatusBadRequest, }, { uc: "precondition error overridden", interceptor: New(WithPreconditionErrorCode(http.StatusContinue)), err: heimdall.ErrArgument, - expGrpcCode: codes.InvalidArgument, - expHttpCode: http.StatusContinue, + expGRPCCode: codes.InvalidArgument, + expHTTPCode: http.StatusContinue, }, { uc: "precondition error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrArgument, - expGrpcCode: codes.InvalidArgument, - expHttpCode: http.StatusBadRequest, + expGRPCCode: codes.InvalidArgument, + expHTTPCode: http.StatusBadRequest, expBody: "

argument error

", }, { uc: "method error default", interceptor: New(), err: heimdall.ErrMethodNotAllowed, - expGrpcCode: codes.InvalidArgument, - expHttpCode: http.StatusMethodNotAllowed, + expGRPCCode: codes.InvalidArgument, + expHTTPCode: http.StatusMethodNotAllowed, }, { uc: "method error overridden", interceptor: New(WithMethodErrorCode(http.StatusContinue)), err: heimdall.ErrMethodNotAllowed, - expGrpcCode: codes.InvalidArgument, - expHttpCode: http.StatusContinue, + expGRPCCode: codes.InvalidArgument, + expHTTPCode: http.StatusContinue, }, { uc: "method error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrMethodNotAllowed, - expGrpcCode: codes.InvalidArgument, - expHttpCode: http.StatusMethodNotAllowed, + expGRPCCode: codes.InvalidArgument, + expHTTPCode: http.StatusMethodNotAllowed, expBody: "

method not allowed

", }, { uc: "no rule error default", interceptor: New(), err: heimdall.ErrNoRuleFound, - expGrpcCode: codes.NotFound, - expHttpCode: http.StatusNotFound, + expGRPCCode: codes.NotFound, + expHTTPCode: http.StatusNotFound, }, { uc: "no rule error overridden", interceptor: New(WithNoRuleErrorCode(http.StatusContinue)), err: heimdall.ErrNoRuleFound, - expGrpcCode: codes.NotFound, - expHttpCode: http.StatusContinue, + expGRPCCode: codes.NotFound, + expHTTPCode: http.StatusContinue, }, { uc: "no rule error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrNoRuleFound, - expGrpcCode: codes.NotFound, - expHttpCode: http.StatusNotFound, + expGRPCCode: codes.NotFound, + expHTTPCode: http.StatusNotFound, expBody: "

no rule found

", }, { uc: "redirect error", interceptor: New(), err: &heimdall.RedirectError{RedirectTo: "http://foo.local", Code: http.StatusFound}, - expGrpcCode: codes.FailedPrecondition, - expHttpCode: http.StatusFound, + expGRPCCode: codes.FailedPrecondition, + expHTTPCode: http.StatusFound, }, { uc: "redirect error verbose", interceptor: New(WithVerboseErrors(true)), err: &heimdall.RedirectError{RedirectTo: "http://foo.local", Code: http.StatusFound}, - expGrpcCode: codes.FailedPrecondition, - expHttpCode: http.StatusFound, + expGRPCCode: codes.FailedPrecondition, + expHTTPCode: http.StatusFound, }, { uc: "internal error default", interceptor: New(), err: heimdall.ErrInternal, - expGrpcCode: codes.Internal, - expHttpCode: http.StatusInternalServerError, + expGRPCCode: codes.Internal, + expHTTPCode: http.StatusInternalServerError, }, { uc: "internal error overridden", interceptor: New(WithInternalServerErrorCode(http.StatusContinue)), err: heimdall.ErrInternal, - expGrpcCode: codes.Internal, - expHttpCode: http.StatusContinue, + expGRPCCode: codes.Internal, + expHTTPCode: http.StatusContinue, }, { uc: "internal error verbose", interceptor: New(WithVerboseErrors(true)), err: heimdall.ErrInternal, - expGrpcCode: codes.Internal, - expHttpCode: http.StatusInternalServerError, + expGRPCCode: codes.Internal, + expHTTPCode: http.StatusInternalServerError, expBody: "

internal error

", }, } { @@ -261,7 +261,7 @@ func TestErrorInterceptor(t *testing.T) { handler.On("Check", mock.Anything, mock.Anything).Return(nil, tc.err) } else { handler.On("Check", mock.Anything, mock.Anything).Return(&envoy_auth.CheckResponse{ - Status: &status.Status{Code: int32(tc.expGrpcCode)}, + Status: &status.Status{Code: int32(tc.expGRPCCode)}, HttpResponse: &envoy_auth.CheckResponse_OkResponse{ OkResponse: &envoy_auth.OkHttpResponse{}, }, @@ -296,12 +296,12 @@ func TestErrorInterceptor(t *testing.T) { srv.Stop() require.NoError(t, err) - assert.Equal(t, int32(tc.expGrpcCode), resp.Status.Code) + assert.Equal(t, int32(tc.expGRPCCode), resp.Status.Code) if tc.err != nil { deniedResp := resp.GetDeniedResponse() require.NotNil(t, deniedResp) - assert.Equal(t, tc.expHttpCode, deniedResp.Status.Code) + assert.Equal(t, tc.expHTTPCode, deniedResp.Status.Code) assert.Equal(t, tc.expBody, deniedResp.Body) } }) From 25af22d75c6649e9c12239c75c379f8de6b2a905 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 17:18:33 +0100 Subject: [PATCH 57/67] docker-compose quickstarts updateds to cover integration with envoy using both HTTP and GRPC options --- examples/docker-compose/quickstarts/README.md | 14 +++- .../docker-compose-envoy-grpc.yaml | 13 ++++ ...oy.yaml => docker-compose-envoy-http.yaml} | 4 +- .../quickstarts/docker-compose.yaml | 2 +- .../quickstarts/envoy-config-grpc.yaml | 64 +++++++++++++++++++ ...voy-config.yaml => envoy-config-http.yaml} | 0 .../quickstarts/heimdall-config.yaml | 18 +++++- 7 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 examples/docker-compose/quickstarts/docker-compose-envoy-grpc.yaml rename examples/docker-compose/quickstarts/{docker-compose-envoy.yaml => docker-compose-envoy-http.yaml} (58%) create mode 100644 examples/docker-compose/quickstarts/envoy-config-grpc.yaml rename examples/docker-compose/quickstarts/{envoy-config.yaml => envoy-config-http.yaml} (100%) diff --git a/examples/docker-compose/quickstarts/README.md b/examples/docker-compose/quickstarts/README.md index c9378b9b3..1cff5784f 100644 --- a/examples/docker-compose/quickstarts/README.md +++ b/examples/docker-compose/quickstarts/README.md @@ -18,6 +18,7 @@ In that setup heimdall is not integrated with any other reverse proxy. curl -v http://127.0.0.1:9090/anonymous curl -v http://127.0.0.1:9090/public curl -v http://127.0.0.1:9090/foo + curl -v -H "Accept: Bar" http://127.0.0.1:9090/foo ``` Check the responses @@ -38,6 +39,7 @@ In that setup heimdall is integrated with Traefik. All requests are sent to trae curl -v http://127.0.0.1:9090/anonymous curl -v http://127.0.0.1:9090/public curl -v http://127.0.0.1:9090/foo + curl -v -H "Accept: Bar" http://127.0.0.1:9090/foo ``` Check the responses @@ -47,10 +49,19 @@ In that setup heimdall is integrated with Traefik. All requests are sent to trae In that setup heimdall is integrated with Envoy Proxy. All requests are sent to envoy, which then contacts heimdall as external authorization middleware and depending on the response from heimdall either forwards the request to the upstream service, or directly responses with an error from heimdall. 1. Start the environment with + ether ```bash - docker-compose -f docker-compose.yaml -f docker-compose-envoy.yaml up + docker-compose -f docker-compose.yaml -f docker-compose-envoy-http.yaml up ``` + + to see integration using the HTTP decision service in action, or + + ```bash + docker-compose -f docker-compose.yaml -f docker-compose-envoy-grpc.yaml up + ``` + + to see integration using the envoy GRPC extauthz decision service in action. 2. Play with it @@ -58,6 +69,7 @@ In that setup heimdall is integrated with Envoy Proxy. All requests are sent to curl -v http://127.0.0.1:9090/anonymous curl -v http://127.0.0.1:9090/public curl -v http://127.0.0.1:9090/foo + curl -v -H "Accept: Bar" http://127.0.0.1:9090/foo ``` Check the responses \ No newline at end of file diff --git a/examples/docker-compose/quickstarts/docker-compose-envoy-grpc.yaml b/examples/docker-compose/quickstarts/docker-compose-envoy-grpc.yaml new file mode 100644 index 000000000..feeb958b3 --- /dev/null +++ b/examples/docker-compose/quickstarts/docker-compose-envoy-grpc.yaml @@ -0,0 +1,13 @@ +version: '3.7' + +services: + heimdall: + command: -c /heimdall/conf/heimdall.yaml serve decision --envoy-extauth + + edge-router: + image: envoyproxy/envoy:v1.25.1 + volumes: + - ./envoy-config-grpc.yaml:/envoy.yaml:ro + ports: + - 9090:9090 + command: -c /envoy.yaml -l debug \ No newline at end of file diff --git a/examples/docker-compose/quickstarts/docker-compose-envoy.yaml b/examples/docker-compose/quickstarts/docker-compose-envoy-http.yaml similarity index 58% rename from examples/docker-compose/quickstarts/docker-compose-envoy.yaml rename to examples/docker-compose/quickstarts/docker-compose-envoy-http.yaml index 53dcb3fea..b0b141312 100644 --- a/examples/docker-compose/quickstarts/docker-compose-envoy.yaml +++ b/examples/docker-compose/quickstarts/docker-compose-envoy-http.yaml @@ -4,7 +4,7 @@ services: edge-router: image: envoyproxy/envoy:v1.25.1 volumes: - - ./envoy-config.yaml:/envoy.yaml:ro + - ./envoy-config-http.yaml:/envoy.yaml:ro ports: - 9090:9090 - command: -c /envoy.yaml \ No newline at end of file + command: -c /envoy.yaml -l debug \ No newline at end of file diff --git a/examples/docker-compose/quickstarts/docker-compose.yaml b/examples/docker-compose/quickstarts/docker-compose.yaml index 175dd7c37..d223e7645 100644 --- a/examples/docker-compose/quickstarts/docker-compose.yaml +++ b/examples/docker-compose/quickstarts/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3.7' services: heimdall: - image: dadrus/heimdall:0.6.1-alpha + image: heimdall:local volumes: - ./heimdall-config.yaml:/heimdall/conf/heimdall.yaml:ro - ./upstream-rules.yaml:/heimdall/conf/rules.yaml:ro diff --git a/examples/docker-compose/quickstarts/envoy-config-grpc.yaml b/examples/docker-compose/quickstarts/envoy-config-grpc.yaml new file mode 100644 index 000000000..c66bfdcaa --- /dev/null +++ b/examples/docker-compose/quickstarts/envoy-config-grpc.yaml @@ -0,0 +1,64 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + address: 0.0.0.0 + port_value: 9090 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: edge + http_filters: + - name: envoy.filters.http.ext_authz + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz + transport_api_version: V3 + grpc_service: + envoy_grpc: + cluster_name: ext-authz + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + virtual_hosts: + - name: direct_response_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: services + + clusters: + - name: ext-authz + type: strict_dns + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: { } + load_assignment: + cluster_name: ext-authz + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: heimdall + port_value: 4456 + - name: services + connect_timeout: 5s + type: strict_dns + dns_lookup_family: V4_ONLY + load_assignment: + cluster_name: services + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: upstream + port_value: 80 \ No newline at end of file diff --git a/examples/docker-compose/quickstarts/envoy-config.yaml b/examples/docker-compose/quickstarts/envoy-config-http.yaml similarity index 100% rename from examples/docker-compose/quickstarts/envoy-config.yaml rename to examples/docker-compose/quickstarts/envoy-config-http.yaml diff --git a/examples/docker-compose/quickstarts/heimdall-config.yaml b/examples/docker-compose/quickstarts/heimdall-config.yaml index 9654b7c54..7fc44035b 100644 --- a/examples/docker-compose/quickstarts/heimdall-config.yaml +++ b/examples/docker-compose/quickstarts/heimdall-config.yaml @@ -1,5 +1,5 @@ log: - level: info + level: debug serve: decision: @@ -20,6 +20,20 @@ rules: type: noop - id: create_jwt type: jwt + error_handlers: + - id: default + type: default + - id: redirect_to_login + type: redirect + config: + to: http://127.0.0.1:8080/login?origin={{ .Request.URL | urlenc }} + when: + - error: + - type: authentication_error + raised_by: reject_requests + request_headers: + Accept: + - Bar default: methods: @@ -28,6 +42,8 @@ rules: execute: - authenticator: reject_requests - unifier: create_jwt + on_error: + - error_handler: redirect_to_login providers: file_system: From 8b29e0f9fb4b4ceb3c138a4e0b5b937cc4819766 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 23:38:26 +0100 Subject: [PATCH 58/67] logging grpc status code --- .../grpcv3/middleware/accesslog/interceptor.go | 11 +++++++++-- .../grpcv3/middleware/accesslog/interceptor_test.go | 12 ++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go index ce3639780..28e5d79ed 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor.go @@ -26,6 +26,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/metadata" "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" "github.com/dadrus/heimdall/internal/accesscontext" "github.com/dadrus/heimdall/internal/x/opentelemetry/tracecontext" @@ -49,11 +50,11 @@ func (i *accessLogInterceptor) Unary() grpc.UnaryServerInterceptor { start, accLog := i.startTransaction(ctx, info.FullMethod) ctx = accesscontext.New(ctx) - res, err := handler(ctx, req) + resp, err := handler(ctx, req) i.finalizeTransaction(ctx, accLog, start, err) - return res, err + return resp, err } } @@ -95,7 +96,13 @@ func (i *accessLogInterceptor) startTransaction(ctx context.Context, fullMethod func (i *accessLogInterceptor) finalizeTransaction( ctx context.Context, accLog zerolog.Logger, start time.Time, err error, ) { + // grpc errors are only used to signal unusual conditions + // in all other cases the error is anyway embedded into the envoy CheckResponse object + // so that on the grpc level no error is returned + grpcStatus, _ := status.FromError(err) + logAccessStatus(ctx, accLog.Info(), err). + Uint32("_grpc_status_code", uint32(grpcStatus.Code())). Int64("_tx_duration_ms", time.Until(start).Milliseconds()). Msg("TX finished") } diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go index ae019d8fd..d309b6459 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go @@ -101,7 +101,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) assert.Equal(t, "TX started", logEvent1["message"]) - require.Len(t, logEvent2, 10) + require.Len(t, logEvent2, 11) assert.Equal(t, "info", logEvent2["level"]) assert.Contains(t, logEvent2, "_tx_start") assert.Contains(t, logEvent2, "_tx_duration_ms") @@ -113,6 +113,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) assert.Equal(t, true, logEvent2["_access_granted"]) assert.Equal(t, "foo", logEvent2["_subject"]) + assert.Equal(t, float64(codes.OK), logEvent2["_grpc_status_code"]) assert.Equal(t, "TX finished", logEvent2["message"]) }, }, @@ -153,7 +154,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { assert.Equal(t, "127.0.0.1", logEvent1["_x_forwarded_for"]) assert.Equal(t, "TX started", logEvent1["message"]) - require.Len(t, logEvent2, 13) + require.Len(t, logEvent2, 14) assert.Equal(t, "info", logEvent2["level"]) assert.Contains(t, logEvent2, "_tx_start") assert.Contains(t, logEvent2, "_tx_duration_ms") @@ -166,6 +167,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { assert.Equal(t, "test error", logEvent2["error"]) assert.Equal(t, "for=127.0.0.1", logEvent1["_forwarded"]) assert.Equal(t, "127.0.0.1", logEvent1["_x_forwarded_for"]) + assert.Equal(t, float64(codes.Unknown), logEvent2["_grpc_status_code"]) assert.Equal(t, "TX finished", logEvent2["message"]) }, }, @@ -208,7 +210,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { assert.NotEqual(t, parentCtx.SpanID().String(), logEvent1["_parent_id"]) assert.Equal(t, "TX started", logEvent1["message"]) - require.Len(t, logEvent2, 11) + require.Len(t, logEvent2, 12) assert.Equal(t, "info", logEvent2["level"]) assert.Contains(t, logEvent2, "_tx_start") assert.Contains(t, logEvent2, "_tx_duration_ms") @@ -221,6 +223,7 @@ func TestAccessLogInterceptorForKnownService(t *testing.T) { assert.Equal(t, false, logEvent2["_access_granted"]) assert.Equal(t, "bar", logEvent2["_subject"]) assert.Equal(t, "test error", logEvent2["error"]) + assert.Equal(t, float64(codes.OK), logEvent2["_grpc_status_code"]) assert.Equal(t, "TX finished", logEvent2["message"]) }, }, @@ -353,7 +356,7 @@ func TestAccessLogInterceptorForUnknownService(t *testing.T) { assert.Contains(t, logEvent1, "_trace_id") assert.Equal(t, "TX started", logEvent1["message"]) - require.Len(t, logEvent2, 10) + require.Len(t, logEvent2, 11) assert.Equal(t, "info", logEvent2["level"]) assert.Contains(t, logEvent2, "_tx_start") assert.Contains(t, logEvent2, "_tx_duration_ms") @@ -365,5 +368,6 @@ func TestAccessLogInterceptorForUnknownService(t *testing.T) { assert.Equal(t, logEvent2["_parent_id"], logEvent2["_parent_id"]) assert.Equal(t, false, logEvent2["_access_granted"]) assert.Contains(t, logEvent2["error"], "unknown service or method") + assert.Equal(t, float64(codes.Unknown), logEvent2["_grpc_status_code"]) assert.Equal(t, "TX finished", logEvent2["message"]) } From d5cdd5a4749dfe143428cbe4634b5072ff5cff23 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 23:38:48 +0100 Subject: [PATCH 59/67] comment added --- internal/handler/envoyextauth/grpcv3/service.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/handler/envoyextauth/grpcv3/service.go b/internal/handler/envoyextauth/grpcv3/service.go index 8f0f8c429..61276df84 100644 --- a/internal/handler/envoyextauth/grpcv3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -82,6 +82,10 @@ func newService( errormiddleware.WithNoRuleErrorCode(service.Respond.With.NoRuleError.Code), errormiddleware.WithInternalServerErrorCode(service.Respond.With.InternalError.Code), ), + // the accesslogger is used here to have access to the error object + // as it will be replaced by a CheckResponse object returned to envoy + // and will not contain all the details, typically required to enable + // error traceback accessLogger.Unary(), loggermiddleware.New(logger), cachemiddleware.New(cch), From 6b85527c0e426ef05d96dc96f05b2cb91fc237b0 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Sun, 5 Mar 2023 23:40:21 +0100 Subject: [PATCH 60/67] linter warning resolved --- .../grpcv3/middleware/accesslog/interceptor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go index d309b6459..b325632aa 100644 --- a/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go +++ b/internal/handler/envoyextauth/grpcv3/middleware/accesslog/interceptor_test.go @@ -49,7 +49,7 @@ import ( "github.com/dadrus/heimdall/internal/x/testsupport" ) -func TestAccessLogInterceptorForKnownService(t *testing.T) { +func TestAccessLogInterceptorForKnownService(t *testing.T) { //nolint:maintidx otel.SetTracerProvider(sdktrace.NewTracerProvider()) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) From 8e38c3e4a2a8a8bb09017e45d9e904558ed9ee85 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Mon, 6 Mar 2023 00:39:33 +0100 Subject: [PATCH 61/67] flag description updated --- cmd/serve/decision.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/serve/decision.go b/cmd/serve/decision.go index 59fda395d..9c163ed7c 100644 --- a/cmd/serve/decision.go +++ b/cmd/serve/decision.go @@ -44,7 +44,7 @@ func NewDecisionCommand() *cobra.Command { } cmd.PersistentFlags().Bool("envoy-extauth", false, - "Whether to start the decision mode for integration with envoy extauth gRPC service") + "If specified, decision mode is started for integration with envoy extauth gRPC service") return cmd } From 78ecabbd3d74e8a14d7a77f70ba2261c6aedd8fb Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Mon, 6 Mar 2023 00:40:05 +0100 Subject: [PATCH 62/67] documentation added --- docs/content/docs/guides/envoy.adoc | 47 +++++++++++++++++-- .../docs/operations/observability.adoc | 28 ++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/docs/content/docs/guides/envoy.adoc b/docs/content/docs/guides/envoy.adoc index 8f61d3587..7593a522a 100644 --- a/docs/content/docs/guides/envoy.adoc +++ b/docs/content/docs/guides/envoy.adoc @@ -10,7 +10,7 @@ menu: weight: 20 --- -https://www.envoyproxy.io/[Envoy] is a high performance distributed proxy designed for single services and applications, as well as a communication bus and “universal data plane” designed for large microservice “service mesh” architectures. When operating heimdall in link:{{< relref "/docs/getting_started/concepts.adoc#_decision_mode" >}}[Decision Operation Mode], integration with Envoy can be achieved by making use of an https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto.html?highlight=allowed_upstream_headers[External Authorization] filter. In such setup, Envoy delegates authentication and authorization to heimdall. If heimdall answers with a `200 OK` HTTP code, Envoy grants access and forwards the original request to the upstream service. Otherwise, the response from heimdall is treated as an error and is returned to the client. +https://www.envoyproxy.io/[Envoy] is a high performance distributed proxy designed for single services and applications, as well as a communication bus and “universal data plane” designed for large microservice “service mesh” architectures. When operating heimdall in link:{{< relref "/docs/getting_started/concepts.adoc#_decision_mode" >}}[Decision Operation Mode], integration with Envoy can be achieved by making use of an https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto.html[External Authorization] filter (both, `grpc_service`, as well as `http_service` are supported). In such setup, Envoy delegates authentication and authorization to heimdall. If heimdall answers with a `200 OK` HTTP code, Envoy grants access and forwards the original request to the upstream service. Otherwise, the response from heimdall is treated as an error and is returned to the client. To achieve this, configure Envoy @@ -35,9 +35,34 @@ clusters: port_value: 4456 # other cluster entries ---- -* to include an https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto.html?highlight=allowed_upstream_headers[External Authorization] HTTP filter in the definition of the HTTP connection manager, as well as to let that filter contain the required header name(s), heimdall sets in the HTTP responses (depends on your link:{{< relref "/docs/configuration/rules/pipeline_mechanisms/contextualizers.adoc" >}}[Contextualizers] and link:{{< relref "/docs/configuration/rules/pipeline_mechanisms/unifiers.adoc" >}}[Unifiers] configuration). + -The following snipped shows, how an https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto.html?highlight=allowed_upstream_headers[External Authorization] can be defined to let Envoy communicating with heimdall by making use of the previously defined `cluster` (see snippet from above) as well as forwarding all request headers to heimdall and to let it forward headers, set by heimdall in its responses (here the `Authorization` header) to the upstream services. +If you want to integrate via Envoys `grpc_service` (see below), the cluster entry from above must have `http2_protocol_options` configured, as otherwise envoy will use HTTP 1 for GRPC communication, which is actually not allowed by GRPC (only HTTP 2 is supported). Here an updated snipped: ++ +[source, yaml] +---- +clusters: + # other cluster entries + - name: ext-authz + type: strict_dns + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: {} + load_assignment: + cluster_name: ext-authz + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: heimdall + port_value: 4456 + # other cluster entries +---- +* to include an https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto.html[External Authorization] HTTP filter in the definition of the HTTP connection manager and depending on the used configuration, either configure the `http_service` and let it contain the required header name(s), heimdall sets in the HTTP responses (depends on your link:{{< relref "/docs/configuration/rules/pipeline_mechanisms/contextualizers.adoc" >}}[Contextualizers] and link:{{< relref "/docs/configuration/rules/pipeline_mechanisms/unifiers.adoc" >}}[Unifiers] configuration), or configure the `grpc_service`. ++ +The following snipped shows, how an https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto.html[External Authorization] can be defined using `http_service` to let Envoy communicating with heimdall by making use of the previously defined `cluster` (see snippet from above) as well as forwarding all request headers to heimdall and to let it forward headers, set by heimdall in its responses (here the `Authorization` header) to the upstream services. + [source, yaml] ---- @@ -64,6 +89,22 @@ http_filters: - exact: authorization # other http filter ---- ++ +The following snipped shows, how an https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto.html[External Authorization] can be defined using `grpc_service` to let Envoy communicating with heimdall by making use of the previously defined `cluster` (see snippet from above). In that configuration envoy by default forwards all request header to heimdall and also forwards headers, set by heimdall in its responses to the upstream services. ++ +[source, yaml] +---- +http_filters: + # other http filter + - name: envoy.filters.http.ext_authz + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz + transport_api_version: V3 + grpc_service: + envoy_grpc: + cluster_name: ext-authz + # other http filter +---- NOTE: Envoy does not set `X-Forwarded-*` headers, as long as the `envoy.filters.http.dynamic_forward_proxy` is not configured. That means, matching of URLs happens based on those URLs, used by Envoy while communicating with heimdall. That means your rules should ignore the scheme and host parts, respectively use the values specific for heimdall and not of the domain. diff --git a/docs/content/docs/operations/observability.adoc b/docs/content/docs/operations/observability.adoc index 5983e3ac4..1a13aa8cc 100644 --- a/docs/content/docs/operations/observability.adoc +++ b/docs/content/docs/operations/observability.adoc @@ -104,12 +104,19 @@ Following are the fields, which are always set for both events: * `_tx_start` - Timestamp in Unix epoch format, when the transaction started. * `_client_ip` - The IP of the client of the request. + +If the event has been emitted for an HTTP request, following fields are set as well: + * `_http_method` - The HTTP method used by the client while calling heimdall's endpoint. * `_http_path` - The used HTTP path. * `_http_user_agent` - The agent used by the client. The value is taken from the HTTP "User-Agent" header. * `_http_host` - The host part of the URI, the client is using while communicating with Heimdall. * `_http_scheme` - The scheme part of the URI, the client is using while communicating with Heimdall. +If the event has been emitted for a GRPC request, following fields are set: + +* `_grpc_method` - The full GRPC method used. + If the request comes from an intermediary, like e.g. an API Gateway and heimdall is configured to trust that "proxy" (see link:{{< relref "/docs/configuration/services/decision.adoc#_trusted_proxies" >}}[`trusted_proxies` configuration] of the Decision service, as well as the link:{{< relref "/docs/configuration/services/proxy.adoc#_trusted_proxies" >}}[`trusted_proxies` configuration] of the Proxy service), then following fields will be part of the events as well if the corresponding HTTP headers were sent. * `_http_x_forwarded_proto` - The value of the "X-Forwarded-Proto" header. @@ -122,12 +129,19 @@ If the request comes from an intermediary, like e.g. an API Gateway and heimdall Following are the fields, which are set in the transaction finalization event in addition: * `_body_bytes_sent` - The length of the response body. -* `_http_status_code` - The numeric HTTP response status code * `_tx_duration_ms` - The duration of the transaction in milliseconds. If heimdall is operated in proxy mode, it will also include the time used to communicate with the upstream service. * `_access_granted` - Set either to `true` or `false`, indicating whether heimdall granted access or not. * `_subject` - The subject identifier if the access was granted. * `_error` - The information about an error, which e.g. led to the denial of the request. +If the finalization event has been emitted for an HTTP request, following fields are set as well: + +* `_http_status_code` - The numeric HTTP response status code + +If the finalization event has been emitted for a GRPC request, following fields are set: + +* `_grpc_status_code` - The numeric GRPC status code. + Following are the fields, which are set if tracing is enabled: * `_trace_id` - The trace id as defined by OpenTelemetry. @@ -446,6 +460,18 @@ The following table provide detailed information about these | Counter | Count all http requests by service (decision, proxy, management), status code, method and path. +| `grpc_request_duration_seconds` +| Histogram +| Duration of all requests by tunneled HTTP status code, service (decision) and method, as well as by GRPC method and status code. + +| `grpc_requests_in_progress_total` +| Gauge +| All the requests in progress by service (decision), tunneled HTTP method, as well as by GRPC method. + +| `grpc_requests_total` +| Counter +| Count all requests by service (decision), tunneled HTTP status code, service and method, as well as by GRPC method and status code. + 3+| _Certificate expiry information_ | `certificate_expiry_seconds` From b01429e2543dfbe9aab6d1f26958a30d17851731 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Mon, 6 Mar 2023 00:51:40 +0100 Subject: [PATCH 63/67] fla renamed --- cmd/serve/decision.go | 4 ++-- cmd/serve/decision_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/serve/decision.go b/cmd/serve/decision.go index 9c163ed7c..ca96acd0f 100644 --- a/cmd/serve/decision.go +++ b/cmd/serve/decision.go @@ -43,7 +43,7 @@ func NewDecisionCommand() *cobra.Command { }, } - cmd.PersistentFlags().Bool("envoy-extauth", false, + cmd.PersistentFlags().Bool("envoy-grpc", false, "If specified, decision mode is started for integration with envoy extauth gRPC service") return cmd @@ -52,7 +52,7 @@ func NewDecisionCommand() *cobra.Command { func createDecisionApp(cmd *cobra.Command) (*fx.App, error) { configPath, _ := cmd.Flags().GetString("config") envPrefix, _ := cmd.Flags().GetString("env-config-prefix") - useEnvoyExtAuth, _ := cmd.Flags().GetBool("envoy-extauth") + useEnvoyExtAuth, _ := cmd.Flags().GetBool("envoy-grpc") opts := []fx.Option{ fx.NopLogger, diff --git a/cmd/serve/decision_test.go b/cmd/serve/decision_test.go index dd39a2bf8..8d8dad62a 100644 --- a/cmd/serve/decision_test.go +++ b/cmd/serve/decision_test.go @@ -54,7 +54,7 @@ func TestCreateDecisionAppForEnvoyGRPCRequests(t *testing.T) { t.Setenv("SERVE_MANAGEMENT_PORT", strconv.Itoa(port2)) cmd := NewDecisionCommand() - err = cmd.ParseFlags([]string{"--envoy-extauth=true"}) + err = cmd.ParseFlags([]string{"--envoy-grpc"}) require.NoError(t, err) _, err = createDecisionApp(cmd) From 2dbaefd0762c7827e94e52e26435ba2e40a90829 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Mon, 6 Mar 2023 00:52:14 +0100 Subject: [PATCH 64/67] documentation update --- docs/content/docs/configuration/services/decision.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/configuration/services/decision.adoc b/docs/content/docs/configuration/services/decision.adoc index 68a59cd44..d92a232f8 100644 --- a/docs/content/docs/configuration/services/decision.adoc +++ b/docs/content/docs/configuration/services/decision.adoc @@ -9,7 +9,7 @@ menu: parent: "Services" --- -Decision is one of the operating modes supported by heimdall, used if you start heimdall with `heimdall serve decision`. By default, heimdall listens on `0.0.0.0:4456/` endpoint for incoming requests in this mode of operation and also configures useful default timeouts. No other options are configured. You can, and should however adjust the configuration for your needs. +Decision is one of the operating modes supported by heimdall, used if you start heimdall with `heimdall serve decision` or `heimdall serve decision --envoy-grpc`. By default, heimdall listens on `0.0.0.0:4456/` endpoint for incoming requests in this mode of operation and also configures useful default timeouts. No other options are configured. You can, and should however adjust the configuration for your needs. This service exposes only the Decision service endpoint. From 2c6c6ba11347cec26754243afab9c3c6606549ab Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Mon, 6 Mar 2023 00:52:36 +0100 Subject: [PATCH 65/67] documentation update --- docs/content/docs/guides/envoy.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/guides/envoy.adoc b/docs/content/docs/guides/envoy.adoc index 7593a522a..9a372a5d5 100644 --- a/docs/content/docs/guides/envoy.adoc +++ b/docs/content/docs/guides/envoy.adoc @@ -106,7 +106,7 @@ http_filters: # other http filter ---- -NOTE: Envoy does not set `X-Forwarded-*` headers, as long as the `envoy.filters.http.dynamic_forward_proxy` is not configured. That means, matching of URLs happens based on those URLs, used by Envoy while communicating with heimdall. That means your rules should ignore the scheme and host parts, respectively use the values specific for heimdall and not of the domain. +NOTE: Envoy does not set `X-Forwarded-*` headers, as long as the `envoy.filters.http.dynamic_forward_proxy` is not configured. In such cases matching of URLs happens based on those URLs, used by Envoy while communicating with heimdall. That means your rules should ignore the scheme and host parts, respectively use the values specific for heimdall and not of the domain. == Demo Setup From fea779ec4ef6082e3e520fe24e308badc4a4496b Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Mon, 6 Mar 2023 00:58:21 +0100 Subject: [PATCH 66/67] documentation update --- docs/content/docs/guides/envoy.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/guides/envoy.adoc b/docs/content/docs/guides/envoy.adoc index 9a372a5d5..3a91ae0a0 100644 --- a/docs/content/docs/guides/envoy.adoc +++ b/docs/content/docs/guides/envoy.adoc @@ -10,7 +10,7 @@ menu: weight: 20 --- -https://www.envoyproxy.io/[Envoy] is a high performance distributed proxy designed for single services and applications, as well as a communication bus and “universal data plane” designed for large microservice “service mesh” architectures. When operating heimdall in link:{{< relref "/docs/getting_started/concepts.adoc#_decision_mode" >}}[Decision Operation Mode], integration with Envoy can be achieved by making use of an https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto.html[External Authorization] filter (both, `grpc_service`, as well as `http_service` are supported). In such setup, Envoy delegates authentication and authorization to heimdall. If heimdall answers with a `200 OK` HTTP code, Envoy grants access and forwards the original request to the upstream service. Otherwise, the response from heimdall is treated as an error and is returned to the client. +https://www.envoyproxy.io/[Envoy] is a high performance distributed proxy designed for single services and applications, as well as a communication bus and “universal data plane” designed for large microservice “service mesh” architectures. When operating heimdall in link:{{< relref "/docs/getting_started/concepts.adoc#_decision_mode" >}}[Decision Operation Mode], integration with Envoy can be achieved by making use of an https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto.html[External Authorization] filter. In such setup, Envoy delegates authentication and authorization to heimdall. If heimdall answers with a `200 OK` HTTP code, Envoy grants access and forwards the original request to the upstream service. Otherwise, the response from heimdall is treated as an error and is returned to the client. To achieve this, configure Envoy @@ -36,7 +36,7 @@ clusters: # other cluster entries ---- + -If you want to integrate via Envoys `grpc_service` (see below), the cluster entry from above must have `http2_protocol_options` configured, as otherwise envoy will use HTTP 1 for GRPC communication, which is actually not allowed by GRPC (only HTTP 2 is supported). Here an updated snipped: +If you want to integrate via Envoy's `grpc_service` (see below), the cluster entry from above must have `http2_protocol_options` configured, as otherwise envoy will use HTTP 1 for GRPC communication, which is actually not allowed by GRPC (only HTTP 2 is supported). Here is an updated snipped: + [source, yaml] ---- From 06f196e3b939785fed1a7ad316a4f9b884e6e73b Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Mon, 6 Mar 2023 01:08:52 +0100 Subject: [PATCH 67/67] docker-compose quickstarts updated --- examples/docker-compose/quickstarts/README.md | 2 +- .../docker-compose/quickstarts/docker-compose-envoy-grpc.yaml | 4 ++-- .../docker-compose/quickstarts/docker-compose-envoy-http.yaml | 2 +- examples/docker-compose/quickstarts/docker-compose.yaml | 2 +- examples/docker-compose/quickstarts/heimdall-config.yaml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/docker-compose/quickstarts/README.md b/examples/docker-compose/quickstarts/README.md index 1cff5784f..7510b5ecc 100644 --- a/examples/docker-compose/quickstarts/README.md +++ b/examples/docker-compose/quickstarts/README.md @@ -61,7 +61,7 @@ In that setup heimdall is integrated with Envoy Proxy. All requests are sent to docker-compose -f docker-compose.yaml -f docker-compose-envoy-grpc.yaml up ``` - to see integration using the envoy GRPC extauthz decision service in action. + to see integration using the envoy GRPC extauthz decision service in action (not available before v0.7.0-alpha). 2. Play with it diff --git a/examples/docker-compose/quickstarts/docker-compose-envoy-grpc.yaml b/examples/docker-compose/quickstarts/docker-compose-envoy-grpc.yaml index feeb958b3..ebfee7971 100644 --- a/examples/docker-compose/quickstarts/docker-compose-envoy-grpc.yaml +++ b/examples/docker-compose/quickstarts/docker-compose-envoy-grpc.yaml @@ -2,7 +2,7 @@ version: '3.7' services: heimdall: - command: -c /heimdall/conf/heimdall.yaml serve decision --envoy-extauth + command: -c /heimdall/conf/heimdall.yaml serve decision --envoy-grpc edge-router: image: envoyproxy/envoy:v1.25.1 @@ -10,4 +10,4 @@ services: - ./envoy-config-grpc.yaml:/envoy.yaml:ro ports: - 9090:9090 - command: -c /envoy.yaml -l debug \ No newline at end of file + command: -c /envoy.yaml \ No newline at end of file diff --git a/examples/docker-compose/quickstarts/docker-compose-envoy-http.yaml b/examples/docker-compose/quickstarts/docker-compose-envoy-http.yaml index b0b141312..ff22140e9 100644 --- a/examples/docker-compose/quickstarts/docker-compose-envoy-http.yaml +++ b/examples/docker-compose/quickstarts/docker-compose-envoy-http.yaml @@ -7,4 +7,4 @@ services: - ./envoy-config-http.yaml:/envoy.yaml:ro ports: - 9090:9090 - command: -c /envoy.yaml -l debug \ No newline at end of file + command: -c /envoy.yaml \ No newline at end of file diff --git a/examples/docker-compose/quickstarts/docker-compose.yaml b/examples/docker-compose/quickstarts/docker-compose.yaml index d223e7645..686224a1c 100644 --- a/examples/docker-compose/quickstarts/docker-compose.yaml +++ b/examples/docker-compose/quickstarts/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3.7' services: heimdall: - image: heimdall:local + image: dadrus/heimdall:latest volumes: - ./heimdall-config.yaml:/heimdall/conf/heimdall.yaml:ro - ./upstream-rules.yaml:/heimdall/conf/rules.yaml:ro diff --git a/examples/docker-compose/quickstarts/heimdall-config.yaml b/examples/docker-compose/quickstarts/heimdall-config.yaml index 7fc44035b..919235419 100644 --- a/examples/docker-compose/quickstarts/heimdall-config.yaml +++ b/examples/docker-compose/quickstarts/heimdall-config.yaml @@ -1,5 +1,5 @@ log: - level: debug + level: info serve: decision: