From 74998d85ff0a6070e18e171650a6612b2daa6384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraci=20Paix=C3=A3o=20Kr=C3=B6hling?= Date: Tue, 10 Aug 2021 13:20:08 +0200 Subject: [PATCH] Changed pdata context to be a map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Juraci Paixão Kröhling --- config/configauth/pdatacontext.go | 81 ++++++++++++----- config/configauth/pdatacontext_test.go | 93 ++++++++++++++++++++ exporter/loggingexporter/logging_exporter.go | 15 +--- extension/oidcauthextension/README.md | 6 +- extension/oidcauthextension/extension.go | 8 +- internal/otlptext/databuffer.go | 7 ++ internal/otlptext/traces.go | 1 + model/pdata/pdatacontext.go | 6 ++ 8 files changed, 180 insertions(+), 37 deletions(-) create mode 100644 config/configauth/pdatacontext_test.go diff --git a/config/configauth/pdatacontext.go b/config/configauth/pdatacontext.go index eb8c635fb055..b317bb1b60ed 100644 --- a/config/configauth/pdatacontext.go +++ b/config/configauth/pdatacontext.go @@ -16,48 +16,87 @@ package configauth import ( "context" + "encoding/json" + "fmt" + "reflect" "go.opentelemetry.io/collector/model/pdata" ) type ctxKey struct{} -type AuthContext struct { - sub string - raw string - group []string +type AuthContext interface { + Equal(other interface{}) bool + GetAttribute(attrName string) interface{} } -func InjectAuthContext(ctx context.Context, sub, raw string, group []string) context.Context { - return context.WithValue(ctx, ctxKey{}, &AuthContext{ - sub: sub, - raw: raw, - group: group, - }) +func InjectAuthContext(ctx context.Context, attrs map[interface{}]interface{}) context.Context { + ac := &authC{delegate: attrs} + return context.WithValue(ctx, ctxKey{}, ac) } -func ExtractAuthContext(ctx context.Context) (*AuthContext, bool) { - ac, ok := ctx.Value(ctxKey{}).(*AuthContext) +func ExtractAuthContext(ctx context.Context) (AuthContext, bool) { + ac, ok := ctx.Value(ctxKey{}).(*authC) if !ok { return nil, false } return ac, true } -func InjectPDataContext(pda pdata.PDataContext, ac *AuthContext) { +func InjectPDataContext(pda pdata.PDataContext, ac AuthContext) { pda.Set(ctxKey{}, ac) } -func ExtractPDataContext(pda pdata.PDataContext) *AuthContext { - return pda.Get(ctxKey{}).(*AuthContext) +func ExtractPDataContext(pda pdata.PDataContext) AuthContext { + return pda.Get(ctxKey{}).(AuthContext) } -func (ac *AuthContext) Subject() string { - return ac.sub +type authC struct { + delegate map[interface{}]interface{} } -func (ac *AuthContext) Raw() string { - return ac.raw + +func (ac authC) Equal(other interface{}) bool { + if other == nil { + return false + } + otherAuthC, ok := other.(*authC) + if !ok { + return false + } + return reflect.DeepEqual(ac.delegate, otherAuthC.delegate) +} + +func (ac authC) GetAttribute(attrName string) interface{} { + return ac.delegate[attrName] +} + +func (ac authC) String() string { + return fmt.Sprintf("Auth Context: %v", ac.delegate) } -func (ac *AuthContext) Groups() []string { - return ac.group + +// MarshalJSON serializes our context into a JSON blob. Note that interface{} is converted to string or []string. +func (ac *authC) MarshalJSON() ([]byte, error) { + strmap := make(map[string]interface{}) + for k, v := range ac.delegate { + strmap[fmt.Sprintf("%v", k)] = v + } + return json.Marshal(strmap) +} + +// UnmarshalJSON deserializes our context from a JSON blob. Note that we cannot infer the original type before the serialization, +// so all entries are either string or []string. +func (ac *authC) UnmarshalJSON(data []byte) error { + strmap := make(map[string]interface{}) + if err := json.Unmarshal(data, &strmap); err != nil { + return err + } + + // converts the map[string]interface{} to map[interface{}]interface{} + delegate := make(map[interface{}]interface{}) + for k, v := range strmap { + delegate[k] = v + } + ac.delegate = delegate + + return nil } diff --git a/config/configauth/pdatacontext_test.go b/config/configauth/pdatacontext_test.go new file mode 100644 index 000000000000..85e728f62d00 --- /dev/null +++ b/config/configauth/pdatacontext_test.go @@ -0,0 +1,93 @@ +// Copyright The OpenTelemetry Authors +// +// 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. + +package configauth + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMarshalJSON(t *testing.T) { + for _, tt := range []struct { + name string + ac *authC + expected string + }{ + { + name: "simple", + ac: &authC{ + delegate: map[interface{}]interface{}{ + "subject": "username", + "tenant": "acme", + }, + }, + expected: `{"subject":"username","tenant":"acme"}`, + }, + { + name: "groups", + ac: &authC{ + delegate: map[interface{}]interface{}{ + "subject": "username", + "membership": []string{"dev", "ops"}, + }, + }, + expected: `{"membership":["dev","ops"],"subject":"username"}`, + }, + } { + t.Run(tt.name, func(t *testing.T) { + json, err := tt.ac.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, tt.expected, string(json)) + }) + } +} + +func TestUnmarshalJSON(t *testing.T) { + for _, tt := range []struct { + name string + json string + expected *authC + }{ + { + name: "simple", + json: `{"subject":"username","tenant":"acme"}`, + expected: &authC{ + delegate: map[interface{}]interface{}{ + "subject": "username", + "tenant": "acme", + }, + }, + }, + { + name: "groups", + json: `{"membership":["dev","ops"],"subject":"username"}`, + expected: &authC{ + delegate: map[interface{}]interface{}{ + "subject": "username", + "membership": []interface{}{"dev", "ops"}, + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + ac := &authC{} + err := json.Unmarshal([]byte(tt.json), ac) + assert.NoError(t, err) + assert.Equal(t, tt.expected, ac) + }) + } +} diff --git a/exporter/loggingexporter/logging_exporter.go b/exporter/loggingexporter/logging_exporter.go index fe0d4d170e62..daf0e49a95a0 100644 --- a/exporter/loggingexporter/logging_exporter.go +++ b/exporter/loggingexporter/logging_exporter.go @@ -23,7 +23,6 @@ import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config" - "go.opentelemetry.io/collector/config/configauth" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/exporter/exporterhelper" "go.opentelemetry.io/collector/internal/otlptext" @@ -39,19 +38,7 @@ type loggingExporter struct { } func (s *loggingExporter) pushTraces(_ context.Context, td pdata.Traces) error { - fields := []zap.Field{ - zap.Int("#spans", td.SpanCount()), - } - - ac := configauth.ExtractPDataContext(td.ResourceSpans().At(0).PDataContext()) - if ac != nil { - fields = append(fields, zap.String("sub", ac.Subject())) - fields = append(fields, zap.Strings("groups", ac.Groups())) - } else { - fields = append(fields, zap.String("auth", "none")) - } - - s.logger.Info("TracesExporter", fields...) + s.logger.Info("TracesExporter", zap.Int("#spans", td.SpanCount())) if !s.debug { return nil diff --git a/extension/oidcauthextension/README.md b/extension/oidcauthextension/README.md index 9561b7db43dc..95ed0a636403 100644 --- a/extension/oidcauthextension/README.md +++ b/extension/oidcauthextension/README.md @@ -1,6 +1,10 @@ # Authenticator - OIDC -This extension implements a `configauth.ServerAuthenticator`, to be used in receivers inside the `auth` settings. The authenticator type has to be set to `oidc`. +This extension implements a `configauth.ServerAuthenticator`, to be used in receivers inside the `auth` settings. The authenticator type has to be set to `oidc`. This authenticator places the following attributes in the authentication context: + +- `raw`, string, with the raw authentication token. +- `subject`, string, with the subject extracted from the token. This is typically the value for the field under `sub` in the token, or `username_claim` when specified. +- `membership`, list of string (`[]string`), with the groups extracted from the token. This is the value for the field specified via `groups_claim`, when available in the token. ## Configuration diff --git a/extension/oidcauthextension/extension.go b/extension/oidcauthextension/extension.go index c072c73e6b42..8a52b78bcc97 100644 --- a/extension/oidcauthextension/extension.go +++ b/extension/oidcauthextension/extension.go @@ -139,7 +139,13 @@ func (e *oidcExtension) Authenticate(ctx context.Context, headers map[string][]s return ctx, fmt.Errorf("failed to get groups from claims in the token: %w", err) } - return configauth.InjectAuthContext(ctx, sub, raw, groups), nil + attrs := map[interface{}]interface{}{ + "raw": raw, + "subject": sub, + "membership": groups, + } + + return configauth.InjectAuthContext(ctx, attrs), nil } // GRPCUnaryServerInterceptor is a helper method to provide a gRPC-compatible UnaryInterceptor, typically calling the authenticator's Authenticate method. diff --git a/internal/otlptext/databuffer.go b/internal/otlptext/databuffer.go index 0ed99c79c241..fb629691b76e 100644 --- a/internal/otlptext/databuffer.go +++ b/internal/otlptext/databuffer.go @@ -37,6 +37,13 @@ func (b *dataBuffer) logAttr(label string, value string) { b.logEntry(" %-15s: %s", label, value) } +func (b *dataBuffer) logPDataContext(label string, am pdata.PDataContext) { + b.logEntry("%s:", label) + am.Range(func(_ interface{}, value interface{}) { + b.logEntry(" -> %v", value) // the context key is likely an empty struct + }) +} + func (b *dataBuffer) logAttributeMap(label string, am pdata.AttributeMap) { if am.Len() == 0 { return diff --git a/internal/otlptext/traces.go b/internal/otlptext/traces.go index d3e3cc6548c3..2ebf9d99bec7 100644 --- a/internal/otlptext/traces.go +++ b/internal/otlptext/traces.go @@ -32,6 +32,7 @@ func (textTracesMarshaler) MarshalTraces(td pdata.Traces) ([]byte, error) { for i := 0; i < rss.Len(); i++ { buf.logEntry("ResourceSpans #%d", i) rs := rss.At(i) + buf.logPDataContext("Pipeline Data Context", rs.PDataContext()) buf.logAttributeMap("Resource labels", rs.Resource().Attributes()) ilss := rs.InstrumentationLibrarySpans() for j := 0; j < ilss.Len(); j++ { diff --git a/model/pdata/pdatacontext.go b/model/pdata/pdatacontext.go index 057a889dc726..52841f870f94 100644 --- a/model/pdata/pdatacontext.go +++ b/model/pdata/pdatacontext.go @@ -38,3 +38,9 @@ func (pdc PDataContext) Get(key interface{}) interface{} { } return nil } + +func (pdc PDataContext) Range(f func(k interface{}, v interface{})) { + for _, kv := range pdc.orig.List { + f(kv.Key, kv.Value) + } +}