snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
127
authz/audit/audit_logger.go
Normal file
127
authz/audit/audit_logger.go
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
*
|
||||
* 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 contains interfaces for audit logging during authorization.
|
||||
package audit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// loggerBuilderRegistry holds a map of audit logger builders and a mutex
|
||||
// to facilitate thread-safe reading/writing operations.
|
||||
type loggerBuilderRegistry struct {
|
||||
mu sync.Mutex
|
||||
builders map[string]LoggerBuilder
|
||||
}
|
||||
|
||||
var (
|
||||
registry = loggerBuilderRegistry{
|
||||
builders: make(map[string]LoggerBuilder),
|
||||
}
|
||||
)
|
||||
|
||||
// RegisterLoggerBuilder registers the builder in a global map
|
||||
// using b.Name() as the key.
|
||||
//
|
||||
// This should only be called during initialization time (i.e. in an init()
|
||||
// function). If multiple builders are registered with the same name,
|
||||
// the one registered last will take effect.
|
||||
func RegisterLoggerBuilder(b LoggerBuilder) {
|
||||
registry.mu.Lock()
|
||||
defer registry.mu.Unlock()
|
||||
registry.builders[b.Name()] = b
|
||||
}
|
||||
|
||||
// GetLoggerBuilder returns a builder with the given name.
|
||||
// It returns nil if the builder is not found in the registry.
|
||||
func GetLoggerBuilder(name string) LoggerBuilder {
|
||||
registry.mu.Lock()
|
||||
defer registry.mu.Unlock()
|
||||
return registry.builders[name]
|
||||
}
|
||||
|
||||
// Event contains information passed to the audit logger as part of an
|
||||
// audit logging event.
|
||||
type Event struct {
|
||||
// FullMethodName is the full method name of the audited RPC, in the format
|
||||
// of "/pkg.Service/Method". For example, "/helloworld.Greeter/SayHello".
|
||||
FullMethodName string
|
||||
// Principal is the identity of the caller. Currently it will only be
|
||||
// available in certificate-based TLS authentication.
|
||||
Principal string
|
||||
// PolicyName is the authorization policy name or the xDS RBAC filter name.
|
||||
PolicyName string
|
||||
// MatchedRule is the matched rule or policy name in the xDS RBAC filter.
|
||||
// It will be empty if there is no match.
|
||||
MatchedRule string
|
||||
// Authorized indicates whether the audited RPC is authorized or not.
|
||||
Authorized bool
|
||||
}
|
||||
|
||||
// LoggerConfig represents an opaque data structure holding an audit
|
||||
// logger configuration. Concrete types representing configuration of specific
|
||||
// audit loggers must embed this interface to implement it.
|
||||
type LoggerConfig interface {
|
||||
loggerConfig()
|
||||
}
|
||||
|
||||
// Logger is the interface to be implemented by audit loggers.
|
||||
//
|
||||
// An audit logger is a logger instance that can be configured via the
|
||||
// authorization policy API or xDS HTTP RBAC filters. When the authorization
|
||||
// decision meets the condition for audit, all the configured audit loggers'
|
||||
// Log() method will be invoked to log that event.
|
||||
//
|
||||
// Please refer to
|
||||
// https://github.com/grpc/proposal/blob/master/A59-audit-logging.md for more
|
||||
// details about audit logging.
|
||||
type Logger interface {
|
||||
// Log performs audit logging for the provided audit event.
|
||||
//
|
||||
// This method is invoked in the RPC path and therefore implementations
|
||||
// must not block.
|
||||
Log(*Event)
|
||||
}
|
||||
|
||||
// LoggerBuilder is the interface to be implemented by audit logger
|
||||
// builders that are used at runtime to configure and instantiate audit loggers.
|
||||
//
|
||||
// Users who want to implement their own audit logging logic should
|
||||
// implement this interface, along with the Logger interface, and register
|
||||
// it by calling RegisterLoggerBuilder() at init time.
|
||||
//
|
||||
// Please refer to
|
||||
// https://github.com/grpc/proposal/blob/master/A59-audit-logging.md for more
|
||||
// details about audit logging.
|
||||
type LoggerBuilder interface {
|
||||
// ParseLoggerConfig parses the given JSON bytes into a structured
|
||||
// logger config this builder can use to build an audit logger.
|
||||
ParseLoggerConfig(config json.RawMessage) (LoggerConfig, error)
|
||||
// Build builds an audit logger with the given logger config.
|
||||
// This will only be called with valid configs returned from
|
||||
// ParseLoggerConfig() and any runtime issues such as failing to
|
||||
// create a file should be handled by the logger implementation instead of
|
||||
// failing the logger instantiation. So implementers need to make sure it
|
||||
// can return a logger without error at this stage.
|
||||
Build(LoggerConfig) Logger
|
||||
// Name returns the name of logger built by this builder.
|
||||
// This is used to register and pick the builder.
|
||||
Name() string
|
||||
}
|
||||
370
authz/audit/audit_logging_test.go
Normal file
370
authz/audit/audit_logging_test.go
Normal file
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
*
|
||||
* 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"
|
||||
"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/codes"
|
||||
"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/status"
|
||||
"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 // Field to store last received event
|
||||
}
|
||||
|
||||
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(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. Additionally, we specify
|
||||
// directly which authz outcome we expect from each type of call.
|
||||
tests := []struct {
|
||||
name string
|
||||
authzPolicy string
|
||||
wantAuthzOutcomes map[bool]int
|
||||
eventContent *audit.Event
|
||||
wantUnaryCallCode codes.Code
|
||||
wantStreamingCallCode codes.Code
|
||||
}{
|
||||
{
|
||||
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},
|
||||
wantUnaryCallCode: codes.OK,
|
||||
wantStreamingCallCode: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
wantUnaryCallCode: codes.OK,
|
||||
wantStreamingCallCode: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
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},
|
||||
wantUnaryCallCode: codes.OK,
|
||||
wantStreamingCallCode: codes.PermissionDenied,
|
||||
},
|
||||
{
|
||||
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},
|
||||
wantUnaryCallCode: codes.PermissionDenied,
|
||||
wantStreamingCallCode: codes.PermissionDenied,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Construct the credentials for the tests and the stub server
|
||||
serverCreds := loadServerCreds(t)
|
||||
clientCreds := loadClientCreds(t)
|
||||
ss := &stubserver.StubServer{
|
||||
UnaryCallF: func(context.Context, *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
|
||||
},
|
||||
}
|
||||
// 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()
|
||||
ss.S = s
|
||||
stubserver.StartTestService(t, ss)
|
||||
|
||||
// Setup gRPC test client with certificates containing a SPIFFE Id.
|
||||
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%v) failed: %v", ss.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != test.wantUnaryCallCode {
|
||||
t.Errorf("Unexpected UnaryCall fail: got %v want %v", err, test.wantUnaryCallCode)
|
||||
}
|
||||
if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != test.wantUnaryCallCode {
|
||||
t.Errorf("Unexpected UnaryCall fail: got %v want %v", err, test.wantUnaryCallCode)
|
||||
}
|
||||
stream, err := client.StreamingInputCall(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("StreamingInputCall failed: %v", err)
|
||||
}
|
||||
req := &testpb.StreamingInputCallRequest{
|
||||
Payload: &testpb.Payload{
|
||||
Body: []byte("hi"),
|
||||
},
|
||||
}
|
||||
if err := stream.Send(req); err != nil && err != io.EOF {
|
||||
t.Fatalf("stream.Send failed: %v", err)
|
||||
}
|
||||
if _, err := stream.CloseAndRecv(); status.Code(err) != test.wantStreamingCallCode {
|
||||
t.Errorf("Unexpected stream.CloseAndRecv fail: got %v want %v", err, test.wantStreamingCallCode)
|
||||
}
|
||||
|
||||
// Compare expected number of allows/denies with content of the internal
|
||||
// map of statAuditLogger.
|
||||
if diff := cmp.Diff(lb.authzDecisionStat, test.wantAuthzOutcomes); diff != "" {
|
||||
t.Errorf("Authorization decisions do not match\ndiff (-got +want):\n%s", diff)
|
||||
}
|
||||
// Compare last event received by statAuditLogger with expected event.
|
||||
if test.eventContent != nil {
|
||||
if diff := cmp.Diff(lb.lastEvent, test.eventContent); diff != "" {
|
||||
t.Errorf("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",
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// loadKeys 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 {
|
||||
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
|
||||
}
|
||||
110
authz/audit/stdout/stdout_logger.go
Normal file
110
authz/audit/stdout/stdout_logger.go
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
*
|
||||
* 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 stdout defines an stdout audit logger.
|
||||
package stdout
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/authz/audit"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
)
|
||||
|
||||
var grpcLogger = grpclog.Component("authz-audit")
|
||||
|
||||
// Name is the string to identify this logger type in the registry
|
||||
const Name = "stdout_logger"
|
||||
|
||||
func init() {
|
||||
audit.RegisterLoggerBuilder(&loggerBuilder{
|
||||
goLogger: log.New(os.Stdout, "", 0),
|
||||
})
|
||||
}
|
||||
|
||||
type event struct {
|
||||
FullMethodName string `json:"rpc_method"`
|
||||
Principal string `json:"principal"`
|
||||
PolicyName string `json:"policy_name"`
|
||||
MatchedRule string `json:"matched_rule"`
|
||||
Authorized bool `json:"authorized"`
|
||||
Timestamp string `json:"timestamp"` // Time when the audit event is logged via Log method
|
||||
}
|
||||
|
||||
// logger implements the audit.logger interface by logging to standard output.
|
||||
type logger struct {
|
||||
goLogger *log.Logger
|
||||
}
|
||||
|
||||
// Log marshals the audit.Event to json and prints it to standard output.
|
||||
func (l *logger) Log(event *audit.Event) {
|
||||
jsonContainer := map[string]any{
|
||||
"grpc_audit_log": convertEvent(event),
|
||||
}
|
||||
jsonBytes, err := json.Marshal(jsonContainer)
|
||||
if err != nil {
|
||||
grpcLogger.Errorf("failed to marshal AuditEvent data to JSON: %v", err)
|
||||
return
|
||||
}
|
||||
l.goLogger.Println(string(jsonBytes))
|
||||
}
|
||||
|
||||
// loggerConfig represents the configuration for the stdout logger.
|
||||
// It is currently empty and implements the audit.Logger interface by embedding it.
|
||||
type loggerConfig struct {
|
||||
audit.LoggerConfig
|
||||
}
|
||||
|
||||
type loggerBuilder struct {
|
||||
goLogger *log.Logger
|
||||
}
|
||||
|
||||
func (loggerBuilder) Name() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
// Build returns a new instance of the stdout logger.
|
||||
// Passed in configuration is ignored as the stdout logger does not
|
||||
// expect any configuration to be provided.
|
||||
func (lb *loggerBuilder) Build(audit.LoggerConfig) audit.Logger {
|
||||
return &logger{
|
||||
goLogger: lb.goLogger,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseLoggerConfig is a no-op since the stdout logger does not accept any configuration.
|
||||
func (*loggerBuilder) ParseLoggerConfig(config json.RawMessage) (audit.LoggerConfig, error) {
|
||||
if len(config) != 0 && string(config) != "{}" {
|
||||
grpcLogger.Warningf("Stdout logger doesn't support custom configs. Ignoring:\n%s", string(config))
|
||||
}
|
||||
return &loggerConfig{}, nil
|
||||
}
|
||||
|
||||
func convertEvent(auditEvent *audit.Event) *event {
|
||||
return &event{
|
||||
FullMethodName: auditEvent.FullMethodName,
|
||||
Principal: auditEvent.Principal,
|
||||
PolicyName: auditEvent.PolicyName,
|
||||
MatchedRule: auditEvent.MatchedRule,
|
||||
Authorized: auditEvent.Authorized,
|
||||
Timestamp: time.Now().Format(time.RFC3339Nano),
|
||||
}
|
||||
}
|
||||
140
authz/audit/stdout/stdout_logger_test.go
Normal file
140
authz/audit/stdout/stdout_logger_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
*
|
||||
* 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 stdout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/authz/audit"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func (s) TestStdoutLogger_Log(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
event *audit.Event
|
||||
wantMessage string
|
||||
wantErr string
|
||||
}{
|
||||
"few fields": {
|
||||
event: &audit.Event{PolicyName: "test policy", Principal: "test principal"},
|
||||
wantMessage: `{"fullMethodName":"","principal":"test principal","policyName":"test policy","matchedRule":"","authorized":false`,
|
||||
},
|
||||
"all fields": {
|
||||
event: &audit.Event{
|
||||
FullMethodName: "/helloworld.Greeter/SayHello",
|
||||
Principal: "spiffe://example.org/ns/default/sa/default/backend",
|
||||
PolicyName: "example-policy",
|
||||
MatchedRule: "dev-access",
|
||||
Authorized: true,
|
||||
},
|
||||
wantMessage: `{"fullMethodName":"/helloworld.Greeter/SayHello",` +
|
||||
`"principal":"spiffe://example.org/ns/default/sa/default/backend","policyName":"example-policy",` +
|
||||
`"matchedRule":"dev-access","authorized":true`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
before := time.Now().Unix()
|
||||
var buf bytes.Buffer
|
||||
builder := &loggerBuilder{goLogger: log.New(&buf, "", 0)}
|
||||
auditLogger := builder.Build(nil)
|
||||
|
||||
auditLogger.Log(test.event)
|
||||
|
||||
var container map[string]any
|
||||
if err := json.Unmarshal(buf.Bytes(), &container); err != nil {
|
||||
t.Fatalf("Failed to unmarshal audit log event: %v", err)
|
||||
}
|
||||
innerEvent := extractEvent(container["grpc_audit_log"].(map[string]any))
|
||||
if innerEvent.Timestamp == "" {
|
||||
t.Fatalf("Resulted event has no timestamp: %v", innerEvent)
|
||||
}
|
||||
after := time.Now().Unix()
|
||||
innerEventUnixTime, err := time.Parse(time.RFC3339Nano, innerEvent.Timestamp)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to convert event timestamp into Unix time format: %v", err)
|
||||
}
|
||||
if before > innerEventUnixTime.Unix() || after < innerEventUnixTime.Unix() {
|
||||
t.Errorf("The audit event timestamp is outside of the test interval: test start %v, event timestamp %v, test end %v", before, innerEventUnixTime.Unix(), after)
|
||||
}
|
||||
if diff := cmp.Diff(trimEvent(innerEvent), test.event); diff != "" {
|
||||
t.Fatalf("Unexpected message\ndiff (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestStdoutLoggerBuilder_NilConfig(t *testing.T) {
|
||||
builder := &loggerBuilder{
|
||||
goLogger: log.New(os.Stdout, "", log.LstdFlags),
|
||||
}
|
||||
config, err := builder.ParseLoggerConfig(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse stdout logger configuration: %v", err)
|
||||
}
|
||||
if l := builder.Build(config); l == nil {
|
||||
t.Fatal("Failed to build stdout audit logger")
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestStdoutLoggerBuilder_Registration(t *testing.T) {
|
||||
if audit.GetLoggerBuilder("stdout_logger") == nil {
|
||||
t.Fatal("stdout logger is not registered")
|
||||
}
|
||||
}
|
||||
|
||||
// extractEvent extracts an stdout.event from a map
|
||||
// unmarshalled from a logged json message.
|
||||
func extractEvent(container map[string]any) event {
|
||||
return event{
|
||||
FullMethodName: container["rpc_method"].(string),
|
||||
Principal: container["principal"].(string),
|
||||
PolicyName: container["policy_name"].(string),
|
||||
MatchedRule: container["matched_rule"].(string),
|
||||
Authorized: container["authorized"].(bool),
|
||||
Timestamp: container["timestamp"].(string),
|
||||
}
|
||||
}
|
||||
|
||||
// trimEvent converts a logged stdout.event into an audit.Event
|
||||
// by removing Timestamp field. It is used for comparing events during testing.
|
||||
func trimEvent(testEvent event) *audit.Event {
|
||||
return &audit.Event{
|
||||
FullMethodName: testEvent.FullMethodName,
|
||||
Principal: testEvent.Principal,
|
||||
PolicyName: testEvent.PolicyName,
|
||||
MatchedRule: testEvent.MatchedRule,
|
||||
Authorized: testEvent.Authorized,
|
||||
}
|
||||
}
|
||||
708
authz/grpc_authz_end2end_test.go
Normal file
708
authz/grpc_authz_end2end_test.go
Normal file
@@ -0,0 +1,708 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 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"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/authz"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/testdata"
|
||||
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
var authzTests = map[string]struct {
|
||||
authzPolicy string
|
||||
md metadata.MD
|
||||
wantStatus *status.Status
|
||||
}{
|
||||
"DeniesRPCMatchInDenyNoMatchInAllow": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_StreamingOutputCall",
|
||||
"request": {
|
||||
"paths":
|
||||
[
|
||||
"/grpc.testing.TestService/StreamingOutputCall"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"deny_rules":
|
||||
[
|
||||
{
|
||||
"name": "deny_TestServiceCalls",
|
||||
"request": {
|
||||
"paths":
|
||||
[
|
||||
"/grpc.testing.TestService/*"
|
||||
],
|
||||
"headers":
|
||||
[
|
||||
{
|
||||
"key": "key-abc",
|
||||
"values":
|
||||
[
|
||||
"val-abc",
|
||||
"val-def"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
md: metadata.Pairs("key-abc", "val-abc"),
|
||||
wantStatus: status.New(codes.PermissionDenied, "authz: unauthorized RPC request rejected"),
|
||||
},
|
||||
"DeniesRPCMatchInDenyAndAllow": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_all",
|
||||
"request": {
|
||||
"paths":
|
||||
[
|
||||
"*"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"deny_rules":
|
||||
[
|
||||
{
|
||||
"name": "deny_all",
|
||||
"request": {
|
||||
"paths":
|
||||
[
|
||||
"*"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantStatus: status.New(codes.PermissionDenied, "authz: unauthorized RPC request rejected"),
|
||||
},
|
||||
"AllowsRPCNoMatchInDenyMatchInAllow": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_all"
|
||||
}
|
||||
],
|
||||
"deny_rules":
|
||||
[
|
||||
{
|
||||
"name": "deny_TestServiceCalls",
|
||||
"request": {
|
||||
"paths":
|
||||
[
|
||||
"/grpc.testing.TestService/UnaryCall",
|
||||
"/grpc.testing.TestService/StreamingInputCall"
|
||||
],
|
||||
"headers":
|
||||
[
|
||||
{
|
||||
"key": "key-abc",
|
||||
"values":
|
||||
[
|
||||
"val-abc",
|
||||
"val-def"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
md: metadata.Pairs("key-xyz", "val-xyz"),
|
||||
wantStatus: status.New(codes.OK, ""),
|
||||
},
|
||||
"DeniesRPCNoMatchInDenyAndAllow": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_some_user",
|
||||
"source": {
|
||||
"principals":
|
||||
[
|
||||
"some_user"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"deny_rules":
|
||||
[
|
||||
{
|
||||
"name": "deny_StreamingOutputCall",
|
||||
"request": {
|
||||
"paths":
|
||||
[
|
||||
"/grpc.testing.TestService/StreamingOutputCall"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantStatus: status.New(codes.PermissionDenied, "authz: unauthorized RPC request rejected"),
|
||||
},
|
||||
"AllowsRPCEmptyDenyMatchInAllow": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_UnaryCall",
|
||||
"request":
|
||||
{
|
||||
"paths":
|
||||
[
|
||||
"/grpc.testing.TestService/UnaryCall"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "allow_StreamingInputCall",
|
||||
"request":
|
||||
{
|
||||
"paths":
|
||||
[
|
||||
"/grpc.testing.TestService/StreamingInputCall"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantStatus: status.New(codes.OK, ""),
|
||||
},
|
||||
"DeniesRPCEmptyDenyNoMatchInAllow": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_StreamingOutputCall",
|
||||
"request":
|
||||
{
|
||||
"paths":
|
||||
[
|
||||
"/grpc.testing.TestService/StreamingOutputCall"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantStatus: status.New(codes.PermissionDenied, "authz: unauthorized RPC request rejected"),
|
||||
},
|
||||
"DeniesRPCRequestWithPrincipalsFieldOnUnauthenticatedConnection": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_authenticated",
|
||||
"source": {
|
||||
"principals": ["*", ""]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantStatus: status.New(codes.PermissionDenied, "authz: unauthorized RPC request rejected"),
|
||||
},
|
||||
"DeniesRPCRequestNoMatchInAllowFailsPresenceMatch": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_TestServiceCalls",
|
||||
"request": {
|
||||
"paths":
|
||||
[
|
||||
"/grpc.testing.TestService/*"
|
||||
],
|
||||
"headers":
|
||||
[
|
||||
{
|
||||
"key": "key-abc",
|
||||
"values":
|
||||
[
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
md: metadata.Pairs("key-abc", ""),
|
||||
wantStatus: status.New(codes.PermissionDenied, "authz: unauthorized RPC request rejected"),
|
||||
},
|
||||
}
|
||||
|
||||
func (s) TestStaticPolicyEnd2End(t *testing.T) {
|
||||
for name, test := range authzTests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Start a gRPC server with gRPC authz unary and stream server interceptors.
|
||||
i, _ := authz.NewStatic(test.authzPolicy)
|
||||
|
||||
stub := &stubserver.StubServer{
|
||||
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
|
||||
return &testpb.SimpleResponse{}, nil
|
||||
},
|
||||
StreamingInputCallF: func(stream testgrpc.TestService_StreamingInputCallServer) error {
|
||||
for {
|
||||
_, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return stream.SendAndClose(&testpb.StreamingInputCallResponse{})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
},
|
||||
S: grpc.NewServer(grpc.ChainUnaryInterceptor(i.UnaryInterceptor), grpc.ChainStreamInterceptor(i.StreamInterceptor)),
|
||||
}
|
||||
stubserver.StartTestService(t, stub)
|
||||
defer stub.Stop()
|
||||
|
||||
// Establish a connection to the server.
|
||||
cc, err := grpc.NewClient(stub.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%v) failed: %v", stub.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
ctx = metadata.NewOutgoingContext(ctx, test.md)
|
||||
|
||||
// Verifying authorization decision for Unary RPC.
|
||||
_, err = client.UnaryCall(ctx, &testpb.SimpleRequest{})
|
||||
if got := status.Convert(err); got.Code() != test.wantStatus.Code() || got.Message() != test.wantStatus.Message() {
|
||||
t.Fatalf("[UnaryCall] error want:{%v} got:{%v}", test.wantStatus.Err(), got.Err())
|
||||
}
|
||||
|
||||
// Verifying authorization decision for Streaming RPC.
|
||||
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)
|
||||
}
|
||||
_, err = stream.CloseAndRecv()
|
||||
if got := status.Convert(err); got.Code() != test.wantStatus.Code() || got.Message() != test.wantStatus.Message() {
|
||||
t.Fatalf("[StreamingCall] error want:{%v} got:{%v}", test.wantStatus.Err(), got.Err())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestAllowsRPCRequestWithPrincipalsFieldOnTLSAuthenticatedConnection(t *testing.T) {
|
||||
authzPolicy := `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_authenticated",
|
||||
"source": {
|
||||
"principals": ["*", ""]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
// Start a gRPC server with gRPC authz unary server interceptor.
|
||||
i, _ := authz.NewStatic(authzPolicy)
|
||||
creds, err := credentials.NewServerTLSFromFile(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate credentials: %v", err)
|
||||
}
|
||||
|
||||
stub := &stubserver.StubServer{
|
||||
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
|
||||
return &testpb.SimpleResponse{}, nil
|
||||
},
|
||||
S: grpc.NewServer(grpc.Creds(creds), grpc.ChainUnaryInterceptor(i.UnaryInterceptor)),
|
||||
}
|
||||
stubserver.StartTestService(t, stub)
|
||||
defer stub.S.Stop()
|
||||
|
||||
// Establish a connection to the server.
|
||||
creds, err = credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load credentials: %v", err)
|
||||
}
|
||||
cc, err := grpc.NewClient(stub.Address, grpc.WithTransportCredentials(creds))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%v) failed: %v", stub.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Verifying authorization decision.
|
||||
if _, err = client.UnaryCall(ctx, &testpb.SimpleRequest{}); err != nil {
|
||||
t.Fatalf("client.UnaryCall(_, _) = %v; want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestAllowsRPCRequestWithPrincipalsFieldOnMTLSAuthenticatedConnection(t *testing.T) {
|
||||
authzPolicy := `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_authenticated",
|
||||
"source": {
|
||||
"principals": ["*", ""]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
// Start a gRPC server with gRPC authz unary server interceptor.
|
||||
i, _ := authz.NewStatic(authzPolicy)
|
||||
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")
|
||||
}
|
||||
creds := credentials.NewTLS(&tls.Config{
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ClientCAs: certPool,
|
||||
})
|
||||
stub := &stubserver.StubServer{
|
||||
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
|
||||
return &testpb.SimpleResponse{}, nil
|
||||
},
|
||||
S: grpc.NewServer(grpc.Creds(creds), grpc.ChainUnaryInterceptor(i.UnaryInterceptor)),
|
||||
}
|
||||
stubserver.StartTestService(t, stub)
|
||||
defer stub.Stop()
|
||||
|
||||
// Establish a connection to the server.
|
||||
cert, err = tls.LoadX509KeyPair(testdata.Path("x509/client1_cert.pem"), testdata.Path("x509/client1_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")
|
||||
}
|
||||
creds = credentials.NewTLS(&tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: roots,
|
||||
ServerName: "x.test.example.com",
|
||||
})
|
||||
cc, err := grpc.NewClient(stub.Address, grpc.WithTransportCredentials(creds))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%v) failed: %v", stub.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Verifying authorization decision.
|
||||
if _, err = client.UnaryCall(ctx, &testpb.SimpleRequest{}); err != nil {
|
||||
t.Fatalf("client.UnaryCall(_, _) = %v; want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestFileWatcherEnd2End(t *testing.T) {
|
||||
for name, test := range authzTests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
file := createTmpPolicyFile(t, name, []byte(test.authzPolicy))
|
||||
i, _ := authz.NewFileWatcher(file, 1*time.Second)
|
||||
defer i.Close()
|
||||
|
||||
stub := &stubserver.StubServer{
|
||||
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
|
||||
return &testpb.SimpleResponse{}, nil
|
||||
},
|
||||
StreamingInputCallF: func(stream testgrpc.TestService_StreamingInputCallServer) error {
|
||||
for {
|
||||
_, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return stream.SendAndClose(&testpb.StreamingInputCallResponse{})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
},
|
||||
// Start a gRPC server with gRPC authz unary and stream server interceptors.
|
||||
S: grpc.NewServer(grpc.ChainUnaryInterceptor(i.UnaryInterceptor), grpc.ChainStreamInterceptor(i.StreamInterceptor)),
|
||||
}
|
||||
stubserver.StartTestService(t, stub)
|
||||
defer stub.Stop()
|
||||
|
||||
// Establish a connection to the server.
|
||||
cc, err := grpc.NewClient(stub.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%v) failed: %v", stub.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
ctx = metadata.NewOutgoingContext(ctx, test.md)
|
||||
|
||||
// Verifying authorization decision for Unary RPC.
|
||||
_, err = client.UnaryCall(ctx, &testpb.SimpleRequest{})
|
||||
if got := status.Convert(err); got.Code() != test.wantStatus.Code() || got.Message() != test.wantStatus.Message() {
|
||||
t.Fatalf("[UnaryCall] error want:{%v} got:{%v}", test.wantStatus.Err(), got.Err())
|
||||
}
|
||||
|
||||
// Verifying authorization decision for Streaming RPC.
|
||||
stream, err := client.StreamingInputCall(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed StreamingInputCall : %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 : %v", err)
|
||||
}
|
||||
_, err = stream.CloseAndRecv()
|
||||
if got := status.Convert(err); got.Code() != test.wantStatus.Code() || got.Message() != test.wantStatus.Message() {
|
||||
t.Fatalf("[StreamingCall] error want:{%v} got:{%v}", test.wantStatus.Err(), got.Err())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func retryUntil(ctx context.Context, tsc testgrpc.TestServiceClient, want *status.Status) (lastErr error) {
|
||||
for ctx.Err() == nil {
|
||||
_, lastErr = tsc.UnaryCall(ctx, &testpb.SimpleRequest{})
|
||||
if s := status.Convert(lastErr); s.Code() == want.Code() && s.Message() == want.Message() {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (s) TestFileWatcher_ValidPolicyRefresh(t *testing.T) {
|
||||
valid1 := authzTests["DeniesRPCMatchInDenyAndAllow"]
|
||||
file := createTmpPolicyFile(t, "valid_policy_refresh", []byte(valid1.authzPolicy))
|
||||
i, _ := authz.NewFileWatcher(file, 100*time.Millisecond)
|
||||
defer i.Close()
|
||||
|
||||
stub := &stubserver.StubServer{
|
||||
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
|
||||
return &testpb.SimpleResponse{}, nil
|
||||
},
|
||||
// Start a gRPC server with gRPC authz unary server interceptor.
|
||||
S: grpc.NewServer(grpc.ChainUnaryInterceptor(i.UnaryInterceptor)),
|
||||
}
|
||||
stubserver.StartTestService(t, stub)
|
||||
defer stub.Stop()
|
||||
|
||||
// Establish a connection to the server.
|
||||
cc, err := grpc.NewClient(stub.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%v) failed: %v", stub.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Verifying authorization decision.
|
||||
_, err = client.UnaryCall(ctx, &testpb.SimpleRequest{})
|
||||
if got := status.Convert(err); got.Code() != valid1.wantStatus.Code() || got.Message() != valid1.wantStatus.Message() {
|
||||
t.Fatalf("client.UnaryCall(_, _) = %v; want = %v", got.Err(), valid1.wantStatus.Err())
|
||||
}
|
||||
|
||||
// Rewrite the file with a different valid authorization policy.
|
||||
valid2 := authzTests["AllowsRPCEmptyDenyMatchInAllow"]
|
||||
if err := os.WriteFile(file, []byte(valid2.authzPolicy), os.ModePerm); err != nil {
|
||||
t.Fatalf("os.WriteFile(%q) failed: %v", file, err)
|
||||
}
|
||||
|
||||
// Verifying authorization decision.
|
||||
if got := retryUntil(ctx, client, valid2.wantStatus); got != nil {
|
||||
t.Fatalf("client.UnaryCall(_, _) = %v; want = %v", got, valid2.wantStatus.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestFileWatcher_InvalidPolicySkipReload(t *testing.T) {
|
||||
valid := authzTests["DeniesRPCMatchInDenyAndAllow"]
|
||||
file := createTmpPolicyFile(t, "invalid_policy_skip_reload", []byte(valid.authzPolicy))
|
||||
i, _ := authz.NewFileWatcher(file, 20*time.Millisecond)
|
||||
defer i.Close()
|
||||
|
||||
stub := &stubserver.StubServer{
|
||||
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
|
||||
return &testpb.SimpleResponse{}, nil
|
||||
},
|
||||
// Start a gRPC server with gRPC authz unary server interceptors.
|
||||
S: grpc.NewServer(grpc.ChainUnaryInterceptor(i.UnaryInterceptor)),
|
||||
}
|
||||
stubserver.StartTestService(t, stub)
|
||||
defer stub.Stop()
|
||||
|
||||
// Establish a connection to the server.
|
||||
cc, err := grpc.NewClient(stub.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%v) failed: %v", stub.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Verifying authorization decision.
|
||||
_, err = client.UnaryCall(ctx, &testpb.SimpleRequest{})
|
||||
if got := status.Convert(err); got.Code() != valid.wantStatus.Code() || got.Message() != valid.wantStatus.Message() {
|
||||
t.Fatalf("client.UnaryCall(_, _) = %v; want = %v", got.Err(), valid.wantStatus.Err())
|
||||
}
|
||||
|
||||
// Skips the invalid policy update, and continues to use the valid policy.
|
||||
if err := os.WriteFile(file, []byte("{}"), os.ModePerm); err != nil {
|
||||
t.Fatalf("os.WriteFile(%q) failed: %v", file, err)
|
||||
}
|
||||
|
||||
// Wait 40 ms for background go routine to read updated files.
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
|
||||
// Verifying authorization decision.
|
||||
_, err = client.UnaryCall(ctx, &testpb.SimpleRequest{})
|
||||
if got := status.Convert(err); got.Code() != valid.wantStatus.Code() || got.Message() != valid.wantStatus.Message() {
|
||||
t.Fatalf("client.UnaryCall(_, _) = %v; want = %v", got.Err(), valid.wantStatus.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestFileWatcher_RecoversFromReloadFailure(t *testing.T) {
|
||||
valid1 := authzTests["DeniesRPCMatchInDenyAndAllow"]
|
||||
file := createTmpPolicyFile(t, "recovers_from_reload_failure", []byte(valid1.authzPolicy))
|
||||
i, _ := authz.NewFileWatcher(file, 100*time.Millisecond)
|
||||
defer i.Close()
|
||||
|
||||
stub := &stubserver.StubServer{
|
||||
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
|
||||
return &testpb.SimpleResponse{}, nil
|
||||
},
|
||||
S: grpc.NewServer(grpc.ChainUnaryInterceptor(i.UnaryInterceptor)),
|
||||
}
|
||||
stubserver.StartTestService(t, stub)
|
||||
defer stub.Stop()
|
||||
|
||||
// Establish a connection to the server.
|
||||
cc, err := grpc.NewClient(stub.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%v) failed: %v", stub.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Verifying authorization decision.
|
||||
_, err = client.UnaryCall(ctx, &testpb.SimpleRequest{})
|
||||
if got := status.Convert(err); got.Code() != valid1.wantStatus.Code() || got.Message() != valid1.wantStatus.Message() {
|
||||
t.Fatalf("client.UnaryCall(_, _) = %v; want = %v", got.Err(), valid1.wantStatus.Err())
|
||||
}
|
||||
|
||||
// Skips the invalid policy update, and continues to use the valid policy.
|
||||
if err := os.WriteFile(file, []byte("{}"), os.ModePerm); err != nil {
|
||||
t.Fatalf("os.WriteFile(%q) failed: %v", file, err)
|
||||
}
|
||||
|
||||
// Wait 120 ms for background go routine to read updated files.
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
// Verifying authorization decision.
|
||||
_, err = client.UnaryCall(ctx, &testpb.SimpleRequest{})
|
||||
if got := status.Convert(err); got.Code() != valid1.wantStatus.Code() || got.Message() != valid1.wantStatus.Message() {
|
||||
t.Fatalf("client.UnaryCall(_, _) = %v; want = %v", got.Err(), valid1.wantStatus.Err())
|
||||
}
|
||||
|
||||
// Rewrite the file with a different valid authorization policy.
|
||||
valid2 := authzTests["AllowsRPCEmptyDenyMatchInAllow"]
|
||||
if err := os.WriteFile(file, []byte(valid2.authzPolicy), os.ModePerm); err != nil {
|
||||
t.Fatalf("os.WriteFile(%q) failed: %v", file, err)
|
||||
}
|
||||
|
||||
// Verifying authorization decision.
|
||||
if got := retryUntil(ctx, client, valid2.wantStatus); got != nil {
|
||||
t.Fatalf("client.UnaryCall(_, _) = %v; want = %v", got, valid2.wantStatus.Err())
|
||||
}
|
||||
}
|
||||
211
authz/grpc_authz_server_interceptors.go
Normal file
211
authz/grpc_authz_server_interceptors.go
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2021 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal/xds/rbac"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var logger = grpclog.Component("authz")
|
||||
|
||||
// StaticInterceptor contains engines used to make authorization decisions. It
|
||||
// either contains two engines deny engine followed by an allow engine or only
|
||||
// one allow engine.
|
||||
type StaticInterceptor struct {
|
||||
engines rbac.ChainEngine
|
||||
}
|
||||
|
||||
// NewStatic returns a new StaticInterceptor from a static authorization policy
|
||||
// JSON string.
|
||||
func NewStatic(authzPolicy string) (*StaticInterceptor, error) {
|
||||
rbacs, policyName, err := translatePolicy(authzPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chainEngine, err := rbac.NewChainEngine(rbacs, policyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &StaticInterceptor{*chainEngine}, nil
|
||||
}
|
||||
|
||||
// UnaryInterceptor intercepts incoming Unary RPC requests.
|
||||
// Only authorized requests are allowed to pass. Otherwise, an unauthorized
|
||||
// error is returned to the client.
|
||||
func (i *StaticInterceptor) UnaryInterceptor(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
err := i.engines.IsAuthorized(ctx)
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.PermissionDenied {
|
||||
if logger.V(2) {
|
||||
logger.Infof("unauthorized RPC request rejected: %v", err)
|
||||
}
|
||||
return nil, status.Errorf(codes.PermissionDenied, "authz: unauthorized RPC request rejected")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
// StreamInterceptor intercepts incoming Stream RPC requests.
|
||||
// Only authorized requests are allowed to pass. Otherwise, an unauthorized
|
||||
// error is returned to the client.
|
||||
func (i *StaticInterceptor) StreamInterceptor(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
err := i.engines.IsAuthorized(ss.Context())
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.PermissionDenied {
|
||||
if logger.V(2) {
|
||||
logger.Infof("unauthorized RPC request rejected: %v", err)
|
||||
}
|
||||
return status.Errorf(codes.PermissionDenied, "authz: unauthorized RPC request rejected")
|
||||
}
|
||||
return err
|
||||
}
|
||||
return handler(srv, ss)
|
||||
}
|
||||
|
||||
// FileWatcherInterceptor contains details used to make authorization decisions
|
||||
// by watching a file path that contains authorization policy in JSON format.
|
||||
type FileWatcherInterceptor struct {
|
||||
options FileWatcherOptions
|
||||
internalInterceptor unsafe.Pointer // *StaticInterceptor
|
||||
policyContents []byte
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// FileWatcherOptions contains configuration options for the
|
||||
// FileWatcherInterceptor.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
|
||||
// later release.
|
||||
type FileWatcherOptions struct {
|
||||
// PolicyFile contains a JSON string of the authorization policy.
|
||||
PolicyFile string
|
||||
// RefreshDuration is the delay between policy refreshes.
|
||||
RefreshDuration time.Duration
|
||||
// OnPolicyUpdate is a callback to be invoked when a policy is
|
||||
// loaded/updated. The loaded policy string is passed as an argument.
|
||||
//
|
||||
// The callback is executed synchronously, so should complete quickly or
|
||||
// risk blocking future updates.
|
||||
OnPolicyUpdate func(string)
|
||||
}
|
||||
|
||||
// NewFileWatcher returns a new FileWatcherInterceptor from a policy file
|
||||
// that contains JSON string of authorization policy and a refresh duration to
|
||||
// specify the amount of time between policy refreshes.
|
||||
func NewFileWatcher(file string, duration time.Duration) (*FileWatcherInterceptor, error) {
|
||||
return NewFileWatcherWithOptions(FileWatcherOptions{PolicyFile: file, RefreshDuration: duration, OnPolicyUpdate: nil})
|
||||
}
|
||||
|
||||
// NewFileWatcherWithOptions returns a new FileWatcherInterceptor from a set of
|
||||
// options.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
|
||||
// later release.
|
||||
func NewFileWatcherWithOptions(options FileWatcherOptions) (*FileWatcherInterceptor, error) {
|
||||
if options.PolicyFile == "" {
|
||||
return nil, fmt.Errorf("authz: authorization policy file path is empty")
|
||||
}
|
||||
if options.RefreshDuration <= time.Duration(0) {
|
||||
return nil, fmt.Errorf("authz: requires refresh interval(%v) greater than 0s", options.RefreshDuration)
|
||||
}
|
||||
i := &FileWatcherInterceptor{options: options}
|
||||
if err := i.updateInternalInterceptor(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
i.cancel = cancel
|
||||
// Create a background go routine for policy refresh.
|
||||
go i.run(ctx)
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *FileWatcherInterceptor) run(ctx context.Context) {
|
||||
ticker := time.NewTicker(i.options.RefreshDuration)
|
||||
for {
|
||||
if err := i.updateInternalInterceptor(); err != nil {
|
||||
logger.Warningf("authorization policy reload status err: %v", err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateInternalInterceptor checks if the policy file that is watching has changed,
|
||||
// and if so, updates the internalInterceptor with the policy. Unlike the
|
||||
// constructor, if there is an error in reading the file or parsing the policy, the
|
||||
// previous internalInterceptors will not be replaced.
|
||||
func (i *FileWatcherInterceptor) updateInternalInterceptor() error {
|
||||
policyContents, err := os.ReadFile(i.options.PolicyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("policyFile(%s) read failed: %v", i.options.PolicyFile, err)
|
||||
}
|
||||
if bytes.Equal(i.policyContents, policyContents) {
|
||||
return nil
|
||||
}
|
||||
i.policyContents = policyContents
|
||||
policyContentsString := string(policyContents)
|
||||
interceptor, err := NewStatic(policyContentsString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
atomic.StorePointer(&i.internalInterceptor, unsafe.Pointer(interceptor))
|
||||
logger.Infof("authorization policy reload status: successfully loaded new policy %v", policyContentsString)
|
||||
if i.options.OnPolicyUpdate != nil {
|
||||
i.options.OnPolicyUpdate(policyContentsString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close cleans up resources allocated by the interceptor.
|
||||
func (i *FileWatcherInterceptor) Close() {
|
||||
i.cancel()
|
||||
}
|
||||
|
||||
// UnaryInterceptor intercepts incoming Unary RPC requests.
|
||||
// Only authorized requests are allowed to pass. Otherwise, an unauthorized
|
||||
// error is returned to the client.
|
||||
func (i *FileWatcherInterceptor) UnaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
return ((*StaticInterceptor)(atomic.LoadPointer(&i.internalInterceptor))).UnaryInterceptor(ctx, req, info, handler)
|
||||
}
|
||||
|
||||
// StreamInterceptor intercepts incoming Stream RPC requests.
|
||||
// Only authorized requests are allowed to pass. Otherwise, an unauthorized
|
||||
// error is returned to the client.
|
||||
func (i *FileWatcherInterceptor) StreamInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
return ((*StaticInterceptor)(atomic.LoadPointer(&i.internalInterceptor))).StreamInterceptor(srv, ss, info, handler)
|
||||
}
|
||||
174
authz/grpc_authz_server_interceptors_test.go
Normal file
174
authz/grpc_authz_server_interceptors_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 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"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/authz"
|
||||
)
|
||||
|
||||
const defaultTestTimeout = 10 * time.Second
|
||||
|
||||
func createTmpPolicyFile(t *testing.T, dirSuffix string, policy []byte) string {
|
||||
t.Helper()
|
||||
|
||||
// Create a temp directory. Passing an empty string for the first argument
|
||||
// uses the system temp directory.
|
||||
dir, err := os.MkdirTemp("", dirSuffix)
|
||||
if err != nil {
|
||||
t.Fatalf("os.MkdirTemp() failed: %v", err)
|
||||
}
|
||||
t.Logf("Using tmpdir: %s", dir)
|
||||
// Write policy into file.
|
||||
filename := path.Join(dir, "policy.json")
|
||||
if err := os.WriteFile(filename, policy, os.ModePerm); err != nil {
|
||||
t.Fatalf("os.WriteFile(%q) failed: %v", filename, err)
|
||||
}
|
||||
t.Logf("Wrote policy %s to file at %s", string(policy), filename)
|
||||
return filename
|
||||
}
|
||||
|
||||
func (s) TestNewStatic(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
authzPolicy string
|
||||
wantErr error
|
||||
}{
|
||||
"InvalidPolicyFailsToCreateInterceptor": {
|
||||
authzPolicy: `{}`,
|
||||
wantErr: fmt.Errorf(`"name" is not present`),
|
||||
},
|
||||
"ValidPolicyCreatesInterceptor": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_all"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := authz.NewStatic(test.authzPolicy); fmt.Sprint(err) != fmt.Sprint(test.wantErr) {
|
||||
t.Fatalf("NewStatic(%v) returned err: %v, want err: %v", test.authzPolicy, err, test.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestNewFileWatcher(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
authzPolicy string
|
||||
refreshDuration time.Duration
|
||||
wantErr error
|
||||
}{
|
||||
"InvalidRefreshDurationFailsToCreateInterceptor": {
|
||||
refreshDuration: time.Duration(0),
|
||||
wantErr: fmt.Errorf("authz: requires refresh interval(0s) greater than 0s"),
|
||||
},
|
||||
"InvalidPolicyFailsToCreateInterceptor": {
|
||||
authzPolicy: `{}`,
|
||||
refreshDuration: time.Duration(1),
|
||||
wantErr: fmt.Errorf(`"name" is not present`),
|
||||
},
|
||||
"ValidPolicyCreatesInterceptor": {
|
||||
authzPolicy: `{
|
||||
"name": "authz",
|
||||
"allow_rules":
|
||||
[
|
||||
{
|
||||
"name": "allow_all"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
refreshDuration: time.Duration(1),
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
file := createTmpPolicyFile(t, name, []byte(test.authzPolicy))
|
||||
i, err := authz.NewFileWatcher(file, test.refreshDuration)
|
||||
if fmt.Sprint(err) != fmt.Sprint(test.wantErr) {
|
||||
t.Fatalf("NewFileWatcher(%v) returned err: %v, want err: %v", test.authzPolicy, err, test.wantErr)
|
||||
}
|
||||
if i != nil {
|
||||
i.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestOnPolicyUpdate(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
updates := make(chan string, 1)
|
||||
onPolicyUpdate := func(s string) {
|
||||
updates <- s
|
||||
}
|
||||
|
||||
content := `{"name": "foo1", "allow_rules":[{"name":"bar"}]}`
|
||||
file := createTmpPolicyFile(t, "onpolicyupdate", []byte(content))
|
||||
opts := authz.FileWatcherOptions{PolicyFile: file, RefreshDuration: 50 * time.Millisecond, OnPolicyUpdate: onPolicyUpdate}
|
||||
i, err := authz.NewFileWatcherWithOptions(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFileWatcherWithOptions(%v) returned err: %v", opts, err)
|
||||
}
|
||||
defer i.Close()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout waiting for policy update")
|
||||
case update := <-updates:
|
||||
if update != content {
|
||||
t.Fatalf("Unexpected contents on first load of policy file: got=%v, want=%v", update, content)
|
||||
}
|
||||
}
|
||||
|
||||
// Tweak the file, expect an update.
|
||||
content = `{"name": "foo2", "allow_rules":[{"name":"bar"}]}`
|
||||
if err := os.WriteFile(file, []byte(content), os.ModePerm); err != nil {
|
||||
t.Fatalf("os.WriteFile(%q) failed: %v", file, err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout waiting for policy update")
|
||||
case update := <-updates:
|
||||
if update != content {
|
||||
t.Fatalf("Unexpected contents after policy file changed: got=%v, want=%v", update, content)
|
||||
}
|
||||
}
|
||||
|
||||
sCtx, sCancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
defer sCancel()
|
||||
select {
|
||||
case <-updates:
|
||||
t.Fatal("OnPolicyUpdate callback invoked more times than expected")
|
||||
case <-sCtx.Done():
|
||||
}
|
||||
}
|
||||
398
authz/rbac_translator.go
Normal file
398
authz/rbac_translator.go
Normal file
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* Copyright 2021 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 exposes methods to manage authorization within gRPC.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: This package is EXPERIMENTAL and may be changed or removed
|
||||
// in a later release.
|
||||
package authz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1xdsudpatypepb "github.com/cncf/xds/go/udpa/type/v1"
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
|
||||
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// This is used when converting a custom config from raw JSON to a TypedStruct
|
||||
// The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>"
|
||||
const typeURLPrefix = "grpc.authz.audit_logging/"
|
||||
|
||||
type header struct {
|
||||
Key string
|
||||
Values []string
|
||||
}
|
||||
|
||||
type peer struct {
|
||||
Principals []string
|
||||
}
|
||||
|
||||
type request struct {
|
||||
Paths []string
|
||||
Headers []header
|
||||
}
|
||||
|
||||
type rule struct {
|
||||
Name string
|
||||
Source peer
|
||||
Request request
|
||||
}
|
||||
|
||||
type auditLogger struct {
|
||||
Name string `json:"name"`
|
||||
Config *structpb.Struct `json:"config"`
|
||||
IsOptional bool `json:"is_optional"`
|
||||
}
|
||||
|
||||
type auditLoggingOptions struct {
|
||||
AuditCondition string `json:"audit_condition"`
|
||||
AuditLoggers []*auditLogger `json:"audit_loggers"`
|
||||
}
|
||||
|
||||
// Represents the SDK authorization policy provided by user.
|
||||
type authorizationPolicy struct {
|
||||
Name string
|
||||
DenyRules []rule `json:"deny_rules"`
|
||||
AllowRules []rule `json:"allow_rules"`
|
||||
AuditLoggingOptions auditLoggingOptions `json:"audit_logging_options"`
|
||||
}
|
||||
|
||||
func principalOr(principals []*v3rbacpb.Principal) *v3rbacpb.Principal {
|
||||
return &v3rbacpb.Principal{
|
||||
Identifier: &v3rbacpb.Principal_OrIds{
|
||||
OrIds: &v3rbacpb.Principal_Set{
|
||||
Ids: principals,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func permissionOr(permission []*v3rbacpb.Permission) *v3rbacpb.Permission {
|
||||
return &v3rbacpb.Permission{
|
||||
Rule: &v3rbacpb.Permission_OrRules{
|
||||
OrRules: &v3rbacpb.Permission_Set{
|
||||
Rules: permission,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func permissionAnd(permission []*v3rbacpb.Permission) *v3rbacpb.Permission {
|
||||
return &v3rbacpb.Permission{
|
||||
Rule: &v3rbacpb.Permission_AndRules{
|
||||
AndRules: &v3rbacpb.Permission_Set{
|
||||
Rules: permission,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getStringMatcher(value string) *v3matcherpb.StringMatcher {
|
||||
switch {
|
||||
case value == "*":
|
||||
return &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{
|
||||
SafeRegex: &v3matcherpb.RegexMatcher{Regex: ".+"}},
|
||||
}
|
||||
case strings.HasSuffix(value, "*"):
|
||||
prefix := strings.TrimSuffix(value, "*")
|
||||
return &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: prefix},
|
||||
}
|
||||
case strings.HasPrefix(value, "*"):
|
||||
suffix := strings.TrimPrefix(value, "*")
|
||||
return &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: suffix},
|
||||
}
|
||||
default:
|
||||
return &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: value},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getHeaderMatcher(key, value string) *v3routepb.HeaderMatcher {
|
||||
switch {
|
||||
case value == "*":
|
||||
return &v3routepb.HeaderMatcher{
|
||||
Name: key,
|
||||
HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{
|
||||
SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: ".+"}},
|
||||
}
|
||||
case strings.HasSuffix(value, "*"):
|
||||
prefix := strings.TrimSuffix(value, "*")
|
||||
return &v3routepb.HeaderMatcher{
|
||||
Name: key,
|
||||
HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: prefix},
|
||||
}
|
||||
case strings.HasPrefix(value, "*"):
|
||||
suffix := strings.TrimPrefix(value, "*")
|
||||
return &v3routepb.HeaderMatcher{
|
||||
Name: key,
|
||||
HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: suffix},
|
||||
}
|
||||
default:
|
||||
return &v3routepb.HeaderMatcher{
|
||||
Name: key,
|
||||
HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: value},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parsePrincipalNames(principalNames []string) []*v3rbacpb.Principal {
|
||||
ps := make([]*v3rbacpb.Principal, 0, len(principalNames))
|
||||
for _, principalName := range principalNames {
|
||||
newPrincipalName := &v3rbacpb.Principal{
|
||||
Identifier: &v3rbacpb.Principal_Authenticated_{
|
||||
Authenticated: &v3rbacpb.Principal_Authenticated{
|
||||
PrincipalName: getStringMatcher(principalName),
|
||||
},
|
||||
}}
|
||||
ps = append(ps, newPrincipalName)
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
func parsePeer(source peer) *v3rbacpb.Principal {
|
||||
if len(source.Principals) == 0 {
|
||||
return &v3rbacpb.Principal{
|
||||
Identifier: &v3rbacpb.Principal_Any{
|
||||
Any: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
return principalOr(parsePrincipalNames(source.Principals))
|
||||
}
|
||||
|
||||
func parsePaths(paths []string) []*v3rbacpb.Permission {
|
||||
ps := make([]*v3rbacpb.Permission, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
newPath := &v3rbacpb.Permission{
|
||||
Rule: &v3rbacpb.Permission_UrlPath{
|
||||
UrlPath: &v3matcherpb.PathMatcher{
|
||||
Rule: &v3matcherpb.PathMatcher_Path{Path: getStringMatcher(path)}}}}
|
||||
ps = append(ps, newPath)
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
func parseHeaderValues(key string, values []string) []*v3rbacpb.Permission {
|
||||
vs := make([]*v3rbacpb.Permission, 0, len(values))
|
||||
for _, value := range values {
|
||||
newHeader := &v3rbacpb.Permission{
|
||||
Rule: &v3rbacpb.Permission_Header{
|
||||
Header: getHeaderMatcher(key, value)}}
|
||||
vs = append(vs, newHeader)
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
var unsupportedHeaders = map[string]bool{
|
||||
"host": true,
|
||||
"connection": true,
|
||||
"keep-alive": true,
|
||||
"proxy-authenticate": true,
|
||||
"proxy-authorization": true,
|
||||
"te": true,
|
||||
"trailer": true,
|
||||
"transfer-encoding": true,
|
||||
"upgrade": true,
|
||||
}
|
||||
|
||||
func unsupportedHeader(key string) bool {
|
||||
return key[0] == ':' || strings.HasPrefix(key, "grpc-") || unsupportedHeaders[key]
|
||||
}
|
||||
|
||||
func parseHeaders(headers []header) ([]*v3rbacpb.Permission, error) {
|
||||
hs := make([]*v3rbacpb.Permission, 0, len(headers))
|
||||
for i, header := range headers {
|
||||
if header.Key == "" {
|
||||
return nil, fmt.Errorf(`"headers" %d: "key" is not present`, i)
|
||||
}
|
||||
header.Key = strings.ToLower(header.Key)
|
||||
if unsupportedHeader(header.Key) {
|
||||
return nil, fmt.Errorf(`"headers" %d: unsupported "key" %s`, i, header.Key)
|
||||
}
|
||||
if len(header.Values) == 0 {
|
||||
return nil, fmt.Errorf(`"headers" %d: "values" is not present`, i)
|
||||
}
|
||||
values := parseHeaderValues(header.Key, header.Values)
|
||||
hs = append(hs, permissionOr(values))
|
||||
}
|
||||
return hs, nil
|
||||
}
|
||||
|
||||
func parseRequest(request request) (*v3rbacpb.Permission, error) {
|
||||
var and []*v3rbacpb.Permission
|
||||
if len(request.Paths) > 0 {
|
||||
and = append(and, permissionOr(parsePaths(request.Paths)))
|
||||
}
|
||||
if len(request.Headers) > 0 {
|
||||
headers, err := parseHeaders(request.Headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
and = append(and, permissionAnd(headers))
|
||||
}
|
||||
if len(and) > 0 {
|
||||
return permissionAnd(and), nil
|
||||
}
|
||||
return &v3rbacpb.Permission{
|
||||
Rule: &v3rbacpb.Permission_Any{
|
||||
Any: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseRules(rules []rule, prefixName string) (map[string]*v3rbacpb.Policy, error) {
|
||||
policies := make(map[string]*v3rbacpb.Policy)
|
||||
for i, rule := range rules {
|
||||
if rule.Name == "" {
|
||||
return policies, fmt.Errorf(`%d: "name" is not present`, i)
|
||||
}
|
||||
permission, err := parseRequest(rule.Request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%d: %v", i, err)
|
||||
}
|
||||
policyName := prefixName + "_" + rule.Name
|
||||
policies[policyName] = &v3rbacpb.Policy{
|
||||
Principals: []*v3rbacpb.Principal{parsePeer(rule.Source)},
|
||||
Permissions: []*v3rbacpb.Permission{permission},
|
||||
}
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// Parse auditLoggingOptions to the associated RBAC protos. The single
|
||||
// auditLoggingOptions results in two different parsed protos, one for the allow
|
||||
// policy and one for the deny policy
|
||||
func (options *auditLoggingOptions) toProtos() (allow *v3rbacpb.RBAC_AuditLoggingOptions, deny *v3rbacpb.RBAC_AuditLoggingOptions, err error) {
|
||||
allow = &v3rbacpb.RBAC_AuditLoggingOptions{}
|
||||
deny = &v3rbacpb.RBAC_AuditLoggingOptions{}
|
||||
|
||||
if options.AuditCondition != "" {
|
||||
rbacCondition, ok := v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition_value[options.AuditCondition]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("failed to parse AuditCondition %v. Allowed values {NONE, ON_DENY, ON_ALLOW, ON_DENY_AND_ALLOW}", options.AuditCondition)
|
||||
}
|
||||
allow.AuditCondition = v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition)
|
||||
deny.AuditCondition = toDenyCondition(v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition))
|
||||
}
|
||||
|
||||
for i, config := range options.AuditLoggers {
|
||||
if config.Name == "" {
|
||||
return nil, nil, fmt.Errorf("missing required field: name in audit_logging_options.audit_loggers[%v]", i)
|
||||
}
|
||||
if config.Config == nil {
|
||||
config.Config = &structpb.Struct{}
|
||||
}
|
||||
typedStruct := &v1xdsudpatypepb.TypedStruct{
|
||||
TypeUrl: typeURLPrefix + config.Name,
|
||||
Value: config.Config,
|
||||
}
|
||||
customConfig, err := anypb.New(typedStruct)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error parsing custom audit logger config: %v", err)
|
||||
}
|
||||
|
||||
logger := &v3corepb.TypedExtensionConfig{Name: config.Name, TypedConfig: customConfig}
|
||||
rbacConfig := v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
|
||||
IsOptional: config.IsOptional,
|
||||
AuditLogger: logger,
|
||||
}
|
||||
allow.LoggerConfigs = append(allow.LoggerConfigs, &rbacConfig)
|
||||
deny.LoggerConfigs = append(deny.LoggerConfigs, &rbacConfig)
|
||||
}
|
||||
|
||||
return allow, deny, nil
|
||||
}
|
||||
|
||||
// Maps the AuditCondition coming from AuditLoggingOptions to the proper
|
||||
// condition for the deny policy RBAC proto
|
||||
func toDenyCondition(condition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition) v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition {
|
||||
// Mapping the overall policy AuditCondition to what it must be for the Deny and Allow RBAC
|
||||
// See gRPC A59 for details - https://github.com/grpc/proposal/pull/346/files
|
||||
// |Authorization Policy |DENY RBAC |ALLOW RBAC |
|
||||
// |----------------------|-------------------|---------------------|
|
||||
// |NONE |NONE |NONE |
|
||||
// |ON_DENY |ON_DENY |ON_DENY |
|
||||
// |ON_ALLOW |NONE |ON_ALLOW |
|
||||
// |ON_DENY_AND_ALLOW |ON_DENY |ON_DENY_AND_ALLOW |
|
||||
switch condition {
|
||||
case v3rbacpb.RBAC_AuditLoggingOptions_NONE:
|
||||
return v3rbacpb.RBAC_AuditLoggingOptions_NONE
|
||||
case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY:
|
||||
return v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY
|
||||
case v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW:
|
||||
return v3rbacpb.RBAC_AuditLoggingOptions_NONE
|
||||
case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY_AND_ALLOW:
|
||||
return v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY
|
||||
default:
|
||||
return v3rbacpb.RBAC_AuditLoggingOptions_NONE
|
||||
}
|
||||
}
|
||||
|
||||
// translatePolicy translates SDK authorization policy in JSON format to two
|
||||
// Envoy RBAC polices (deny followed by allow policy) or only one Envoy RBAC
|
||||
// allow policy. Also returns the overall policy name. If the input policy
|
||||
// cannot be parsed or is invalid, an error will be returned.
|
||||
func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, string, error) {
|
||||
policy := &authorizationPolicy{}
|
||||
d := json.NewDecoder(bytes.NewReader([]byte(policyStr)))
|
||||
d.DisallowUnknownFields()
|
||||
if err := d.Decode(policy); err != nil {
|
||||
return nil, "", fmt.Errorf("failed to unmarshal policy: %v", err)
|
||||
}
|
||||
if policy.Name == "" {
|
||||
return nil, "", fmt.Errorf(`"name" is not present`)
|
||||
}
|
||||
if len(policy.AllowRules) == 0 {
|
||||
return nil, "", fmt.Errorf(`"allow_rules" is not present`)
|
||||
}
|
||||
allowLogger, denyLogger, err := policy.AuditLoggingOptions.toProtos()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
rbacs := make([]*v3rbacpb.RBAC, 0, 2)
|
||||
if len(policy.DenyRules) > 0 {
|
||||
denyPolicies, err := parseRules(policy.DenyRules, policy.Name)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf(`"deny_rules" %v`, err)
|
||||
}
|
||||
denyRBAC := &v3rbacpb.RBAC{
|
||||
Action: v3rbacpb.RBAC_DENY,
|
||||
Policies: denyPolicies,
|
||||
AuditLoggingOptions: denyLogger,
|
||||
}
|
||||
rbacs = append(rbacs, denyRBAC)
|
||||
}
|
||||
allowPolicies, err := parseRules(policy.AllowRules, policy.Name)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf(`"allow_rules" %v`, err)
|
||||
}
|
||||
allowRBAC := &v3rbacpb.RBAC{Action: v3rbacpb.RBAC_ALLOW, Policies: allowPolicies, AuditLoggingOptions: allowLogger}
|
||||
return append(rbacs, allowRBAC), policy.Name, nil
|
||||
}
|
||||
1052
authz/rbac_translator_test.go
Normal file
1052
authz/rbac_translator_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user