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  }