-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
authz: End2End test for AuditLogger #6304
Changes from 14 commits
5778913
cebf932
d405ab2
0baf0e6
b569c67
135565a
c19e304
1e7fcc4
e5991f2
620d990
cba398c
de59ce5
9a8ee49
36bc552
b255385
7777c5b
191fdf6
80d38b4
46fda00
a02960d
56eba6e
cb01a30
4f30f15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,335 @@ | ||
/* | ||
* | ||
* Copyright 2023 gRPC 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 authz_test | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"encoding/json" | ||
"io" | ||
"net" | ||
"os" | ||
"strconv" | ||
"testing" | ||
"time" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/authz" | ||
"google.golang.org/grpc/authz/audit" | ||
"google.golang.org/grpc/credentials" | ||
"google.golang.org/grpc/internal/grpctest" | ||
testgrpc "google.golang.org/grpc/interop/grpc_testing" | ||
testpb "google.golang.org/grpc/interop/grpc_testing" | ||
"google.golang.org/grpc/testdata" | ||
|
||
_ "google.golang.org/grpc/authz/audit/stdout" | ||
) | ||
|
||
func TestAudit(t *testing.T) { | ||
grpctest.RunSubTests(t, s{}) | ||
} | ||
|
||
type statAuditLogger struct { | ||
authzDecisionStat map[bool]int // Map to hold the counts of authorization decisions | ||
lastEventContent map[string]string // Map to hold event fields in key:value fashion | ||
} | ||
|
||
func (s *statAuditLogger) Log(event *audit.Event) { | ||
s.authzDecisionStat[event.Authorized]++ | ||
s.lastEventContent["rpc_method"] = event.FullMethodName | ||
s.lastEventContent["principal"] = event.Principal | ||
s.lastEventContent["policy_name"] = event.PolicyName | ||
s.lastEventContent["matched_rule"] = event.MatchedRule | ||
s.lastEventContent["authorized"] = strconv.FormatBool(event.Authorized) | ||
} | ||
|
||
type loggerBuilder struct { | ||
authzDecisionStat map[bool]int | ||
lastEventContent map[string]string | ||
} | ||
|
||
func (loggerBuilder) Name() string { | ||
return "stat_logger" | ||
} | ||
|
||
func (lb *loggerBuilder) Build(audit.LoggerConfig) audit.Logger { | ||
return &statAuditLogger{ | ||
authzDecisionStat: lb.authzDecisionStat, | ||
lastEventContent: lb.lastEventContent, | ||
} | ||
} | ||
|
||
func (*loggerBuilder) ParseLoggerConfig(config json.RawMessage) (audit.LoggerConfig, error) { | ||
return nil, nil | ||
} | ||
|
||
// TestAuditLogger examines audit logging invocations using four different authorization policies. | ||
// It covers scenarios including a disabled audit, auditing both 'allow' and 'deny' outcomes, | ||
// and separately auditing 'allow' and 'deny' outcomes. | ||
// Additionally, it checks if SPIFFE ID from a certificate is propagated correctly. | ||
func (s) TestAuditLogger(t *testing.T) { | ||
// Each test data entry contains an authz policy for a grpc server, | ||
// how many 'allow' and 'deny' outcomes we expect (each test case makes 2 unary calls and one client-streaming call), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please wrap all comments at 80 columns (every file and always). Thanks! |
||
// and a structure to check if the audit.Event fields are properly populated. | ||
tests := []struct { | ||
name string | ||
authzPolicy string | ||
wantAuthzOutcomes map[bool]int | ||
eventContent map[string]string | ||
}{ | ||
{ | ||
name: "No audit", | ||
authzPolicy: `{ | ||
"name": "authz", | ||
"allow_rules": [ | ||
{ | ||
"name": "allow_UnaryCall", | ||
"request": | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missed one |
||
"paths": [ | ||
"/grpc.testing.TestService/UnaryCall" | ||
] | ||
} | ||
} | ||
], | ||
"audit_logging_options": { | ||
"audit_condition": "NONE", | ||
"audit_loggers": [ | ||
{ | ||
"name": "stat_logger", | ||
"config": {}, | ||
"is_optional": false | ||
} | ||
] | ||
} | ||
}`, | ||
wantAuthzOutcomes: map[bool]int{true: 0, false: 0}, | ||
}, | ||
{ | ||
name: "Allow All Deny Streaming - Audit All", | ||
authzPolicy: `{ | ||
"name": "authz", | ||
"allow_rules": [ | ||
{ | ||
"name": "allow_all", | ||
"request": { | ||
"paths": [ | ||
"*" | ||
] | ||
} | ||
} | ||
], | ||
"deny_rules": [ | ||
{ | ||
"name": "deny_all", | ||
"request": { | ||
"paths": | ||
[ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missed one |
||
"/grpc.testing.TestService/StreamingInputCall" | ||
] | ||
} | ||
} | ||
], | ||
"audit_logging_options": { | ||
"audit_condition": "ON_DENY_AND_ALLOW", | ||
"audit_loggers": [ | ||
{ | ||
"name": "stat_logger", | ||
"config": {}, | ||
"is_optional": false | ||
}, | ||
{ | ||
"name": "stdout_logger", | ||
"is_optional": false | ||
} | ||
] | ||
} | ||
}`, | ||
wantAuthzOutcomes: map[bool]int{true: 2, false: 1}, | ||
eventContent: map[string]string{ | ||
"rpc_method": "/grpc.testing.TestService/StreamingInputCall", | ||
"principal": "spiffe://foo.bar.com/client/workload/1", | ||
"policy_name": "authz", | ||
"matched_rule": "authz_deny_all", | ||
"authorized": "false", | ||
}, | ||
}, | ||
{ | ||
name: "Allow Unary - Audit Allow", | ||
authzPolicy: `{ | ||
"name": "authz", | ||
"allow_rules": [ | ||
{ | ||
"name": "allow_UnaryCall", | ||
"request": | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missed one |
||
"paths": [ | ||
"/grpc.testing.TestService/UnaryCall" | ||
] | ||
} | ||
} | ||
], | ||
"audit_logging_options": { | ||
"audit_condition": "ON_ALLOW", | ||
"audit_loggers": [ | ||
{ | ||
"name": "stat_logger", | ||
"config": {}, | ||
"is_optional": false | ||
} | ||
] | ||
} | ||
}`, | ||
wantAuthzOutcomes: map[bool]int{true: 2, false: 0}, | ||
}, | ||
{ | ||
name: "Allow Typo - Audit Deny", | ||
authzPolicy: `{ | ||
"name": "authz", | ||
"allow_rules": [ | ||
{ | ||
"name": "allow_UnaryCall", | ||
"request": | ||
{ | ||
"paths": [ | ||
"/grpc.testing.TestService/UnaryCall_Z" | ||
] | ||
} | ||
} | ||
], | ||
"audit_logging_options": { | ||
"audit_condition": "ON_DENY", | ||
"audit_loggers": [ | ||
{ | ||
"name": "stat_logger", | ||
"config": {}, | ||
"is_optional": false | ||
} | ||
] | ||
} | ||
}`, | ||
wantAuthzOutcomes: map[bool]int{true: 0, false: 3}, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
// Setup test statAuditLogger, gRPC test server with authzPolicy, unary and stream interceptors. | ||
lb := &loggerBuilder{ | ||
authzDecisionStat: map[bool]int{true: 0, false: 0}, | ||
lastEventContent: make(map[string]string), | ||
} | ||
audit.RegisterLoggerBuilder(lb) | ||
i, _ := authz.NewStatic(test.authzPolicy) | ||
|
||
s := grpc.NewServer( | ||
grpc.Creds(loadServerCreds(t)), | ||
grpc.ChainUnaryInterceptor(i.UnaryInterceptor), | ||
grpc.ChainStreamInterceptor(i.StreamInterceptor)) | ||
defer s.Stop() | ||
testgrpc.RegisterTestServiceServer(s, &testServer{}) | ||
lis, err := net.Listen("tcp", "localhost:0") | ||
if err != nil { | ||
t.Fatalf("error listening: %v", err) | ||
} | ||
go s.Serve(lis) | ||
|
||
// Setup gRPC test client with certificates containing a SPIFFE Id. | ||
clientConn, err := grpc.Dial(lis.Addr().String(), grpc.WithTransportCredentials(loadClientCreds(t))) | ||
if err != nil { | ||
t.Fatalf("grpc.Dial(%v) failed: %v", lis.Addr().String(), err) | ||
} | ||
defer clientConn.Close() | ||
client := testgrpc.NewTestServiceClient(clientConn) | ||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
defer cancel() | ||
|
||
// Make 2 unary calls and 1 streaming call. | ||
client.UnaryCall(ctx, &testpb.SimpleRequest{}) | ||
client.UnaryCall(ctx, &testpb.SimpleRequest{}) | ||
stream, err := client.StreamingInputCall(ctx) | ||
if err != nil { | ||
t.Fatalf("failed StreamingInputCall err: %v", err) | ||
} | ||
req := &testpb.StreamingInputCallRequest{ | ||
Payload: &testpb.Payload{ | ||
Body: []byte("hi"), | ||
}, | ||
} | ||
if err := stream.Send(req); err != nil && err != io.EOF { | ||
t.Fatalf("failed stream.Send err: %v", err) | ||
} | ||
stream.CloseAndRecv() | ||
|
||
// Compare expected number of allows/denies with content of internal map of statAuditLogger. | ||
if diff := cmp.Diff(lb.authzDecisionStat, test.wantAuthzOutcomes); diff != "" { | ||
t.Fatalf("Authorization decisions do not match\ndiff (-got +want):\n%s", diff) | ||
} | ||
// Compare event fields with expected values from authz policy. | ||
if test.eventContent != nil { | ||
if diff := cmp.Diff(lb.lastEventContent, test.eventContent); diff != "" { | ||
t.Fatalf("Unexpected message\ndiff (-got +want):\n%s", diff) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func loadServerCreds(t *testing.T) credentials.TransportCredentials { | ||
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem")) | ||
if err != nil { | ||
t.Fatalf("tls.LoadX509KeyPair(x509/server1_cert.pem, x509/server1_key.pem) failed: %v", err) | ||
} | ||
ca, err := os.ReadFile(testdata.Path("x509/client_ca_cert.pem")) | ||
if err != nil { | ||
t.Fatalf("os.ReadFile(x509/client_ca_cert.pem) failed: %v", err) | ||
} | ||
certPool := x509.NewCertPool() | ||
if !certPool.AppendCertsFromPEM(ca) { | ||
t.Fatal("failed to append certificates") | ||
} | ||
return credentials.NewTLS(&tls.Config{ | ||
ClientAuth: tls.RequireAndVerifyClientCert, | ||
Certificates: []tls.Certificate{cert}, | ||
ClientCAs: certPool, | ||
}) | ||
} | ||
|
||
func loadClientCreds(t *testing.T) credentials.TransportCredentials { | ||
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/client_with_spiffe_cert.pem"), testdata.Path("x509/client_with_spiffe_key.pem")) | ||
if err != nil { | ||
t.Fatalf("tls.LoadX509KeyPair(x509/client1_cert.pem, x509/client1_key.pem) failed: %v", err) | ||
} | ||
ca, err := os.ReadFile(testdata.Path("x509/server_ca_cert.pem")) | ||
if err != nil { | ||
t.Fatalf("os.ReadFile(x509/server_ca_cert.pem) failed: %v", err) | ||
} | ||
roots := x509.NewCertPool() | ||
if !roots.AppendCertsFromPEM(ca) { | ||
t.Fatal("failed to append certificates") | ||
} | ||
return credentials.NewTLS(&tls.Config{ | ||
Certificates: []tls.Certificate{cert}, | ||
RootCAs: roots, | ||
ServerName: "x.test.example.com", | ||
}) | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
-----BEGIN CERTIFICATE----- | ||
MIIFxzCCA6+gAwIBAgICA+gwDQYJKoZIhvcNAQELBQAwUDELMAkGA1UEBhMCVVMx | ||
CzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFzAVBgNV | ||
BAMMDnRlc3QtY2xpZW50X2NhMB4XDTIzMDUyMjA1MDA1NVoXDTMzMDUxOTA1MDA1 | ||
NVowTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL | ||
BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTCCAiIwDQYJKoZIhvcN | ||
AQEBBQADggIPADCCAgoCggIBANXyLXGYzQFwLGwjzkeuo/y41voDH1Y9J+ee4qJU | ||
OFuMKKXx5ai7n7dik4//J12OqJbbr416cFkKmcojwwbAdncXMV58EF82Bt8QRov0 | ||
Vtoio/wxlyRlxDlVYwr56W+0EVP9Q+kzA/dTnMgOQYIeSix96CUQRy8XDu1YX3rk | ||
fiUkND9xxuQw8OXi3LXguv/lilLVC/lXiXwa0RWEgMZZU2S1/lAElAG3aZuuWULG | ||
K+PpKPuqkcptbUPCvNN1eUs9/D82aoFuqRCmpTC+7bUO+SJSggpUHcgTbXT9i6OO | ||
9eR0ijcaQjtb0Y6ro+Cv60YOnlGC8It3KoY2SxioyqdceRUohqs4T4hjBEckzz11 | ||
AC0Pj0Gp4NJPcOY68EjhD5rvncn76RRr3z2XZpd+2Nz+Fldxk/aaejfdgqs9lo1g | ||
C+aP+nk9oqSpFAc+rpHsblLZehUur/FHhenn1pYWqkSJsAG0sFW4sDHATRIfva3c | ||
HNHB5kBzruGymywBGO0xOw7+s5XzPiNnbXT5FBY1rKG7RwlqdtDh6LWJRHmEblWV | ||
tPHNiY+rrStv0rN7Hk/YKcSXd5JiTjk3GXjO1YJJVEraEWHlxzdGy+xu/m0iJLed | ||
pxZwuxxdZ/Q2+Ht+X9pO2DsW8BQFbddCwbooxKygwSlmHCN1gRSWqWMZY5nzsxxY | ||
tic9AgMBAAGjgawwgakwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUyiXne0d3g9zN | ||
gjhCdl2d9ykxIfgwDgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUF | ||
BwMCMDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29tL2NsaWVudC93b3Jr | ||
bG9hZC8xMB8GA1UdIwQYMBaAFOr3a0MblN9W9Opu7VsDn3crpoDCMA0GCSqGSIb3 | ||
DQEBCwUAA4ICAQB3pt3mLXDDcReko9eTFahkNyU2zGP7CSi1RcgfP1aJDLBTjePb | ||
JUhoY14tSpOGSliXWscXbNveW+Yk+tB411r8SJuXIkaYZM2BJQDWFzL7aLfAQSx5 | ||
rf8tHVyKO89uBoQtgEnxZp9BFhBp/b2y5DLrZWjM6W9s21C9S9UIFjD8UwrKicNH | ||
HGxIC6AZ6yc0x2Nsq/KW1IZ6HDueZRB4tud75wwkPVpS5fb+XqIJEBP7lgYrJjGp | ||
aLLxV2vn1kX2/qbH31hhWVpNyPkpFsT+IbkPFLDyQoZKHbewD6M56+KBRTTENETQ | ||
hFLgJB0HiICJ2I6cqw1UbDJMJFkcnThsuI8Wg9dxZ+OffYeZ5bnFCVIg0WUi9oMK | ||
JDXZAqYDwBaQHyNszaYzZ5VE2Gd/K8PEDevW4RblI+vAOamIM5w1DjQHWf7n1byt | ||
nGwnxt4IQ5vwlrdX3FDcEkhacHdcniX/FTpYrfOistPh+QpBAvA92DG1CbAf2nKY | ||
yXLx+Ho7tUEBGioU4XvRHccwumfatf5z+JO/EvIi2yWd1tanl5J3o/sSs9ixJfx4 | ||
aSuM+zAwf8EM+YGqYMCZ896+T6/r7NAg+YIDYu1K5b5QqYyPanqNqUf9VTR4oQ4v | ||
+jdb5PkujXbjENvkAhNbUyUbQJ+IU0KHm3/sdhRPN5tuc9C+BTSQvlmKkw== | ||
-----END CERTIFICATE----- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please just
Test
, which is our convention.