-
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
Merged
Merged
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
5778913
Draft of e2e test
erm-g cebf932
No Audit, Audit on Allow and Deny
erm-g d405ab2
Audit on Allow, Audit on Deny
erm-g 0baf0e6
fix typo
erm-g b569c67
SPIFFE related testing
erm-g 135565a
SPIFFE Id validation and certs creation script
erm-g c19e304
Address PR comments
erm-g 1e7fcc4
Wrap tests using grpctest.Tester
erm-g e5991f2
Address PR comments
erm-g 620d990
Change package name to authz_test to fit other end2end tests
erm-g cba398c
Add licence header, remove SPIFFE slice
erm-g de59ce5
Licence year change
erm-g 9a8ee49
Address PR comments part 1
erm-g 36bc552
Address PR comments part 2
erm-g b255385
Address PR comments part 3
erm-g 7777c5b
Address PR comments final part
erm-g 191fdf6
Drop newline for a brace
erm-g 80d38b4
Address PR comments, fix outdated function comment
erm-g 46fda00
Address PR comments
erm-g a02960d
Fix typo
erm-g 56eba6e
Remove unused var
erm-g cb01a30
Address PR comment, change most test error handling to Errorf
erm-g 4f30f15
Address PR comments
erm-g File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,352 @@ | ||
/* | ||
* | ||
* 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 audit_test | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"encoding/json" | ||
"io" | ||
"net" | ||
"os" | ||
"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" | ||
"google.golang.org/grpc/internal/stubserver" | ||
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" | ||
) | ||
|
||
type s struct { | ||
grpctest.Tester | ||
} | ||
|
||
func Test(t *testing.T) { | ||
grpctest.RunSubTests(t, s{}) | ||
} | ||
|
||
type statAuditLogger struct { | ||
authzDecisionStat map[bool]int // Map to hold the counts of authorization decisions | ||
lastEvent *audit.Event // Map to hold event fields in key:value fashion | ||
} | ||
|
||
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 | ||
} | ||
|
||
// 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), | ||
// and a structure to check if the audit.Event fields are properly populated. | ||
tests := []struct { | ||
name string | ||
authzPolicy string | ||
wantAuthzOutcomes map[bool]int | ||
eventContent *audit.Event | ||
}{ | ||
{ | ||
name: "No audit", | ||
authzPolicy: `{ | ||
"name": "authz", | ||
"allow_rules": [ | ||
{ | ||
"name": "allow_UnaryCall", | ||
"request": { | ||
"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": [ | ||
"/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: &audit.Event{ | ||
FullMethodName: "/grpc.testing.TestService/StreamingInputCall", | ||
Principal: "spiffe://foo.bar.com/client/workload/1", | ||
PolicyName: "authz", | ||
MatchedRule: "authz_deny_all", | ||
Authorized: false, | ||
}, | ||
}, | ||
{ | ||
name: "Allow Unary - Audit Allow", | ||
authzPolicy: `{ | ||
"name": "authz", | ||
"allow_rules": [ | ||
{ | ||
"name": "allow_UnaryCall", | ||
"request": { | ||
"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}, | ||
}, | ||
} | ||
|
||
serverCreds := loadServerCreds(t) | ||
clientCreds := loadClientCreds(t) | ||
|
||
ss := &stubserver.StubServer{ | ||
UnaryCallF: func(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) { | ||
return &testpb.SimpleResponse{}, nil | ||
}, | ||
FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error { | ||
_, err := stream.Recv() | ||
if err != io.EOF { | ||
return err | ||
} | ||
return nil | ||
}, | ||
} | ||
|
||
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}, | ||
lastEvent: &audit.Event{}, | ||
} | ||
audit.RegisterLoggerBuilder(lb) | ||
i, _ := authz.NewStatic(test.authzPolicy) | ||
|
||
s := grpc.NewServer( | ||
grpc.Creds(serverCreds), | ||
grpc.ChainUnaryInterceptor(i.UnaryInterceptor), | ||
grpc.ChainStreamInterceptor(i.StreamInterceptor)) | ||
defer s.Stop() | ||
testgrpc.RegisterTestServiceServer(s, ss) | ||
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(clientCreds)) | ||
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() | ||
|
||
client.UnaryCall(ctx, &testpb.SimpleRequest{}) | ||
client.UnaryCall(ctx, &testpb.SimpleRequest{}) | ||
stream, err := client.StreamingInputCall(ctx) | ||
req := &testpb.StreamingInputCallRequest{ | ||
Payload: &testpb.Payload{ | ||
Body: []byte("hi"), | ||
}, | ||
} | ||
stream.Send(req) | ||
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.lastEvent, test.eventContent); diff != "" { | ||
t.Fatalf("Unexpected message\ndiff (-got +want):\n%s", diff) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
// loadServerCreds constructs TLS containing server certs and CA | ||
func loadServerCreds(t *testing.T) credentials.TransportCredentials { | ||
t.Helper() | ||
cert := loadKeys(t, "x509/server1_cert.pem", "x509/server1_key.pem") | ||
certPool := loadCaCerts(t, "x509/client_ca_cert.pem") | ||
return credentials.NewTLS(&tls.Config{ | ||
ClientAuth: tls.RequireAndVerifyClientCert, | ||
Certificates: []tls.Certificate{cert}, | ||
ClientCAs: certPool, | ||
}) | ||
} | ||
|
||
// loadClientCreds constructs TLS containing client certs and CA | ||
func loadClientCreds(t *testing.T) credentials.TransportCredentials { | ||
t.Helper() | ||
cert := loadKeys(t, "x509/client_with_spiffe_cert.pem", "x509/client_with_spiffe_key.pem") | ||
roots := loadCaCerts(t, "x509/server_ca_cert.pem") | ||
return credentials.NewTLS(&tls.Config{ | ||
Certificates: []tls.Certificate{cert}, | ||
RootCAs: roots, | ||
ServerName: "x.test.example.com", | ||
}) | ||
|
||
} | ||
|
||
// loadCaCerts loads X509 key pair from the provided file paths. | ||
// It is used for loading both client and server certificates for the test | ||
func loadKeys(t *testing.T, certPath, key string) tls.Certificate { | ||
t.Helper() | ||
cert, err := tls.LoadX509KeyPair(testdata.Path(certPath), testdata.Path(key)) | ||
if err != nil { | ||
t.Fatalf("tls.LoadX509KeyPair(%q, %q) failed: %v", certPath, key, err) | ||
} | ||
return cert | ||
} | ||
|
||
// loadCaCerts loads CA certificates and constructs x509.CertPool | ||
// It is used for loading both client and server CAs for the test | ||
func loadCaCerts(t *testing.T, certPath string) *x509.CertPool { | ||
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. Nit: CA not Ca |
||
t.Helper() | ||
ca, err := os.ReadFile(testdata.Path(certPath)) | ||
if err != nil { | ||
t.Fatalf("os.ReadFile(%q) failed: %v", certPath, err) | ||
} | ||
roots := x509.NewCertPool() | ||
if !roots.AppendCertsFromPEM(ca) { | ||
t.Fatal("failed to append certificates") | ||
} | ||
return roots | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
-----BEGIN CERTIFICATE----- | ||
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 | ||
-----END CERTIFICATE----- |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
This is not a map anymore.