google.golang.org/grpc@v1.72.2/authz/audit/audit_logging_test.go (about) 1 /* 2 * 3 * Copyright 2023 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package audit_test 20 21 import ( 22 "context" 23 "crypto/tls" 24 "crypto/x509" 25 "encoding/json" 26 "io" 27 "os" 28 "testing" 29 "time" 30 31 "github.com/google/go-cmp/cmp" 32 "google.golang.org/grpc" 33 "google.golang.org/grpc/authz" 34 "google.golang.org/grpc/authz/audit" 35 "google.golang.org/grpc/codes" 36 "google.golang.org/grpc/credentials" 37 "google.golang.org/grpc/internal/grpctest" 38 "google.golang.org/grpc/internal/stubserver" 39 testgrpc "google.golang.org/grpc/interop/grpc_testing" 40 testpb "google.golang.org/grpc/interop/grpc_testing" 41 "google.golang.org/grpc/status" 42 "google.golang.org/grpc/testdata" 43 44 _ "google.golang.org/grpc/authz/audit/stdout" 45 ) 46 47 type s struct { 48 grpctest.Tester 49 } 50 51 func Test(t *testing.T) { 52 grpctest.RunSubTests(t, s{}) 53 } 54 55 type statAuditLogger struct { 56 authzDecisionStat map[bool]int // Map to hold the counts of authorization decisions 57 lastEvent *audit.Event // Field to store last received event 58 } 59 60 func (s *statAuditLogger) Log(event *audit.Event) { 61 s.authzDecisionStat[event.Authorized]++ 62 *s.lastEvent = *event 63 } 64 65 type loggerBuilder struct { 66 authzDecisionStat map[bool]int 67 lastEvent *audit.Event 68 } 69 70 func (loggerBuilder) Name() string { 71 return "stat_logger" 72 } 73 74 func (lb *loggerBuilder) Build(audit.LoggerConfig) audit.Logger { 75 return &statAuditLogger{ 76 authzDecisionStat: lb.authzDecisionStat, 77 lastEvent: lb.lastEvent, 78 } 79 } 80 81 func (*loggerBuilder) ParseLoggerConfig(config json.RawMessage) (audit.LoggerConfig, error) { 82 return nil, nil 83 } 84 85 // TestAuditLogger examines audit logging invocations using four different 86 // authorization policies. It covers scenarios including a disabled audit, 87 // auditing both 'allow' and 'deny' outcomes, and separately auditing 'allow' 88 // and 'deny' outcomes. Additionally, it checks if SPIFFE ID from a certificate 89 // is propagated correctly. 90 func (s) TestAuditLogger(t *testing.T) { 91 // Each test data entry contains an authz policy for a grpc server, 92 // how many 'allow' and 'deny' outcomes we expect (each test case makes 2 93 // unary calls and one client-streaming call), and a structure to check if 94 // the audit.Event fields are properly populated. Additionally, we specify 95 // directly which authz outcome we expect from each type of call. 96 tests := []struct { 97 name string 98 authzPolicy string 99 wantAuthzOutcomes map[bool]int 100 eventContent *audit.Event 101 wantUnaryCallCode codes.Code 102 wantStreamingCallCode codes.Code 103 }{ 104 { 105 name: "No audit", 106 authzPolicy: `{ 107 "name": "authz", 108 "allow_rules": [ 109 { 110 "name": "allow_UnaryCall", 111 "request": { 112 "paths": [ 113 "/grpc.testing.TestService/UnaryCall" 114 ] 115 } 116 } 117 ], 118 "audit_logging_options": { 119 "audit_condition": "NONE", 120 "audit_loggers": [ 121 { 122 "name": "stat_logger", 123 "config": {}, 124 "is_optional": false 125 } 126 ] 127 } 128 }`, 129 wantAuthzOutcomes: map[bool]int{true: 0, false: 0}, 130 wantUnaryCallCode: codes.OK, 131 wantStreamingCallCode: codes.PermissionDenied, 132 }, 133 { 134 name: "Allow All Deny Streaming - Audit All", 135 authzPolicy: `{ 136 "name": "authz", 137 "allow_rules": [ 138 { 139 "name": "allow_all", 140 "request": { 141 "paths": [ 142 "*" 143 ] 144 } 145 } 146 ], 147 "deny_rules": [ 148 { 149 "name": "deny_all", 150 "request": { 151 "paths": [ 152 "/grpc.testing.TestService/StreamingInputCall" 153 ] 154 } 155 } 156 ], 157 "audit_logging_options": { 158 "audit_condition": "ON_DENY_AND_ALLOW", 159 "audit_loggers": [ 160 { 161 "name": "stat_logger", 162 "config": {}, 163 "is_optional": false 164 }, 165 { 166 "name": "stdout_logger", 167 "is_optional": false 168 } 169 ] 170 } 171 }`, 172 wantAuthzOutcomes: map[bool]int{true: 2, false: 1}, 173 eventContent: &audit.Event{ 174 FullMethodName: "/grpc.testing.TestService/StreamingInputCall", 175 Principal: "spiffe://foo.bar.com/client/workload/1", 176 PolicyName: "authz", 177 MatchedRule: "authz_deny_all", 178 Authorized: false, 179 }, 180 wantUnaryCallCode: codes.OK, 181 wantStreamingCallCode: codes.PermissionDenied, 182 }, 183 { 184 name: "Allow Unary - Audit Allow", 185 authzPolicy: `{ 186 "name": "authz", 187 "allow_rules": [ 188 { 189 "name": "allow_UnaryCall", 190 "request": { 191 "paths": [ 192 "/grpc.testing.TestService/UnaryCall" 193 ] 194 } 195 } 196 ], 197 "audit_logging_options": { 198 "audit_condition": "ON_ALLOW", 199 "audit_loggers": [ 200 { 201 "name": "stat_logger", 202 "config": {}, 203 "is_optional": false 204 } 205 ] 206 } 207 }`, 208 wantAuthzOutcomes: map[bool]int{true: 2, false: 0}, 209 wantUnaryCallCode: codes.OK, 210 wantStreamingCallCode: codes.PermissionDenied, 211 }, 212 { 213 name: "Allow Typo - Audit Deny", 214 authzPolicy: `{ 215 "name": "authz", 216 "allow_rules": [ 217 { 218 "name": "allow_UnaryCall", 219 "request": { 220 "paths": [ 221 "/grpc.testing.TestService/UnaryCall_Z" 222 ] 223 } 224 } 225 ], 226 "audit_logging_options": { 227 "audit_condition": "ON_DENY", 228 "audit_loggers": [ 229 { 230 "name": "stat_logger", 231 "config": {}, 232 "is_optional": false 233 } 234 ] 235 } 236 }`, 237 wantAuthzOutcomes: map[bool]int{true: 0, false: 3}, 238 wantUnaryCallCode: codes.PermissionDenied, 239 wantStreamingCallCode: codes.PermissionDenied, 240 }, 241 } 242 243 for _, test := range tests { 244 t.Run(test.name, func(t *testing.T) { 245 // Construct the credentials for the tests and the stub server 246 serverCreds := loadServerCreds(t) 247 clientCreds := loadClientCreds(t) 248 ss := &stubserver.StubServer{ 249 UnaryCallF: func(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) { 250 return &testpb.SimpleResponse{}, nil 251 }, 252 FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error { 253 _, err := stream.Recv() 254 if err != io.EOF { 255 return err 256 } 257 return nil 258 }, 259 } 260 // Setup test statAuditLogger, gRPC test server with authzPolicy, unary 261 // and stream interceptors. 262 lb := &loggerBuilder{ 263 authzDecisionStat: map[bool]int{true: 0, false: 0}, 264 lastEvent: &audit.Event{}, 265 } 266 audit.RegisterLoggerBuilder(lb) 267 i, _ := authz.NewStatic(test.authzPolicy) 268 269 s := grpc.NewServer(grpc.Creds(serverCreds), grpc.ChainUnaryInterceptor(i.UnaryInterceptor), grpc.ChainStreamInterceptor(i.StreamInterceptor)) 270 defer s.Stop() 271 ss.S = s 272 stubserver.StartTestService(t, ss) 273 274 // Setup gRPC test client with certificates containing a SPIFFE Id. 275 cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds)) 276 if err != nil { 277 t.Fatalf("grpc.NewClient(%v) failed: %v", ss.Address, err) 278 } 279 defer cc.Close() 280 client := testgrpc.NewTestServiceClient(cc) 281 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 282 defer cancel() 283 284 if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != test.wantUnaryCallCode { 285 t.Errorf("Unexpected UnaryCall fail: got %v want %v", err, test.wantUnaryCallCode) 286 } 287 if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != test.wantUnaryCallCode { 288 t.Errorf("Unexpected UnaryCall fail: got %v want %v", err, test.wantUnaryCallCode) 289 } 290 stream, err := client.StreamingInputCall(ctx) 291 if err != nil { 292 t.Fatalf("StreamingInputCall failed: %v", err) 293 } 294 req := &testpb.StreamingInputCallRequest{ 295 Payload: &testpb.Payload{ 296 Body: []byte("hi"), 297 }, 298 } 299 if err := stream.Send(req); err != nil && err != io.EOF { 300 t.Fatalf("stream.Send failed: %v", err) 301 } 302 if _, err := stream.CloseAndRecv(); status.Code(err) != test.wantStreamingCallCode { 303 t.Errorf("Unexpected stream.CloseAndRecv fail: got %v want %v", err, test.wantStreamingCallCode) 304 } 305 306 // Compare expected number of allows/denies with content of the internal 307 // map of statAuditLogger. 308 if diff := cmp.Diff(lb.authzDecisionStat, test.wantAuthzOutcomes); diff != "" { 309 t.Errorf("Authorization decisions do not match\ndiff (-got +want):\n%s", diff) 310 } 311 // Compare last event received by statAuditLogger with expected event. 312 if test.eventContent != nil { 313 if diff := cmp.Diff(lb.lastEvent, test.eventContent); diff != "" { 314 t.Errorf("Unexpected message\ndiff (-got +want):\n%s", diff) 315 } 316 } 317 }) 318 } 319 } 320 321 // loadServerCreds constructs TLS containing server certs and CA 322 func loadServerCreds(t *testing.T) credentials.TransportCredentials { 323 t.Helper() 324 cert := loadKeys(t, "x509/server1_cert.pem", "x509/server1_key.pem") 325 certPool := loadCACerts(t, "x509/client_ca_cert.pem") 326 return credentials.NewTLS(&tls.Config{ 327 ClientAuth: tls.RequireAndVerifyClientCert, 328 Certificates: []tls.Certificate{cert}, 329 ClientCAs: certPool, 330 }) 331 } 332 333 // loadClientCreds constructs TLS containing client certs and CA 334 func loadClientCreds(t *testing.T) credentials.TransportCredentials { 335 t.Helper() 336 cert := loadKeys(t, "x509/client_with_spiffe_cert.pem", "x509/client_with_spiffe_key.pem") 337 roots := loadCACerts(t, "x509/server_ca_cert.pem") 338 return credentials.NewTLS(&tls.Config{ 339 Certificates: []tls.Certificate{cert}, 340 RootCAs: roots, 341 ServerName: "x.test.example.com", 342 }) 343 344 } 345 346 // loadKeys loads X509 key pair from the provided file paths. 347 // It is used for loading both client and server certificates for the test 348 func loadKeys(t *testing.T, certPath, key string) tls.Certificate { 349 t.Helper() 350 cert, err := tls.LoadX509KeyPair(testdata.Path(certPath), testdata.Path(key)) 351 if err != nil { 352 t.Fatalf("tls.LoadX509KeyPair(%q, %q) failed: %v", certPath, key, err) 353 } 354 return cert 355 } 356 357 // loadCACerts loads CA certificates and constructs x509.CertPool 358 // It is used for loading both client and server CAs for the test 359 func loadCACerts(t *testing.T, certPath string) *x509.CertPool { 360 t.Helper() 361 ca, err := os.ReadFile(testdata.Path(certPath)) 362 if err != nil { 363 t.Fatalf("os.ReadFile(%q) failed: %v", certPath, err) 364 } 365 roots := x509.NewCertPool() 366 if !roots.AppendCertsFromPEM(ca) { 367 t.Fatal("Failed to append certificates") 368 } 369 return roots 370 }