Skip to content
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 23 commits into from Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
164 changes: 91 additions & 73 deletions authz/audit_logging_test.go → authz/audit/audit_logging_test.go
Expand Up @@ -16,7 +16,7 @@
*
*/

package authz_test
package audit_test

import (
"context"
Expand All @@ -26,7 +26,6 @@ import (
"io"
"net"
"os"
"strconv"
"testing"
"time"

Expand All @@ -36,34 +35,35 @@ import (
"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"
)

func TestAudit(t *testing.T) {
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
lastEventContent map[string]string // Map to hold event fields in key:value fashion
authzDecisionStat map[bool]int // Map to hold the counts of authorization decisions
lastEvent *audit.Event // Map to hold event fields in key:value fashion
Copy link
Member

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.

}

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)
*s.lastEvent = *event
}

type loggerBuilder struct {
authzDecisionStat map[bool]int
lastEventContent map[string]string
lastEvent *audit.Event
}

func (loggerBuilder) Name() string {
Expand All @@ -73,27 +73,29 @@ func (loggerBuilder) Name() string {
func (lb *loggerBuilder) Build(audit.LoggerConfig) audit.Logger {
return &statAuditLogger{
authzDecisionStat: lb.authzDecisionStat,
lastEventContent: lb.lastEventContent,
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.
// 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.
// 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 map[string]string
eventContent *audit.Event
}{
{
name: "No audit",
Expand All @@ -102,8 +104,7 @@ func (s) TestAuditLogger(t *testing.T) {
"allow_rules": [
{
"name": "allow_UnaryCall",
"request":
{
"request": {
"paths": [
"/grpc.testing.TestService/UnaryCall"
]
Expand Down Expand Up @@ -141,8 +142,7 @@ func (s) TestAuditLogger(t *testing.T) {
{
"name": "deny_all",
"request": {
"paths":
[
"paths": [
"/grpc.testing.TestService/StreamingInputCall"
]
}
Expand All @@ -164,12 +164,12 @@ func (s) TestAuditLogger(t *testing.T) {
}
}`,
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",
eventContent: &audit.Event{
FullMethodName: "/grpc.testing.TestService/StreamingInputCall",
Principal: "spiffe://foo.bar.com/client/workload/1",
PolicyName: "authz",
MatchedRule: "authz_deny_all",
Authorized: false,
},
},
{
Expand All @@ -179,8 +179,7 @@ func (s) TestAuditLogger(t *testing.T) {
"allow_rules": [
{
"name": "allow_UnaryCall",
"request":
{
"request": {
"paths": [
"/grpc.testing.TestService/UnaryCall"
]
Expand All @@ -207,8 +206,7 @@ func (s) TestAuditLogger(t *testing.T) {
"allow_rules": [
{
"name": "allow_UnaryCall",
"request":
{
"request": {
"paths": [
"/grpc.testing.TestService/UnaryCall_Z"
]
Expand All @@ -229,31 +227,46 @@ func (s) TestAuditLogger(t *testing.T) {
wantAuthzOutcomes: map[bool]int{true: 0, false: 3},
},
}

//Construct the credentials for the tests and the stub server
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space after // please.

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.
// 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),
lastEvent: &audit.Event{},
}
audit.RegisterLoggerBuilder(lb)
i, _ := authz.NewStatic(test.authzPolicy)

s := grpc.NewServer(
grpc.Creds(loadServerCreds(t)),
grpc.Creds(serverCreds),
grpc.ChainUnaryInterceptor(i.UnaryInterceptor),
grpc.ChainStreamInterceptor(i.StreamInterceptor))
defer s.Stop()
testgrpc.RegisterTestServiceServer(s, &testServer{})
testgrpc.RegisterTestServiceServer(s, ss)
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("error listening: %v", err)
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)))
clientConn, err := grpc.Dial(lis.Addr().String(), grpc.WithTransportCredentials(clientCreds))
if err != nil {
t.Fatalf("grpc.Dial(%v) failed: %v", lis.Addr().String(), err)
}
Expand All @@ -262,74 +275,79 @@ func (s) TestAuditLogger(t *testing.T) {
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)
}
stream, _ := client.StreamingInputCall(ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the error.

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.Send(req)
stream.CloseAndRecv()

// Compare expected number of allows/denies with content of internal map of statAuditLogger.
// Compare expected number of allows/denies with content of the 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 != "" {
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 {
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")
}
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 {
cert, err := tls.LoadX509KeyPair(testdata.Path("x509/client_with_spiffe_cert.pem"), testdata.Path("x509/client_with_spiffe_key.pem"))
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(x509/client1_cert.pem, x509/client1_key.pem) failed: %v", err)
t.Fatalf("tls.LoadX509KeyPair(%q, %q) failed: %v", certPath, key, err)
}
ca, err := os.ReadFile(testdata.Path("x509/server_ca_cert.pem"))
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 {
Copy link
Member

Choose a reason for hiding this comment

The 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(x509/server_ca_cert.pem) failed: %v", err)
t.Fatalf("os.ReadFile(%q) failed: %v", certPath, err)
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(ca) {
t.Fatal("failed to append certificates")
t.Fatal("Failed to append certificates")
}
return credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: roots,
ServerName: "x.test.example.com",
})

return roots
}
22 changes: 11 additions & 11 deletions testdata/x509/create.sh
Expand Up @@ -130,20 +130,20 @@ openssl req -x509 \

# Generate a cert with SPIFFE ID using client_with_spiffe_openssl.cnf
openssl req -new \
dfawley marked this conversation as resolved.
Show resolved Hide resolved
-key client_with_spiffe_key.pem \
-out client_with_spiffe_csr.pem \
-key client_with_spiffe_key.pem \
-out client_with_spiffe_csr.pem \
-subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-client1/ \
-config ./client_with_spiffe_openssl.cnf \
-config ./client_with_spiffe_openssl.cnf \
-reqexts test_client
openssl x509 -req \
-in client_with_spiffe_csr.pem \
-CAkey client_ca_key.pem \
-CA client_ca_cert.pem \
-days 3650 \
-set_serial 1000 \
-out client_with_spiffe_cert.pem \
openssl x509 -req \
-in client_with_spiffe_csr.pem \
-CAkey client_ca_key.pem \
-CA client_ca_cert.pem \
-days 3650 \
-set_serial 1000 \
-out client_with_spiffe_cert.pem \
-extfile ./client_with_spiffe_openssl.cnf \
-extensions test_client \
-extensions test_client \
-sha256
openssl verify -verbose -CAfile client_with_spiffe_cert.pem

Expand Down