From 67e881c3583ede465f8786e1c293e95faf00e16c Mon Sep 17 00:00:00 2001 From: Gregory Cooke Date: Thu, 29 Jun 2023 15:45:33 -0400 Subject: [PATCH] xds: E2E Test for Audit Logging (#6377) Add E2E Test for Audit Logging through the XDS path --- test/xds/xds_server_rbac_test.go | 96 ++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/test/xds/xds_server_rbac_test.go b/test/xds/xds_server_rbac_test.go index 346dfe816c18..914b59db31c6 100644 --- a/test/xds/xds_server_rbac_test.go +++ b/test/xds/xds_server_rbac_test.go @@ -20,14 +20,18 @@ package xds_test import ( "context" + "encoding/json" "fmt" "net" "strconv" "strings" "testing" + v3xdsxdstypepb "github.com/cncf/xds/go/xds/type/v3" v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" + "github.com/google/go-cmp/cmp" "google.golang.org/grpc" + "google.golang.org/grpc/authz/audit" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal" @@ -36,6 +40,7 @@ import ( "google.golang.org/grpc/internal/testutils/xds/e2e" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/structpb" v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" @@ -415,6 +420,8 @@ func (s) TestRBACHTTPFilter(t *testing.T) { rbacCfg *rpb.RBAC wantStatusEmptyCall codes.Code wantStatusUnaryCall codes.Code + wantAuthzOutcomes map[bool]int + eventContent *audit.Event }{ // This test tests an RBAC HTTP Filter which is configured to allow any RPC. // Any RPC passing through this RBAC HTTP Filter should proceed as normal. @@ -433,10 +440,30 @@ func (s) TestRBACHTTPFilter(t *testing.T) { }, }, }, + AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{ + AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW, + LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{ + { + AuditLogger: &v3corepb.TypedExtensionConfig{ + Name: "stat_logger", + TypedConfig: createXDSTypedStruct(t, map[string]interface{}{}, "stat_logger"), + }, + IsOptional: false, + }, + }, + }, }, }, wantStatusEmptyCall: codes.OK, wantStatusUnaryCall: codes.OK, + wantAuthzOutcomes: map[bool]int{true: 2, false: 0}, + // TODO(gtcooke94) add policy name (RBAC filter name) once + // https://github.com/grpc/grpc-go/pull/6327 is merged. + eventContent: &audit.Event{ + FullMethodName: "/grpc.testing.TestService/UnaryCall", + MatchedRule: "anyone", + Authorized: true, + }, }, // This test tests an RBAC HTTP Filter which is configured to allow only // RPC's with certain paths ("UnaryCall"). Only unary calls passing @@ -605,6 +632,12 @@ func (s) TestRBACHTTPFilter(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { func() { + lb := &loggerBuilder{ + authzDecisionStat: map[bool]int{true: 0, false: 0}, + lastEvent: &audit.Event{}, + } + audit.RegisterLoggerBuilder(lb) + managementServer, nodeID, bootstrapContents, resolver, cleanup1 := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{}) defer cleanup1() @@ -660,6 +693,17 @@ func (s) TestRBACHTTPFilter(t *testing.T) { } // Toggle RBAC back on for next iterations. envconfig.XDSRBAC = true + + if test.wantAuthzOutcomes != nil { + if diff := cmp.Diff(lb.authzDecisionStat, test.wantAuthzOutcomes); diff != "" { + t.Fatalf("authorization decision do not match\ndiff (-got +want):\n%s", diff) + } + } + if test.eventContent != nil { + if diff := cmp.Diff(lb.lastEvent, test.eventContent); diff != "" { + t.Fatalf("unexpected event\ndiff (-got +want):\n%s", diff) + } + } }() }) } @@ -895,3 +939,55 @@ func (s) TestRBACToggledOff_WithBadRouteConfiguration(t *testing.T) { t.Fatalf("UnaryCall() returned err with status: %v, if RBAC is disabled all RPC's should proceed as normal", status.Code(err)) } } + +type statAuditLogger struct { + authzDecisionStat map[bool]int // Map to hold counts of authorization decisions + lastEvent *audit.Event // Field to store last received event +} + +func (s *statAuditLogger) Log(event *audit.Event) { + s.authzDecisionStat[event.Authorized]++ + *s.lastEvent = *event +} + +type loggerBuilder struct { + authzDecisionStat map[bool]int + lastEvent *audit.Event +} + +func (loggerBuilder) Name() string { + return "stat_logger" +} + +func (lb *loggerBuilder) Build(audit.LoggerConfig) audit.Logger { + return &statAuditLogger{ + authzDecisionStat: lb.authzDecisionStat, + lastEvent: lb.lastEvent, + } +} + +func (*loggerBuilder) ParseLoggerConfig(config json.RawMessage) (audit.LoggerConfig, error) { + return nil, nil +} + +// This is used when converting a custom config from raw JSON to a TypedStruct. +// The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/". +const typeURLPrefix = "grpc.authz.audit_logging/" + +// Builds custom configs for audit logger RBAC protos. +func createXDSTypedStruct(t *testing.T, in map[string]interface{}, name string) *anypb.Any { + t.Helper() + pb, err := structpb.NewStruct(in) + if err != nil { + t.Fatalf("createXDSTypedStruct failed during structpb.NewStruct: %v", err) + } + typedStruct := &v3xdsxdstypepb.TypedStruct{ + TypeUrl: typeURLPrefix + name, + Value: pb, + } + customConfig, err := anypb.New(typedStruct) + if err != nil { + t.Fatalf("createXDSTypedStruct failed during anypb.New: %v", err) + } + return customConfig +}