Skip to content

Commit 7344074

Browse files
authored
Enriched Span Attributes on upstream auth (#56)
* Update authenticator.go * create a new reason for when upstream auth header is empty * Added Http Headers to Span and enriched spans for upstream auth * added "upstream_auth_empty_tokens_total" metric
1 parent f8a9555 commit 7344074

File tree

4 files changed

+92
-35
lines changed

4 files changed

+92
-35
lines changed

pkg/auth/authenticator.go

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package auth
22

33
import (
44
"context"
5+
"errors"
56
"net/http"
67
"net/url"
78
"os"
@@ -15,6 +16,7 @@ import (
1516
"github.com/snapp-incubator/Cerberus/internal/tracing"
1617
"go.opentelemetry.io/otel/attribute"
1718
otelcodes "go.opentelemetry.io/otel/codes"
19+
"go.opentelemetry.io/otel/trace"
1820
"google.golang.org/grpc/codes"
1921
"google.golang.org/grpc/status"
2022
)
@@ -159,27 +161,8 @@ func (a *Authenticator) Check(ctx context.Context, request *Request) (finalRespo
159161
wsvc, ns, reason := readRequestContext(request)
160162

161163
// generate opentelemetry span with given parameters
162-
parentCtx := tracing.ReadParentSpanFromRequest(ctx, request.Request)
163-
ctx, span := tracing.StartSpan(parentCtx, "CheckFunction",
164-
attribute.String("webservice", wsvc),
165-
attribute.String("namespace", ns),
166-
)
167-
defer func() {
168-
extraAttrs := []attribute.KeyValue{
169-
attribute.String("cerberus-reason", string(reason)),
170-
}
171-
if finalResponse != nil {
172-
extraAttrs = append(extraAttrs,
173-
attribute.Bool("final-response-ok", finalResponse.Allow),
174-
)
175-
for k, v := range finalResponse.Response.Header {
176-
extraAttrs = append(extraAttrs,
177-
attribute.String("final-extra-headers-"+k, strings.Join(v, ",")),
178-
)
179-
}
180-
}
181-
tracing.EndSpan(span, start_time, extraAttrs...)
182-
}()
164+
ctx, span := startSpan(ctx, request.Request, wsvc, ns)
165+
defer endSpan(span, start_time, finalResponse, reason)
183166

184167
if reason != "" {
185168
return generateResponse(reason, nil), nil
@@ -193,6 +176,7 @@ func (a *Authenticator) Check(ctx context.Context, request *Request) (finalRespo
193176
var extraHeaders ExtraHeaders
194177

195178
reason, wsvcCacheEntry := a.readService(wsvc)
179+
addHeadersToSpan(request.Request, wsvcCacheEntry, span)
196180
if reason == "" {
197181
var cerberusExtraHeaders CerberusExtraHeaders
198182

@@ -214,6 +198,53 @@ func (a *Authenticator) Check(ctx context.Context, request *Request) (finalRespo
214198
return
215199
}
216200

201+
// startSpan starts span for Check Function
202+
func startSpan(ctx context.Context, request http.Request, wsvc string, ns string) (context.Context, trace.Span) {
203+
parentCtx := tracing.ReadParentSpanFromRequest(ctx, request)
204+
return tracing.StartSpan(parentCtx, "CheckFunction",
205+
attribute.String("webservice", wsvc),
206+
attribute.String("namespace", ns),
207+
)
208+
}
209+
210+
// endSpan ends Check Function span and adds attributes.
211+
func endSpan(span trace.Span, start_time time.Time, finalResponse *Response, reason CerberusReason) {
212+
extraAttrs := []attribute.KeyValue{
213+
attribute.String("cerberus-reason", string(reason)),
214+
}
215+
if finalResponse != nil {
216+
extraAttrs = append(extraAttrs,
217+
attribute.Bool("final-response-ok", finalResponse.Allow),
218+
)
219+
for k, v := range finalResponse.Response.Header {
220+
extraAttrs = append(extraAttrs,
221+
attribute.String("final-extra-headers-"+k, strings.Join(v, ",")),
222+
)
223+
}
224+
}
225+
tracing.EndSpan(span, start_time, extraAttrs...)
226+
}
227+
228+
// addHeadersToSpan adds request headers to Span
229+
func addHeadersToSpan(request http.Request, service WebservicesCacheEntry, span trace.Span) {
230+
// Add request headers to span
231+
for headerName, headerValues := range request.Header {
232+
headerValue := strings.Join(headerValues, ",")
233+
234+
// For Authorization header, only include first 20 chars
235+
if headerName == service.Spec.LookupHeader && len(headerValue) > 20 {
236+
headerValue = headerValue[:20]
237+
}
238+
if hasUpstreamAuth(service) && headerName == service.Spec.UpstreamHttpAuth.ReadTokenFrom && len(headerValue) > 20 {
239+
headerValue = headerValue[:20]
240+
}
241+
242+
span.SetAttributes(
243+
attribute.String("http.header."+headerName, headerValue),
244+
)
245+
}
246+
}
247+
217248
func readRequestContext(request *Request) (wsvc string, ns string, reason CerberusReason) {
218249
wsvc = request.Context["webservice"]
219250
if wsvc == "" {
@@ -253,14 +284,23 @@ func NewAuthenticator(logger logr.Logger) *Authenticator {
253284
// validateUpstreamAuthRequest validates the service before calling the upstream.
254285
// when calling the upstream authentication, one of read or write tokens must be
255286
// empty and the upstream address must be a valid url.
256-
func validateUpstreamAuthRequest(service WebservicesCacheEntry) CerberusReason {
287+
func validateUpstreamAuthRequest(service WebservicesCacheEntry, request *Request) CerberusReason {
257288
if service.Spec.UpstreamHttpAuth.ReadTokenFrom == "" ||
258289
service.Spec.UpstreamHttpAuth.WriteTokenTo == "" {
259290
return CerberusReasonTargetAuthTokenEmpty
260291
}
261292
if !govalidator.IsRequestURL(service.Spec.UpstreamHttpAuth.Address) {
262293
return CerberusReasonInvalidUpstreamAddress
263294
}
295+
if request != nil {
296+
token := request.Request.Header.Get(service.Spec.UpstreamHttpAuth.ReadTokenFrom)
297+
if token == "" {
298+
upstreamAuthEmptyTokens.Inc()
299+
300+
// uncomment if you want to stop upstream auth call when token is empty
301+
// return CerberusReasonUpstreamAuthHeaderEmpty
302+
}
303+
}
264304
return ""
265305
}
266306

@@ -332,12 +372,16 @@ func (a *Authenticator) checkServiceUpstreamAuth(service WebservicesCacheEntry,
332372
)
333373
}()
334374

335-
if reason := validateUpstreamAuthRequest(service); reason != "" {
375+
if reason := validateUpstreamAuthRequest(service, request); reason != "" {
376+
span.RecordError(errors.New("upstream auth request validation:" + string(reason)))
377+
span.SetStatus(otelcodes.Error, "upstream auth http request faild")
336378
return reason
337379
}
338380
upstreamAuth := service.Spec.UpstreamHttpAuth
339381
req, err := setupUpstreamAuthRequest(&upstreamAuth, request)
340382
if err != nil {
383+
span.RecordError(err)
384+
span.SetStatus(otelcodes.Error, "failed to create upstream auth request")
341385
return CerberusReasonUpstreamAuthNoReq
342386
}
343387
a.adjustTimeout(upstreamAuth.Timeout, downstreamDeadline, hasDownstreamDeadline)
@@ -368,6 +412,8 @@ func (a *Authenticator) checkServiceUpstreamAuth(service WebservicesCacheEntry,
368412
}
369413

370414
if resp.StatusCode != http.StatusOK {
415+
span.RecordError(err)
416+
span.SetStatus(otelcodes.Error, "upstream auth non 200 status code")
371417
return CerberusReasonUnauthorized
372418
}
373419
// add requested careHeaders to extraHeaders for response

pkg/auth/authenticator_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -768,37 +768,37 @@ func TestValidateUpstreamAuthRequest(t *testing.T) {
768768
service := WebservicesCacheEntry{}
769769
service.Spec.UpstreamHttpAuth.ReadTokenFrom = ""
770770
service.Spec.UpstreamHttpAuth.WriteTokenTo = ""
771-
reason := validateUpstreamAuthRequest(service)
771+
reason := validateUpstreamAuthRequest(service, nil)
772772
assert.Equal(t, CerberusReasonTargetAuthTokenEmpty, reason, "Expected target auth token empty")
773773

774774
// Test case 2: WriteTokenTo is empty
775775
service = WebservicesCacheEntry{}
776776
service.Spec.UpstreamHttpAuth.ReadTokenFrom = "token"
777777
service.Spec.UpstreamHttpAuth.WriteTokenTo = ""
778-
reason = validateUpstreamAuthRequest(service)
778+
reason = validateUpstreamAuthRequest(service, nil)
779779
assert.Equal(t, CerberusReasonTargetAuthTokenEmpty, reason, "Expected target auth token empty")
780780

781781
// Test case 3: ReadTokenFrom is empty
782782
service = WebservicesCacheEntry{}
783783
service.Spec.UpstreamHttpAuth.ReadTokenFrom = ""
784784
service.Spec.UpstreamHttpAuth.WriteTokenTo = "token"
785-
reason = validateUpstreamAuthRequest(service)
785+
reason = validateUpstreamAuthRequest(service, nil)
786786
assert.Equal(t, CerberusReasonTargetAuthTokenEmpty, reason, "Expected target auth token empty")
787787

788788
// Test case 4: Address is invalid
789789
service = WebservicesCacheEntry{}
790790
service.Spec.UpstreamHttpAuth.ReadTokenFrom = "token"
791791
service.Spec.UpstreamHttpAuth.WriteTokenTo = "token"
792792
service.Spec.UpstreamHttpAuth.Address = "not a valid URL"
793-
reason = validateUpstreamAuthRequest(service)
793+
reason = validateUpstreamAuthRequest(service, nil)
794794
assert.Equal(t, CerberusReasonInvalidUpstreamAddress, reason, "Expected invalid upstream address")
795795

796796
// Test case 5: Everything is valid
797797
service = WebservicesCacheEntry{}
798798
service.Spec.UpstreamHttpAuth.ReadTokenFrom = "token"
799799
service.Spec.UpstreamHttpAuth.WriteTokenTo = "token"
800800
service.Spec.UpstreamHttpAuth.Address = "http://example.com"
801-
reason = validateUpstreamAuthRequest(service)
801+
reason = validateUpstreamAuthRequest(service, nil)
802802
assert.Empty(t, reason, "Expected no reason")
803803
}
804804

pkg/auth/cerberus_reasons.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ const (
7777
// has an invalid upstream address in it's manifest
7878
CerberusReasonInvalidUpstreamAddress CerberusReason = "invalid-auth-upstream"
7979

80+
// CerberusReasonUpstreamAuthHeaderEmpty means that request header that
81+
// should be forwarded to upstream auth service is empty
82+
CerberusReasonUpstreamAuthHeaderEmpty CerberusReason = "upstream-auth-header-empty"
83+
8084
// CerberusReasonSourceAuthTokenEmpty means that requested webservice
8185
// does not contain source upstream auth lookup header in it's manifest
8286
CerberusReasonSourceAuthTokenEmpty CerberusReason = "upstream-source-identifier-empty"

pkg/auth/metrics.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const (
1313
HasUpstreamAuth = "upstream_auth_enabled"
1414
ObjectKindLabel = "kind"
1515
WithDownstreamDeadlineLabel = "with_downstream_deadline"
16-
WebserviceLabel = "webservice"
16+
WebserviceLabel = "webservice"
1717

1818
MetricsKindSecret = "secret"
1919
MetricsKindWebservice = "webservice"
@@ -131,6 +131,13 @@ var (
131131
},
132132
[]string{WithDownstreamDeadlineLabel},
133133
)
134+
135+
upstreamAuthEmptyTokens = prometheus.NewCounter(
136+
prometheus.CounterOpts{
137+
Name: "upstream_auth_empty_tokens_total",
138+
Help: "Total number of UpstreamAuth requests that token were empty",
139+
},
140+
)
134141
)
135142

136143
func init() {
@@ -196,9 +203,9 @@ func AddWithDownstreamDeadlineLabel(labels prometheus.Labels, hasDeadline bool)
196203
}
197204

198205
func AddWebserviceLabel(labels prometheus.Labels, wsvc string) prometheus.Labels {
199-
if labels == nil {
200-
labels = prometheus.Labels{}
201-
}
202-
labels[WebserviceLabel] = wsvc
203-
return labels
204-
}
206+
if labels == nil {
207+
labels = prometheus.Labels{}
208+
}
209+
labels[WebserviceLabel] = wsvc
210+
return labels
211+
}

0 commit comments

Comments
 (0)