From 52b98247b22e091c3d301b3fb3370064d82c3639 Mon Sep 17 00:00:00 2001 From: Andres Llausas Date: Tue, 11 Aug 2026 13:20:12 -0400 Subject: [PATCH 1/5] feat(service): enrich auth decision logs with configurable fields from filter metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read io.kuadrant.logging.fields from the CheckRequest filter metadata and append resolved key-value pairs to the info-level "outgoing authorization response" log line. Mirrors the existing metricLabels() pattern for io.kuadrant.metrics.labels — supports plain strings, numbers, booleans, and deferred CEL expressions (e.g. auth.identity.sub) resolved against the post-auth pipeline context. Identity fields resolve on both allow and deny decisions (authorization deny still has resolved identity from phase 1), and are gracefully omitted when authentication fails. Ref: CONNLINK-1384 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Andres Llausas --- pkg/service/auth.go | 18 ++- pkg/service/auth_pipeline.go | 42 +++++++ pkg/service/auth_pipeline_test.go | 187 ++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 4 deletions(-) diff --git a/pkg/service/auth.go b/pkg/service/auth.go index 77f52c65..83c5af95 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -284,13 +284,13 @@ func (a *AuthService) Check(parentContext gocontext.Context, req *envoy_auth.Che // If we couldn't find the AuthConfig in the config, we return and deny. if authConfig == nil { result := auth.AuthResult{Code: rpc.NOT_FOUND, Message: RESPONSE_MESSAGE_SERVICE_NOT_FOUND} - a.logAuthResult(result, ctx) + a.logAuthResult(result, ctx, nil) return a.deniedResponse(result), nil } if err := context.CheckContext(ctx); err != nil { result := auth.AuthResult{Code: rpc.UNAVAILABLE} - a.logAuthResult(result, ctx) + a.logAuthResult(result, ctx, nil) context.Cancel(ctx) span.RecordError(err) span.SetStatus(otel_codes.Error, err.Error()) @@ -308,7 +308,11 @@ func (a *AuthService) Check(parentContext gocontext.Context, req *envoy_auth.Che span.SetStatus(otel_codes.Error, err.Error()) } - a.logAuthResult(result, ctx) + var loggingFields map[string]string + if p, ok := pipeline.(*AuthPipeline); ok { + loggingFields = p.loggingFields() + } + a.logAuthResult(result, ctx, loggingFields) if result.Success() { return a.successResponse(result, ctx), nil @@ -401,7 +405,7 @@ func (a *AuthService) logAuthRequest(req *envoy_auth.CheckRequest, ctx gocontext } } -func (a *AuthService) logAuthResult(result auth.AuthResult, ctx gocontext.Context) { +func (a *AuthService) logAuthResult(result auth.AuthResult, ctx gocontext.Context, loggingFields map[string]string) { logger := log.FromContext(ctx) success := result.Success() baseLogData := []interface{}{"authorized", success, "response", result.Code.String()} @@ -415,12 +419,18 @@ func (a *AuthService) logAuthResult(result auth.AuthResult, ctx gocontext.Contex } logData = append(logData, "object", reducedResult) } + for k, v := range loggingFields { + logData = append(logData, k, v) + } logger.Info("outgoing authorization response", logData...) // info if logger.V(1).Enabled() { if !success { baseLogData = append(baseLogData, "object", result) } + for k, v := range loggingFields { + baseLogData = append(baseLogData, k, v) + } logger.V(1).Info("outgoing authorization response", baseLogData...) // debug } } diff --git a/pkg/service/auth_pipeline.go b/pkg/service/auth_pipeline.go index 3e2f2b30..42865774 100644 --- a/pkg/service/auth_pipeline.go +++ b/pkg/service/auth_pipeline.go @@ -556,6 +556,48 @@ func (pipeline *AuthPipeline) metricLabels() map[string]string { return labels } +func (pipeline *AuthPipeline) loggingFields() map[string]string { + fields := make(map[string]string) + + filteredMetadata := pipeline.GetRequest().GetAttributes().GetMetadataContext().GetFilterMetadata() + if loggingFields, ok := filteredMetadata["io.kuadrant.logging.fields"]; ok { + for k, v := range loggingFields.Fields { + switch kind := v.Kind.(type) { + case *structpb.Value_StringValue: + fields[k] = kind.StringValue + + case *structpb.Value_NumberValue: + fields[k] = fmt.Sprintf("%v", kind.NumberValue) + + case *structpb.Value_BoolValue: + fields[k] = fmt.Sprintf("%v", kind.BoolValue) + + case *structpb.Value_StructValue: + if celExprField, ok := kind.StructValue.Fields["cel_expr"]; ok { + if exprStr := celExprField.GetStringValue(); exprStr != "" { + expr, err := cel.NewExpression(exprStr) + if err != nil { + pipeline.Logger.Error(err, "failed to parse CEL expression", "expression", exprStr) + continue + } + value, err := expr.ResolveFor(pipeline.GetAuthorizationJSON()) + if err != nil { + pipeline.Logger.Error(err, "failed to evaluate CEL expression", "expression", exprStr) + continue + } + fields[k] = fmt.Sprintf("%v", value) + } + } + + default: + pipeline.Logger.V(1).Info("unexpected value kind", "kind", kind) + } + } + } + + return fields +} + func (pipeline *AuthPipeline) GetRequest() *envoy_auth.CheckRequest { return pipeline.Request } diff --git a/pkg/service/auth_pipeline_test.go b/pkg/service/auth_pipeline_test.go index c37b50cb..14e12fa5 100644 --- a/pkg/service/auth_pipeline_test.go +++ b/pkg/service/auth_pipeline_test.go @@ -703,3 +703,190 @@ func TestPipelineMetricLabels(t *testing.T) { assert.Equal(t, "", labels["unResolvableLabel"]) assert.Equal(t, "", labels["nullLabel"]) } + +func TestPipelineLoggingFields(t *testing.T) { + reqJSON := `{ + "attributes": { + "metadata_context": { + "filter_metadata": { + "io.kuadrant.logging.fields": { + "client_identity": { "cel_expr": "request.host" }, + "request_method": { "cel_expr": "request.method" }, + "static_field": "audit-v1", + "numeric_field": 42, + "bool_field": true, + "unresolvable": { "cel_expr": "auth.nonexistent.value" }, + "null_field": null + } + } + }, + "request": { + "http": { + "host": "my-api", + "path": "/v1/chat/completions", + "method": "POST", + "headers": { + "authorization": "Bearer token123" + } + } + } + } + }` + + var request envoy_auth.CheckRequest + _ = gojson.Unmarshal([]byte(reqJSON), &request) + + pipeline := newTestAuthPipeline( + evaluators.AuthConfig{}, + &request, + ) + + fields := pipeline.loggingFields() + + assert.Equal(t, 5, len(fields)) + + assert.Equal(t, "my-api", fields["client_identity"]) + assert.Equal(t, "POST", fields["request_method"]) + assert.Equal(t, "audit-v1", fields["static_field"]) + assert.Equal(t, "42", fields["numeric_field"]) + assert.Equal(t, "true", fields["bool_field"]) + assert.Equal(t, "", fields["unresolvable"]) + assert.Equal(t, "", fields["null_field"]) +} + +func TestPipelineLoggingFieldsEmpty(t *testing.T) { + var request envoy_auth.CheckRequest + _ = gojson.Unmarshal([]byte(rawRequest), &request) + + pipeline := newTestAuthPipeline( + evaluators.AuthConfig{}, + &request, + ) + + fields := pipeline.loggingFields() + + assert.Equal(t, 0, len(fields)) +} + +func TestLoggingFieldsResolvesIdentityOnAllow(t *testing.T) { + reqJSON := `{ + "attributes": { + "metadata_context": { + "filter_metadata": { + "io.kuadrant.logging.fields": { + "identity_anonymous": { "cel_expr": "auth.identity.anonymous" }, + "req_method": { "cel_expr": "request.method" } + } + } + }, + "request": { + "http": { + "host": "my-api", + "path": "/v1/chat/completions", + "method": "POST" + } + } + } + }` + + var request envoy_auth.CheckRequest + _ = gojson.Unmarshal([]byte(reqJSON), &request) + + authCred := auth.NewAuthCredential("", "") + pipeline := newTestAuthPipeline(evaluators.AuthConfig{ + IdentityConfigs: []auth.AuthConfigEvaluator{ + &evaluators.IdentityConfig{Name: "anonymous", Noop: &identity.Noop{AuthCredentials: authCred}}, + }, + }, &request) + + result := pipeline.Evaluate() + assert.Equal(t, result.Code, rpc.OK) + + fields := pipeline.loggingFields() + + assert.Equal(t, "true", fields["identity_anonymous"]) + assert.Equal(t, "POST", fields["req_method"]) +} + +func TestLoggingFieldsResolvesIdentityOnAuthzDeny(t *testing.T) { + reqJSON := `{ + "attributes": { + "metadata_context": { + "filter_metadata": { + "io.kuadrant.logging.fields": { + "identity_anonymous": { "cel_expr": "auth.identity.anonymous" }, + "req_method": { "cel_expr": "request.method" } + } + } + }, + "request": { + "http": { + "host": "my-api", + "path": "/v1/chat/completions", + "method": "POST" + } + } + } + }` + + var request envoy_auth.CheckRequest + _ = gojson.Unmarshal([]byte(reqJSON), &request) + + authCred := auth.NewAuthCredential("", "") + pipeline := newTestAuthPipeline(evaluators.AuthConfig{ + IdentityConfigs: []auth.AuthConfigEvaluator{ + &evaluators.IdentityConfig{Name: "anonymous", Noop: &identity.Noop{AuthCredentials: authCred}}, + }, + AuthorizationConfigs: []auth.AuthConfigEvaluator{ + &failConfig{}, + }, + }, &request) + + result := pipeline.Evaluate() + assert.Equal(t, result.Code, rpc.PERMISSION_DENIED) + + fields := pipeline.loggingFields() + + assert.Equal(t, "true", fields["identity_anonymous"]) + assert.Equal(t, "POST", fields["req_method"]) +} + +func TestLoggingFieldsGracefulOnAuthnFailure(t *testing.T) { + reqJSON := `{ + "attributes": { + "metadata_context": { + "filter_metadata": { + "io.kuadrant.logging.fields": { + "identity_anonymous": { "cel_expr": "auth.identity.anonymous" }, + "req_method": { "cel_expr": "request.method" } + } + } + }, + "request": { + "http": { + "host": "my-api", + "path": "/v1/chat/completions", + "method": "POST" + } + } + } + }` + + var request envoy_auth.CheckRequest + _ = gojson.Unmarshal([]byte(reqJSON), &request) + + pipeline := newTestAuthPipeline(evaluators.AuthConfig{ + IdentityConfigs: []auth.AuthConfigEvaluator{ + &failConfig{}, + }, + }, &request) + + result := pipeline.Evaluate() + assert.Equal(t, result.Code, rpc.UNAUTHENTICATED) + + fields := pipeline.loggingFields() + + _, hasIdentity := fields["identity_anonymous"] + assert.Check(t, !hasIdentity, "identity field should not resolve when authentication fails") + assert.Equal(t, "POST", fields["req_method"]) +} From 661f341bcf55d8062bee901cdd4df3377ad3675e Mon Sep 17 00:00:00 2001 From: Andres Llausas Date: Tue, 11 Aug 2026 14:56:54 -0400 Subject: [PATCH 2/5] fix(service): redact auth JSON in loggingFields and fix variable shadow Use log.RedactedAuthorizationJSON() when resolving CEL expressions in loggingFields() to prevent sensitive identity data from leaking into INFO-level logs. Rename inner variable from loggingFields to customFields to avoid shadowing the method name, matching the metricLabels() pattern. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Andres Llausas --- pkg/service/auth_pipeline.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/service/auth_pipeline.go b/pkg/service/auth_pipeline.go index 42865774..ca2ce5bd 100644 --- a/pkg/service/auth_pipeline.go +++ b/pkg/service/auth_pipeline.go @@ -560,8 +560,8 @@ func (pipeline *AuthPipeline) loggingFields() map[string]string { fields := make(map[string]string) filteredMetadata := pipeline.GetRequest().GetAttributes().GetMetadataContext().GetFilterMetadata() - if loggingFields, ok := filteredMetadata["io.kuadrant.logging.fields"]; ok { - for k, v := range loggingFields.Fields { + if customFields, ok := filteredMetadata["io.kuadrant.logging.fields"]; ok { + for k, v := range customFields.Fields { switch kind := v.Kind.(type) { case *structpb.Value_StringValue: fields[k] = kind.StringValue @@ -580,7 +580,7 @@ func (pipeline *AuthPipeline) loggingFields() map[string]string { pipeline.Logger.Error(err, "failed to parse CEL expression", "expression", exprStr) continue } - value, err := expr.ResolveFor(pipeline.GetAuthorizationJSON()) + value, err := expr.ResolveFor(log.RedactedAuthorizationJSON(pipeline.GetAuthorizationJSON())) if err != nil { pipeline.Logger.Error(err, "failed to evaluate CEL expression", "expression", exprStr) continue From f63a762ccc646fc7eb61e37e8d695b24f344f15a Mon Sep 17 00:00:00 2001 From: Andres Llausas Date: Tue, 11 Aug 2026 15:35:03 -0400 Subject: [PATCH 3/5] fix(service): revert auth JSON redaction in loggingFields CEL resolution The logging fields feature is PII opt-in by design (CONNLINK-1384): operators explicitly declare which fields to log via TelemetryPolicy. Using RedactedAuthorizationJSON would resolve opted-in fields to [REDACTED], defeating the feature's purpose. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Andres Llausas --- pkg/service/auth_pipeline.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/auth_pipeline.go b/pkg/service/auth_pipeline.go index ca2ce5bd..5d227837 100644 --- a/pkg/service/auth_pipeline.go +++ b/pkg/service/auth_pipeline.go @@ -580,7 +580,7 @@ func (pipeline *AuthPipeline) loggingFields() map[string]string { pipeline.Logger.Error(err, "failed to parse CEL expression", "expression", exprStr) continue } - value, err := expr.ResolveFor(log.RedactedAuthorizationJSON(pipeline.GetAuthorizationJSON())) + value, err := expr.ResolveFor(pipeline.GetAuthorizationJSON()) if err != nil { pipeline.Logger.Error(err, "failed to evaluate CEL expression", "expression", exprStr) continue From 56ba5bb3f0f6661b0e1e131e94a7c023d52744d8 Mon Sep 17 00:00:00 2001 From: Andres Llausas Date: Tue, 25 Aug 2026 14:42:49 -0400 Subject: [PATCH 4/5] feat(service): gate logging fields behind --enable-logging-fields flag Add instance-level opt-in flag (default false) so cluster admins control whether filter metadata logging fields are resolved and emitted. Addresses multi-tenant concern raised in review. Flag: --enable-logging-fields / ENABLE_LOGGING_FIELDS Co-Authored-By: Claude Opus 4.6 Signed-off-by: Andres Llausas --- main.go | 6 ++++-- pkg/service/auth.go | 11 +++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 6169dd1d..b274dc92 100644 --- a/main.go +++ b/main.go @@ -133,6 +133,7 @@ type authServerOptions struct { maxHttpRequestBodySize int64 kubeClientQPS float32 kubeClientBurst int + enableLoggingFields bool addSensitiveFields []string removeSensitiveFields []string addSensitiveHeaders []string @@ -201,6 +202,7 @@ func authServerCmd(opts *authServerOptions) *cobra.Command { cmd.PersistentFlags().StringVar(&opts.oidcTLSMaxVersion, "oidc-tls-max-version", utils.EnvVar("OIDC_TLS_MAX_VERSION", ""), "Maximum TLS version for the OIDC Discovery server (1.0, 1.1, 1.2, 1.3)") cmd.PersistentFlags().StringSliceVar(&opts.oidcTLSCipherSuites, "oidc-tls-cipher-suites", utils.EnvVarStringSlice("OIDC_TLS_CIPHER_SUITES", ","), "Comma-separated list of TLS cipher suites (IANA names) for the OIDC Discovery server") cmd.PersistentFlags().IntVar(&opts.evaluatorCacheSize, "evaluator-cache-size", utils.EnvVar("EVALUATOR_CACHE_SIZE", 1), "Cache size of each Authorino evaluator if enabled in the AuthConfig - in megabytes") + cmd.PersistentFlags().BoolVar(&opts.enableLoggingFields, "enable-logging-fields", utils.EnvVar("ENABLE_LOGGING_FIELDS", false), "Enable configurable logging fields from filter metadata in authorization decision logs") cmd.PersistentFlags().BoolVar(&opts.deepMetricsEnabled, "deep-metrics-enabled", utils.EnvVar("DEEP_METRICS_ENABLED", false), "Enable deep metrics at the level of each evaluator when requested in the AuthConfig, exported by the metrics server") cmd.PersistentFlags().IntVar(&opts.webhookServicePort, "webhook-service-port", 9443, "Port number of the webhook server") cmd.PersistentFlags().BoolVar(&opts.enableLeaderElection, "enable-leader-election", false, "Enable leader election for status updater - ensures only one instance of Authorino tries to update the status of reconciled resources") @@ -529,7 +531,7 @@ func startExtAuthServerGRPC(authConfigIndex index.Index, opts authServerOptions) grpcServer := grpc.NewServer(grpcServerOpts...) reflection.Register(grpcServer) - envoy_auth.RegisterAuthorizationServer(grpcServer, &service.AuthService{Index: authConfigIndex, Timeout: timeoutMs(opts.timeout)}) + envoy_auth.RegisterAuthorizationServer(grpcServer, &service.AuthService{Index: authConfigIndex, Timeout: timeoutMs(opts.timeout), EnableLoggingFields: opts.enableLoggingFields}) healthpb.RegisterHealthServer(grpcServer, &service.HealthService{}) grpc_prometheus.Register(grpcServer) grpc_prometheus.EnableHandlingTimeHistogram() @@ -545,7 +547,7 @@ func startExtAuthServerGRPC(authConfigIndex index.Index, opts authServerOptions) } func startExtAuthServerHTTP(authConfigIndex index.Index, opts authServerOptions) { - startHTTPService("auth", opts.extAuthHTTPPort, service.HTTPAuthorizationBasePath, opts.tlsCertPath, opts.tlsCertKeyPath, opts.tlsMinVersion, opts.tlsMaxVersion, opts.tlsCipherSuites, service.NewAuthService(authConfigIndex, timeoutMs(opts.timeout), opts.maxHttpRequestBodySize)) + startHTTPService("auth", opts.extAuthHTTPPort, service.HTTPAuthorizationBasePath, opts.tlsCertPath, opts.tlsCertKeyPath, opts.tlsMinVersion, opts.tlsMaxVersion, opts.tlsCipherSuites, service.NewAuthService(authConfigIndex, timeoutMs(opts.timeout), opts.maxHttpRequestBodySize, opts.enableLoggingFields)) } func startOIDCServer(authConfigIndex index.Index, opts authServerOptions) { diff --git a/pkg/service/auth.go b/pkg/service/auth.go index 83c5af95..b59e4779 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -76,10 +76,11 @@ type AuthService struct { Index index.Index Timeout time.Duration MaxHttpRequestBodySize int64 + EnableLoggingFields bool } -func NewAuthService(index index.Index, timeout time.Duration, maxHttpRequestBodySize int64) *AuthService { - return &AuthService{Index: index, Timeout: timeout, MaxHttpRequestBodySize: maxHttpRequestBodySize} +func NewAuthService(index index.Index, timeout time.Duration, maxHttpRequestBodySize int64, enableLoggingFields bool) *AuthService { + return &AuthService{Index: index, Timeout: timeout, MaxHttpRequestBodySize: maxHttpRequestBodySize, EnableLoggingFields: enableLoggingFields} } // ServeHTTP invokes authorization check for a simple GET/POST HTTP authorization request @@ -309,8 +310,10 @@ func (a *AuthService) Check(parentContext gocontext.Context, req *envoy_auth.Che } var loggingFields map[string]string - if p, ok := pipeline.(*AuthPipeline); ok { - loggingFields = p.loggingFields() + if a.EnableLoggingFields { + if p, ok := pipeline.(*AuthPipeline); ok { + loggingFields = p.loggingFields() + } } a.logAuthResult(result, ctx, loggingFields) From e68456be54813b5dbd76707369e254b85904abdd Mon Sep 17 00:00:00 2001 From: Andres Llausas Date: Wed, 2 Sep 2026 08:48:08 -0400 Subject: [PATCH 5/5] feat(service): namespace logging fields and add configurable value truncation Prefix all custom logging field keys with "logging." to prevent collision with built-in structured log fields (authorized, response, object) that could allow log spoofing. Add --logging-fields-max-value-bytes flag (default 1024, env LOGGING_FIELDS_MAX_VALUE_BYTES) to cap resolved string and CEL values, preventing stdout backpressure from oversized values. Set to 0 to disable truncation. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Andres Llausas --- main.go | 6 ++++-- pkg/service/auth.go | 15 +++++++------ pkg/service/auth_pipeline.go | 20 ++++++++++++----- pkg/service/auth_pipeline_test.go | 36 +++++++++++++++---------------- 4 files changed, 45 insertions(+), 32 deletions(-) diff --git a/main.go b/main.go index b274dc92..6f17bc0f 100644 --- a/main.go +++ b/main.go @@ -134,6 +134,7 @@ type authServerOptions struct { kubeClientQPS float32 kubeClientBurst int enableLoggingFields bool + loggingFieldsMaxValueBytes int addSensitiveFields []string removeSensitiveFields []string addSensitiveHeaders []string @@ -203,6 +204,7 @@ func authServerCmd(opts *authServerOptions) *cobra.Command { cmd.PersistentFlags().StringSliceVar(&opts.oidcTLSCipherSuites, "oidc-tls-cipher-suites", utils.EnvVarStringSlice("OIDC_TLS_CIPHER_SUITES", ","), "Comma-separated list of TLS cipher suites (IANA names) for the OIDC Discovery server") cmd.PersistentFlags().IntVar(&opts.evaluatorCacheSize, "evaluator-cache-size", utils.EnvVar("EVALUATOR_CACHE_SIZE", 1), "Cache size of each Authorino evaluator if enabled in the AuthConfig - in megabytes") cmd.PersistentFlags().BoolVar(&opts.enableLoggingFields, "enable-logging-fields", utils.EnvVar("ENABLE_LOGGING_FIELDS", false), "Enable configurable logging fields from filter metadata in authorization decision logs") + cmd.PersistentFlags().IntVar(&opts.loggingFieldsMaxValueBytes, "logging-fields-max-value-bytes", utils.EnvVar("LOGGING_FIELDS_MAX_VALUE_BYTES", 1024), "Maximum length in bytes for logging field values; 0 disables truncation") cmd.PersistentFlags().BoolVar(&opts.deepMetricsEnabled, "deep-metrics-enabled", utils.EnvVar("DEEP_METRICS_ENABLED", false), "Enable deep metrics at the level of each evaluator when requested in the AuthConfig, exported by the metrics server") cmd.PersistentFlags().IntVar(&opts.webhookServicePort, "webhook-service-port", 9443, "Port number of the webhook server") cmd.PersistentFlags().BoolVar(&opts.enableLeaderElection, "enable-leader-election", false, "Enable leader election for status updater - ensures only one instance of Authorino tries to update the status of reconciled resources") @@ -531,7 +533,7 @@ func startExtAuthServerGRPC(authConfigIndex index.Index, opts authServerOptions) grpcServer := grpc.NewServer(grpcServerOpts...) reflection.Register(grpcServer) - envoy_auth.RegisterAuthorizationServer(grpcServer, &service.AuthService{Index: authConfigIndex, Timeout: timeoutMs(opts.timeout), EnableLoggingFields: opts.enableLoggingFields}) + envoy_auth.RegisterAuthorizationServer(grpcServer, &service.AuthService{Index: authConfigIndex, Timeout: timeoutMs(opts.timeout), EnableLoggingFields: opts.enableLoggingFields, LoggingFieldsMaxValueBytes: opts.loggingFieldsMaxValueBytes}) healthpb.RegisterHealthServer(grpcServer, &service.HealthService{}) grpc_prometheus.Register(grpcServer) grpc_prometheus.EnableHandlingTimeHistogram() @@ -547,7 +549,7 @@ func startExtAuthServerGRPC(authConfigIndex index.Index, opts authServerOptions) } func startExtAuthServerHTTP(authConfigIndex index.Index, opts authServerOptions) { - startHTTPService("auth", opts.extAuthHTTPPort, service.HTTPAuthorizationBasePath, opts.tlsCertPath, opts.tlsCertKeyPath, opts.tlsMinVersion, opts.tlsMaxVersion, opts.tlsCipherSuites, service.NewAuthService(authConfigIndex, timeoutMs(opts.timeout), opts.maxHttpRequestBodySize, opts.enableLoggingFields)) + startHTTPService("auth", opts.extAuthHTTPPort, service.HTTPAuthorizationBasePath, opts.tlsCertPath, opts.tlsCertKeyPath, opts.tlsMinVersion, opts.tlsMaxVersion, opts.tlsCipherSuites, service.NewAuthService(authConfigIndex, timeoutMs(opts.timeout), opts.maxHttpRequestBodySize, opts.enableLoggingFields, opts.loggingFieldsMaxValueBytes)) } func startOIDCServer(authConfigIndex index.Index, opts authServerOptions) { diff --git a/pkg/service/auth.go b/pkg/service/auth.go index b59e4779..8429ee18 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -73,14 +73,15 @@ func init() { // AuthService is the server API for the authorization service. type AuthService struct { - Index index.Index - Timeout time.Duration - MaxHttpRequestBodySize int64 - EnableLoggingFields bool + Index index.Index + Timeout time.Duration + MaxHttpRequestBodySize int64 + EnableLoggingFields bool + LoggingFieldsMaxValueBytes int } -func NewAuthService(index index.Index, timeout time.Duration, maxHttpRequestBodySize int64, enableLoggingFields bool) *AuthService { - return &AuthService{Index: index, Timeout: timeout, MaxHttpRequestBodySize: maxHttpRequestBodySize, EnableLoggingFields: enableLoggingFields} +func NewAuthService(index index.Index, timeout time.Duration, maxHttpRequestBodySize int64, enableLoggingFields bool, loggingFieldsMaxValueBytes int) *AuthService { + return &AuthService{Index: index, Timeout: timeout, MaxHttpRequestBodySize: maxHttpRequestBodySize, EnableLoggingFields: enableLoggingFields, LoggingFieldsMaxValueBytes: loggingFieldsMaxValueBytes} } // ServeHTTP invokes authorization check for a simple GET/POST HTTP authorization request @@ -312,7 +313,7 @@ func (a *AuthService) Check(parentContext gocontext.Context, req *envoy_auth.Che var loggingFields map[string]string if a.EnableLoggingFields { if p, ok := pipeline.(*AuthPipeline); ok { - loggingFields = p.loggingFields() + loggingFields = p.loggingFields(a.LoggingFieldsMaxValueBytes) } } a.logAuthResult(result, ctx, loggingFields) diff --git a/pkg/service/auth_pipeline.go b/pkg/service/auth_pipeline.go index 5d227837..913e7597 100644 --- a/pkg/service/auth_pipeline.go +++ b/pkg/service/auth_pipeline.go @@ -556,21 +556,31 @@ func (pipeline *AuthPipeline) metricLabels() map[string]string { return labels } -func (pipeline *AuthPipeline) loggingFields() map[string]string { +const loggingFieldPrefix = "logging." + +func truncateValue(s string, maxLen int) string { + if maxLen > 0 && len(s) > maxLen { + return s[:maxLen] + "...(truncated)" + } + return s +} + +func (pipeline *AuthPipeline) loggingFields(maxValueBytes int) map[string]string { fields := make(map[string]string) filteredMetadata := pipeline.GetRequest().GetAttributes().GetMetadataContext().GetFilterMetadata() if customFields, ok := filteredMetadata["io.kuadrant.logging.fields"]; ok { for k, v := range customFields.Fields { + key := loggingFieldPrefix + k switch kind := v.Kind.(type) { case *structpb.Value_StringValue: - fields[k] = kind.StringValue + fields[key] = truncateValue(kind.StringValue, maxValueBytes) case *structpb.Value_NumberValue: - fields[k] = fmt.Sprintf("%v", kind.NumberValue) + fields[key] = fmt.Sprintf("%v", kind.NumberValue) case *structpb.Value_BoolValue: - fields[k] = fmt.Sprintf("%v", kind.BoolValue) + fields[key] = fmt.Sprintf("%v", kind.BoolValue) case *structpb.Value_StructValue: if celExprField, ok := kind.StructValue.Fields["cel_expr"]; ok { @@ -585,7 +595,7 @@ func (pipeline *AuthPipeline) loggingFields() map[string]string { pipeline.Logger.Error(err, "failed to evaluate CEL expression", "expression", exprStr) continue } - fields[k] = fmt.Sprintf("%v", value) + fields[key] = truncateValue(fmt.Sprintf("%v", value), maxValueBytes) } } diff --git a/pkg/service/auth_pipeline_test.go b/pkg/service/auth_pipeline_test.go index 14e12fa5..3daa9773 100644 --- a/pkg/service/auth_pipeline_test.go +++ b/pkg/service/auth_pipeline_test.go @@ -741,17 +741,17 @@ func TestPipelineLoggingFields(t *testing.T) { &request, ) - fields := pipeline.loggingFields() + fields := pipeline.loggingFields(1024) assert.Equal(t, 5, len(fields)) - assert.Equal(t, "my-api", fields["client_identity"]) - assert.Equal(t, "POST", fields["request_method"]) - assert.Equal(t, "audit-v1", fields["static_field"]) - assert.Equal(t, "42", fields["numeric_field"]) - assert.Equal(t, "true", fields["bool_field"]) - assert.Equal(t, "", fields["unresolvable"]) - assert.Equal(t, "", fields["null_field"]) + assert.Equal(t, "my-api", fields["logging.client_identity"]) + assert.Equal(t, "POST", fields["logging.request_method"]) + assert.Equal(t, "audit-v1", fields["logging.static_field"]) + assert.Equal(t, "42", fields["logging.numeric_field"]) + assert.Equal(t, "true", fields["logging.bool_field"]) + assert.Equal(t, "", fields["logging.unresolvable"]) + assert.Equal(t, "", fields["logging.null_field"]) } func TestPipelineLoggingFieldsEmpty(t *testing.T) { @@ -763,7 +763,7 @@ func TestPipelineLoggingFieldsEmpty(t *testing.T) { &request, ) - fields := pipeline.loggingFields() + fields := pipeline.loggingFields(1024) assert.Equal(t, 0, len(fields)) } @@ -802,10 +802,10 @@ func TestLoggingFieldsResolvesIdentityOnAllow(t *testing.T) { result := pipeline.Evaluate() assert.Equal(t, result.Code, rpc.OK) - fields := pipeline.loggingFields() + fields := pipeline.loggingFields(1024) - assert.Equal(t, "true", fields["identity_anonymous"]) - assert.Equal(t, "POST", fields["req_method"]) + assert.Equal(t, "true", fields["logging.identity_anonymous"]) + assert.Equal(t, "POST", fields["logging.req_method"]) } func TestLoggingFieldsResolvesIdentityOnAuthzDeny(t *testing.T) { @@ -845,10 +845,10 @@ func TestLoggingFieldsResolvesIdentityOnAuthzDeny(t *testing.T) { result := pipeline.Evaluate() assert.Equal(t, result.Code, rpc.PERMISSION_DENIED) - fields := pipeline.loggingFields() + fields := pipeline.loggingFields(1024) - assert.Equal(t, "true", fields["identity_anonymous"]) - assert.Equal(t, "POST", fields["req_method"]) + assert.Equal(t, "true", fields["logging.identity_anonymous"]) + assert.Equal(t, "POST", fields["logging.req_method"]) } func TestLoggingFieldsGracefulOnAuthnFailure(t *testing.T) { @@ -884,9 +884,9 @@ func TestLoggingFieldsGracefulOnAuthnFailure(t *testing.T) { result := pipeline.Evaluate() assert.Equal(t, result.Code, rpc.UNAUTHENTICATED) - fields := pipeline.loggingFields() + fields := pipeline.loggingFields(1024) - _, hasIdentity := fields["identity_anonymous"] + _, hasIdentity := fields["logging.identity_anonymous"] assert.Check(t, !hasIdentity, "identity field should not resolve when authentication fails") - assert.Equal(t, "POST", fields["req_method"]) + assert.Equal(t, "POST", fields["logging.req_method"]) }