grpc-go/internal/xds/rbac/rbac_engine_test.go

1935 lines
63 KiB
Go

/*
* 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 rbac
import (
"context"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"fmt"
"net"
"net/url"
"reflect"
"testing"
"time"
v1xdsudpatypepb "github.com/cncf/xds/go/udpa/type/v1"
v3xdsxdstypepb "github.com/cncf/xds/go/xds/type/v3"
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"
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"google.golang.org/grpc"
"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/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
const defaultTestTimeout = 10 * time.Second
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
type addr struct {
ipAddress string
}
func (addr) Network() string { return "" }
func (a *addr) String() string { return a.ipAddress }
// TestNewChainEngine tests the construction of the ChainEngine. Due to some
// types of RBAC configuration being logically wrong and returning an error
// rather than successfully constructing the RBAC Engine, this test tests both
// RBAC Configurations deemed successful and also RBAC Configurations that will
// raise errors.
func (s) TestNewChainEngine(t *testing.T) {
tests := []struct {
name string
policies []*v3rbacpb.RBAC
wantErr bool
policyName string
}{
{
name: "SuccessCaseAnyMatchSingular",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
},
{
name: "SuccessCaseAnyMatchMultiple",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
{
Action: v3rbacpb.RBAC_DENY,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
},
{
name: "SuccessCaseSimplePolicySingular",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"localhost-fan": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 8080}},
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
},
// SuccessCaseSimplePolicyTwoPolicies tests the construction of the
// chained engines in the case where there are two policies in a list,
// one with an allow policy and one with a deny policy. A situation
// where two policies (allow and deny) is a very common use case for
// this API, and should successfully build.
{
name: "SuccessCaseSimplePolicyTwoPolicies",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"localhost-fan": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 8080}},
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
{
Action: v3rbacpb.RBAC_DENY,
Policies: map[string]*v3rbacpb.Policy{
"localhost-fan": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 8080}},
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
},
{
name: "SuccessCaseEnvoyExampleSingular",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"service-admin": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "cluster.local/ns/default/sa/admin"}}}}},
{Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "cluster.local/ns/default/sa/superuser"}}}}},
},
},
"product-viewer": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{
Rules: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}},
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/products"}}}}}},
{Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{
Rules: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 80}},
{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 443}},
},
},
},
},
},
},
},
},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
},
{
name: "SourceIpMatcherSuccessSingular",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"certain-source-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
},
},
},
},
},
{
name: "SourceIpMatcherFailureSingular",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"certain-source-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "not a correct address", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
},
},
},
},
wantErr: true,
},
{
name: "DestinationIpMatcherSuccess",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"certain-destination-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
},
{
name: "DestinationIpMatcherFailure",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"certain-destination-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "not a correct address", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
wantErr: true,
},
{
name: "MatcherToNotPolicy",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"not-secret-content": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_NotRule{NotRule: &v3rbacpb.Permission{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/secret-content"}}}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
},
{
name: "MatcherToNotPrincipal",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"not-from-certain-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_NotId{NotId: &v3rbacpb.Principal{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}}},
},
},
},
},
},
},
// PrincipalProductViewer tests the construction of a chained engine
// with a policy that allows any downstream to send a GET request on a
// certain path.
{
name: "PrincipalProductViewer",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"product-viewer": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{
Identifier: &v3rbacpb.Principal_AndIds{AndIds: &v3rbacpb.Principal_Set{Ids: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}},
{Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{
Ids: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/books"}}}}}},
{Identifier: &v3rbacpb.Principal_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/cars"}}}}}},
},
}}},
}}},
},
},
},
},
},
},
},
// Certain Headers tests the construction of a chained engine with a
// policy that allows any downstream to send an HTTP request with
// certain headers.
{
name: "CertainHeaders",
policies: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"certain-headers": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{
Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{Ids: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}},
{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "GET"}}}}},
{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_RangeMatch{RangeMatch: &v3typepb.Int64Range{
Start: 0,
End: 64,
}}}}},
{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PresentMatch{PresentMatch: true}}}},
{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "GET"}}}},
{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: "GET"}}}},
{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ContainsMatch{ContainsMatch: "GET"}}}},
{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ContainsMatch{ContainsMatch: "GET"}}}},
}}},
},
},
},
},
},
},
},
{
name: "LogAction",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_LOG,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
wantErr: true,
},
{
name: "ActionNotSpecified",
policies: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
},
{
name: "SimpleAuditLogger",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "SimpleAuditLogger_TestAuditLoggerBuffer")},
IsOptional: false,
},
},
},
},
},
},
{
name: "AuditLoggerCustomConfig",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerCustomConfig",
TypedConfig: createUDPATypedStruct(t, map[string]any{"abc": 123, "xyz": "123"}, "AuditLoggerCustomConfig_TestAuditLoggerCustomConfig")},
IsOptional: false,
},
},
},
},
},
policyName: "test_policy",
},
{
name: "AuditLoggerCustomConfigXdsTypedStruct",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerCustomConfig",
TypedConfig: createXDSTypedStruct(t, map[string]any{"abc": 123, "xyz": "123"}, "AuditLoggerCustomConfigXdsTypedStruct_TestAuditLoggerCustomConfig")},
IsOptional: false,
},
},
},
},
},
policyName: "test_policy",
},
{
name: "Missing Optional AuditLogger doesn't fail",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "UnsupportedLogger",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "Missing Optional AuditLogger doesn't fail_UnsupportedLogger")},
IsOptional: true,
},
},
},
},
},
},
{
name: "Missing Non-Optional AuditLogger fails",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "UnsupportedLogger",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "Missing Non-Optional AuditLogger fails_UnsupportedLogger")},
IsOptional: false,
},
},
},
},
},
wantErr: true,
},
{
name: "Cannot_parse_missing_CustomConfig",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerCustomConfig",
},
IsOptional: false,
},
},
},
},
},
wantErr: true,
},
{
name: "Cannot_parse_bad_CustomConfig",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerCustomConfig",
TypedConfig: createUDPATypedStruct(t, map[string]any{"abc": "BADVALUE", "xyz": "123"}, "Cannot_parse_bad_CustomConfig_TestAuditLoggerCustomConfig")},
IsOptional: false,
},
},
},
},
},
wantErr: true,
},
{
name: "Cannot_parse_missing_typedConfig_name",
policies: []*v3rbacpb.RBAC{
{
Action: v3rbacpb.RBAC_ALLOW,
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerCustomConfig",
TypedConfig: createUDPATypedStruct(t, map[string]any{"abc": 123, "xyz": "123"}, "")},
IsOptional: false,
},
},
},
},
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
b := TestAuditLoggerBufferBuilder{testName: test.name}
audit.RegisterLoggerBuilder(&b)
b2 := TestAuditLoggerCustomConfigBuilder{testName: test.name}
audit.RegisterLoggerBuilder(&b2)
if _, err := NewChainEngine(test.policies, test.policyName); (err != nil) != test.wantErr {
t.Fatalf("NewChainEngine(%+v) returned err: %v, wantErr: %v", test.policies, err, test.wantErr)
}
})
}
}
type rbacQuery struct {
rpcData *rpcData
wantStatusCode codes.Code
wantAuditEvents []*audit.Event
}
// TestChainEngine tests the chain of RBAC Engines by configuring the chain of
// engines in a certain way in different scenarios. After configuring the chain
// of engines in a certain way, this test pings the chain of engines with
// different types of data representing incoming RPC's (piped into a context),
// and verifies that it works as expected.
func (s) TestChainEngine(t *testing.T) {
defer func(gc func(ctx context.Context) net.Conn) {
getConnection = gc
}(getConnection)
tests := []struct {
name string
rbacConfigs []*v3rbacpb.RBAC
rbacQueries []rbacQuery
policyName string
}{
// SuccessCaseAnyMatch tests a single RBAC Engine instantiated with
// a config with a policy with any rules for both permissions and
// principals, meaning that any data about incoming RPC's that the RBAC
// Engine is queried with should match that policy.
{
name: "SuccessCaseAnyMatch",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"anyone": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
rbacQueries: []rbacQuery{
{
rpcData: &rpcData{
fullMethod: "some method",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
},
},
// SuccessCaseSimplePolicy is a test that tests a single policy
// that only allows an rpc to proceed if the rpc is calling with a certain
// path.
{
name: "SuccessCaseSimplePolicy",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"localhost-fan": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
rbacQueries: []rbacQuery{
// This RPC should match with the local host fan policy. Thus,
// this RPC should be allowed to proceed.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
// This RPC shouldn't match with the local host fan policy. Thus,
// this rpc shouldn't be allowed to proceed.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
// SuccessCaseEnvoyExample is a test based on the example provided
// in the EnvoyProxy docs. The RBAC Config contains two policies,
// service admin and product viewer, that provides an example of a real
// RBAC Config that might be configured for a given for a given backend
// service.
{
name: "SuccessCaseEnvoyExample",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"service-admin": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "//cluster.local/ns/default/sa/admin"}}}}},
{Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "//cluster.local/ns/default/sa/superuser"}}}}},
},
},
"product-viewer": {
Permissions: []*v3rbacpb.Permission{
{
Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{
Rules: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}},
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/products"}}}}}},
},
},
},
},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
rbacQueries: []rbacQuery{
// This incoming RPC Call should match with the service admin
// policy.
{
rpcData: &rpcData{
fullMethod: "some method",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
AuthInfo: credentials.TLSInfo{
State: tls.ConnectionState{
PeerCertificates: []*x509.Certificate{
{
URIs: []*url.URL{
{
Host: "cluster.local",
Path: "/ns/default/sa/admin",
},
},
},
},
},
},
},
},
wantStatusCode: codes.OK,
},
// These incoming RPC calls should not match any policy.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
{
rpcData: &rpcData{
fullMethod: "get-product-list",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
AuthInfo: credentials.TLSInfo{
State: tls.ConnectionState{
PeerCertificates: []*x509.Certificate{
{
Subject: pkix.Name{
CommonName: "localhost",
},
},
},
},
},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
{
name: "NotMatcher",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"not-secret-content": {
Permissions: []*v3rbacpb.Permission{
{
Rule: &v3rbacpb.Permission_NotRule{
NotRule: &v3rbacpb.Permission{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/secret-content"}}}}}},
},
},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
rbacQueries: []rbacQuery{
// This incoming RPC Call should match with the not-secret-content policy.
{
rpcData: &rpcData{
fullMethod: "/regular-content",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
// This incoming RPC Call shouldn't match with the not-secret-content-policy.
{
rpcData: &rpcData{
fullMethod: "/secret-content",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
{
name: "DirectRemoteIpMatcher",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"certain-direct-remote-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
},
},
},
},
rbacQueries: []rbacQuery{
// This incoming RPC Call should match with the certain-direct-remote-ip policy.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
// This incoming RPC Call shouldn't match with the certain-direct-remote-ip policy.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
// This test tests a RBAC policy configured with a remote-ip policy.
// This should be logically equivalent to configuring a Engine with a
// direct-remote-ip policy, as per A41 - "allow equating RBAC's
// direct_remote_ip and remote_ip."
{
name: "RemoteIpMatcher",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"certain-remote-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_RemoteIp{RemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
},
},
},
},
rbacQueries: []rbacQuery{
// This incoming RPC Call should match with the certain-remote-ip policy.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
// This incoming RPC Call shouldn't match with the certain-remote-ip policy.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
{
name: "DestinationIpMatcher",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"certain-destination-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
rbacQueries: []rbacQuery{
// This incoming RPC Call shouldn't match with the
// certain-destination-ip policy, as the test listens on local
// host.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
// AllowAndDenyPolicy tests a policy with an allow (on path) and
// deny (on port) policy chained together. This represents how a user
// configured interceptor would use this, and also is a potential
// configuration for a dynamic xds interceptor.
{
name: "AllowAndDenyPolicy",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"certain-source-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
},
},
Action: v3rbacpb.RBAC_ALLOW,
},
{
Policies: map[string]*v3rbacpb.Policy{
"localhost-fan": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
Action: v3rbacpb.RBAC_DENY,
},
},
rbacQueries: []rbacQuery{
// This RPC should match with the allow policy, and shouldn't
// match with the deny and thus should be allowed to proceed.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
// This RPC should match with both the allow policy and deny policy
// and thus shouldn't be allowed to proceed as matched with deny.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
// This RPC shouldn't match with either policy, and thus
// shouldn't be allowed to proceed as didn't match with allow.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
// This RPC shouldn't match with allow, match with deny, and
// thus shouldn't be allowed to proceed.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
// This test tests that when there are no SANs or Subject's
// distinguished name in incoming RPC's, that authenticated matchers
// match against the empty string.
{
name: "default-matching-no-credentials",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"service-admin": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: ""}}}}},
},
},
},
},
},
rbacQueries: []rbacQuery{
// This incoming RPC Call should match with the service admin
// policy. No authentication info is provided, so the
// authenticated matcher should match to the string matcher on
// the empty string, matching to the service-admin policy.
{
rpcData: &rpcData{
fullMethod: "some method",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
AuthInfo: credentials.TLSInfo{
State: tls.ConnectionState{
PeerCertificates: []*x509.Certificate{
{
URIs: []*url.URL{
{
Host: "cluster.local",
Path: "/ns/default/sa/admin",
},
},
},
},
},
},
},
},
wantStatusCode: codes.OK,
},
},
},
// This test tests that an RBAC policy configured with a metadata
// matcher as a permission doesn't match with any incoming RPC.
{
name: "metadata-never-matches",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"metadata-never-matches": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Metadata{
Metadata: &v3matcherpb.MetadataMatcher{},
}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
rbacQueries: []rbacQuery{
{
rpcData: &rpcData{
fullMethod: "some method",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
// This test tests that an RBAC policy configured with a metadata
// matcher with invert set to true as a permission always matches with
// any incoming RPC.
{
name: "metadata-invert-always-matches",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"metadata-invert-always-matches": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Metadata{
Metadata: &v3matcherpb.MetadataMatcher{Invert: true},
}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
},
},
rbacQueries: []rbacQuery{
{
rpcData: &rpcData{
fullMethod: "some method",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
},
},
// AllowAndDenyPolicy tests a policy with an allow (on path) and
// deny (on port) policy chained together. This represents how a user
// configured interceptor would use this, and also is a potential
// configuration for a dynamic xds interceptor. Further, it tests that
// the audit logger works properly in each scenario.
{
name: "AuditLoggingAllowAndDenyPolicy_ON_ALLOW",
policyName: "test_policy",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"localhost-fan": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
Action: v3rbacpb.RBAC_DENY,
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_NONE,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "AuditLoggingAllowAndDenyPolicy_ON_ALLOW_TestAuditLoggerBuffer")},
IsOptional: false,
},
},
},
},
{
Policies: map[string]*v3rbacpb.Policy{
"certain-source-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
},
},
Action: v3rbacpb.RBAC_ALLOW,
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "AuditLoggingAllowAndDenyPolicy_ON_ALLOW_TestAuditLoggerBuffer")},
IsOptional: false,
},
},
},
},
},
rbacQueries: []rbacQuery{
// This RPC should match with the allow policy, and shouldn't
// match with the deny and thus should be allowed to proceed.
{
rpcData: &rpcData{
fullMethod: "",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
AuthInfo: credentials.TLSInfo{
State: tls.ConnectionState{
PeerCertificates: []*x509.Certificate{
{
URIs: []*url.URL{
{
Scheme: "spiffe",
Host: "cluster.local",
Path: "/ns/default/sa/admin",
},
},
},
},
},
SPIFFEID: &url.URL{
Scheme: "spiffe",
Host: "cluster.local",
Path: "/ns/default/sa/admin",
},
},
},
},
wantStatusCode: codes.OK,
wantAuditEvents: []*audit.Event{
{
FullMethodName: "",
Principal: "spiffe://cluster.local/ns/default/sa/admin",
PolicyName: "test_policy",
MatchedRule: "certain-source-ip",
Authorized: true,
},
},
},
// This RPC should match with both the allow policy and deny policy
// and thus shouldn't be allowed to proceed as matched with deny.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
// This RPC shouldn't match with either policy, and thus
// shouldn't be allowed to proceed as didn't match with allow.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
// This RPC shouldn't match with allow, match with deny, and
// thus shouldn't be allowed to proceed.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
{
name: "AuditLoggingAllowAndDenyPolicy_ON_DENY",
policyName: "test_policy",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"localhost-fan": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
Action: v3rbacpb.RBAC_DENY,
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "AuditLoggingAllowAndDenyPolicy_ON_DENY_TestAuditLoggerBuffer")},
IsOptional: false,
},
},
},
},
{
Policies: map[string]*v3rbacpb.Policy{
"certain-source-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
},
},
Action: v3rbacpb.RBAC_ALLOW,
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "AuditLoggingAllowAndDenyPolicy_ON_DENY_TestAuditLoggerBuffer")},
IsOptional: false,
},
},
},
},
},
rbacQueries: []rbacQuery{
// This RPC should match with the allow policy, and shouldn't
// match with the deny and thus should be allowed to proceed.
// Audit logging matches with nothing.
{
rpcData: &rpcData{
fullMethod: "",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
// This RPC should match with both the allow policy and deny policy
// and thus shouldn't be allowed to proceed as matched with deny.
// Audit logging matches with deny and short circuits.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
AuthInfo: credentials.TLSInfo{
State: tls.ConnectionState{
PeerCertificates: []*x509.Certificate{
{
URIs: []*url.URL{
{
Host: "cluster.local",
Path: "/ns/default/sa/admin",
},
},
},
},
},
},
},
},
wantStatusCode: codes.PermissionDenied,
wantAuditEvents: []*audit.Event{
{
FullMethodName: "localhost-fan-page",
PolicyName: "test_policy",
MatchedRule: "localhost-fan",
Authorized: false,
},
},
},
// This RPC shouldn't match with either policy, and thus
// shouldn't be allowed to proceed as didn't match with allow.
// Audit logging matches with the allow policy.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
wantAuditEvents: []*audit.Event{
{
FullMethodName: "",
PolicyName: "test_policy",
MatchedRule: "",
Authorized: false,
},
},
},
// This RPC shouldn't match with allow, match with deny, and
// thus shouldn't be allowed to proceed.
// Audit logging will have the deny logged.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
wantAuditEvents: []*audit.Event{
{
FullMethodName: "localhost-fan-page",
PolicyName: "test_policy",
MatchedRule: "localhost-fan",
Authorized: false,
},
},
},
},
},
{
name: "AuditLoggingAllowAndDenyPolicy_NONE",
policyName: "test_policy",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"localhost-fan": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
Action: v3rbacpb.RBAC_DENY,
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_NONE,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "AuditLoggingAllowAndDenyPolicy_NONE_TestAuditLoggerBuffer")},
IsOptional: false,
},
},
},
},
{
Policies: map[string]*v3rbacpb.Policy{
"certain-source-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
},
},
Action: v3rbacpb.RBAC_ALLOW,
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_NONE,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "AuditLoggingAllowAndDenyPolicy_NONE_TestAuditLoggerBuffer")},
IsOptional: false,
},
},
},
},
},
rbacQueries: []rbacQuery{
// This RPC should match with the allow policy, and shouldn't
// match with the deny and thus should be allowed to proceed.
// Audit logging is NONE.
{
rpcData: &rpcData{
fullMethod: "",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
// This RPC should match with both the allow policy and deny policy
// and thus shouldn't be allowed to proceed as matched with deny.
// Audit logging is NONE.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
// This RPC shouldn't match with either policy, and thus
// shouldn't be allowed to proceed as didn't match with allow.
// Audit logging is NONE.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
// This RPC shouldn't match with allow, match with deny, and
// thus shouldn't be allowed to proceed.
// Audit logging is NONE.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
},
},
},
{
name: "AuditLoggingAllowAndDenyPolicy_ON_DENY_AND_ALLOW",
policyName: "test_policy",
rbacConfigs: []*v3rbacpb.RBAC{
{
Policies: map[string]*v3rbacpb.Policy{
"localhost-fan": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_Any{Any: true}},
},
},
},
Action: v3rbacpb.RBAC_DENY,
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "AuditLoggingAllowAndDenyPolicy_ON_DENY_AND_ALLOW_TestAuditLoggerBuffer")},
IsOptional: false,
},
},
},
},
{
Policies: map[string]*v3rbacpb.Policy{
"certain-source-ip": {
Permissions: []*v3rbacpb.Permission{
{Rule: &v3rbacpb.Permission_Any{Any: true}},
},
Principals: []*v3rbacpb.Principal{
{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
},
},
},
Action: v3rbacpb.RBAC_ALLOW,
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY_AND_ALLOW,
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
{AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "AuditLoggingAllowAndDenyPolicy_ON_DENY_AND_ALLOW_TestAuditLoggerBuffer")},
IsOptional: false,
},
},
},
},
},
rbacQueries: []rbacQuery{
// This RPC should match with the allow policy, and shouldn't
// match with the deny and thus should be allowed to proceed.
// Audit logging matches with nothing.
{
rpcData: &rpcData{
fullMethod: "",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
wantAuditEvents: []*audit.Event{
{
FullMethodName: "",
PolicyName: "test_policy",
MatchedRule: "certain-source-ip",
Authorized: true,
},
},
},
// This RPC should match with both the allow policy and deny policy
// and thus shouldn't be allowed to proceed as matched with deny.
// Audit logging matches with deny and short circuits.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
wantAuditEvents: []*audit.Event{
{
FullMethodName: "localhost-fan-page",
PolicyName: "test_policy",
MatchedRule: "localhost-fan",
Authorized: false,
},
},
},
// This RPC shouldn't match with either policy, and thus
// shouldn't be allowed to proceed as didn't match with allow.
// Audit logging matches with the allow policy.
{
rpcData: &rpcData{
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
wantAuditEvents: []*audit.Event{
{
FullMethodName: "",
PolicyName: "test_policy",
MatchedRule: "",
Authorized: false,
},
},
},
// This RPC shouldn't match with allow, match with deny, and
// thus shouldn't be allowed to proceed.
// Audit logging will have the deny logged.
{
rpcData: &rpcData{
fullMethod: "localhost-fan-page",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "10.0.0.0"},
},
},
wantStatusCode: codes.PermissionDenied,
wantAuditEvents: []*audit.Event{
{
FullMethodName: "localhost-fan-page",
PolicyName: "test_policy",
MatchedRule: "localhost-fan",
Authorized: false,
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
b := TestAuditLoggerBufferBuilder{testName: test.name}
audit.RegisterLoggerBuilder(&b)
b2 := TestAuditLoggerCustomConfigBuilder{testName: test.name}
audit.RegisterLoggerBuilder(&b2)
// Instantiate the chainedRBACEngine with different configurations that are
// interesting to test and to query.
cre, err := NewChainEngine(test.rbacConfigs, test.policyName)
if err != nil {
t.Fatalf("Error constructing RBAC Engine: %v", err)
}
// Query the created chain of RBAC Engines with different args to see
// if the chain of RBAC Engines configured as such works as intended.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for _, data := range test.rbacQueries {
func() {
// Construct the context with three data points that have enough
// information to represent incoming RPC's. This will be how a
// user uses this API. A user will have to put MD, PeerInfo, and
// the connection the RPC is sent on in the context.
ctx = metadata.NewIncomingContext(ctx, data.rpcData.md)
// Make a TCP connection with a certain destination port. The
// address/port of this connection will be used to populate the
// destination ip/port in RPCData struct. This represents what
// the user of ChainEngine will have to place into context,
// as this is only way to get destination ip and port.
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("Error listening: %v", err)
}
defer lis.Close()
connCh := make(chan net.Conn, 1)
go func() {
conn, err := lis.Accept()
if err != nil {
t.Errorf("Error accepting connection: %v", err)
return
}
connCh <- conn
}()
_, err = net.Dial("tcp", lis.Addr().String())
if err != nil {
t.Fatalf("Error dialing: %v", err)
}
conn := <-connCh
defer conn.Close()
getConnection = func(context.Context) net.Conn {
return conn
}
ctx = peer.NewContext(ctx, data.rpcData.peerInfo)
stream := &ServerTransportStreamWithMethod{
method: data.rpcData.fullMethod,
}
ctx = grpc.NewContextWithServerTransportStream(ctx, stream)
err = cre.IsAuthorized(ctx)
if gotCode := status.Code(err); gotCode != data.wantStatusCode {
t.Fatalf("IsAuthorized(%+v, %+v) returned (%+v), want(%+v)", ctx, data.rpcData.fullMethod, gotCode, data.wantStatusCode)
}
if !reflect.DeepEqual(b.auditEvents, data.wantAuditEvents) {
t.Fatalf("Unexpected audit event for query:%v", data)
}
// This builder's auditEvents can be shared for several queries, make sure it's empty.
b.auditEvents = nil
}()
}
})
}
}
type ServerTransportStreamWithMethod struct {
method string
}
func (sts *ServerTransportStreamWithMethod) Method() string {
return sts.method
}
func (sts *ServerTransportStreamWithMethod) SetHeader(md metadata.MD) error {
return nil
}
func (sts *ServerTransportStreamWithMethod) SendHeader(md metadata.MD) error {
return nil
}
func (sts *ServerTransportStreamWithMethod) SetTrailer(md metadata.MD) error {
return nil
}
// An audit logger that will log to the auditEvents slice.
type TestAuditLoggerBuffer struct {
auditEvents *[]*audit.Event
}
func (logger *TestAuditLoggerBuffer) Log(e *audit.Event) {
*(logger.auditEvents) = append(*(logger.auditEvents), e)
}
// Builds TestAuditLoggerBuffer.
type TestAuditLoggerBufferBuilder struct {
auditEvents []*audit.Event
testName string
}
// The required config for TestAuditLoggerBuffer.
type TestAuditLoggerBufferConfig struct {
audit.LoggerConfig
}
func (b *TestAuditLoggerBufferBuilder) ParseLoggerConfig(configJSON json.RawMessage) (config audit.LoggerConfig, err error) {
return TestAuditLoggerBufferConfig{}, nil
}
func (b *TestAuditLoggerBufferBuilder) Build(config audit.LoggerConfig) audit.Logger {
return &TestAuditLoggerBuffer{auditEvents: &b.auditEvents}
}
func (b *TestAuditLoggerBufferBuilder) Name() string {
return b.testName + "_TestAuditLoggerBuffer"
}
// An audit logger to test using a custom config.
type TestAuditLoggerCustomConfig struct{}
func (logger *TestAuditLoggerCustomConfig) Log(*audit.Event) {}
// Build TestAuditLoggerCustomConfig. This builds a TestAuditLoggerCustomConfig
// logger that uses a custom config.
type TestAuditLoggerCustomConfigBuilder struct {
testName string
}
// The custom config for the TestAuditLoggerCustomConfig logger.
type TestAuditLoggerCustomConfigConfig struct {
audit.LoggerConfig
Abc int
Xyz string
}
// Parses TestAuditLoggerCustomConfigConfig. Hard-coded to match with it's test
// case above.
func (b TestAuditLoggerCustomConfigBuilder) ParseLoggerConfig(configJSON json.RawMessage) (audit.LoggerConfig, error) {
c := TestAuditLoggerCustomConfigConfig{}
err := json.Unmarshal(configJSON, &c)
if err != nil {
return nil, fmt.Errorf("could not parse custom config: %v", err)
}
return c, nil
}
func (b *TestAuditLoggerCustomConfigBuilder) Build(config audit.LoggerConfig) audit.Logger {
return &TestAuditLoggerCustomConfig{}
}
func (b *TestAuditLoggerCustomConfigBuilder) Name() string {
return b.testName + "_TestAuditLoggerCustomConfig"
}
// Builds custom configs for audit logger RBAC protos.
func createUDPATypedStruct(t *testing.T, in map[string]any, name string) *anypb.Any {
t.Helper()
pb, err := structpb.NewStruct(in)
if err != nil {
t.Fatalf("createUDPATypedStructFailed during structpb.NewStruct: %v", err)
}
typedURL := ""
if name != "" {
typedURL = typeURLPrefix + name
}
typedStruct := &v1xdsudpatypepb.TypedStruct{
TypeUrl: typedURL,
Value: pb,
}
customConfig, err := anypb.New(typedStruct)
if err != nil {
t.Fatalf("createUDPATypedStructFailed during anypb.New: %v", err)
}
return customConfig
}
// Builds custom configs for audit logger RBAC protos.
func createXDSTypedStruct(t *testing.T, in map[string]any, name string) *anypb.Any {
t.Helper()
pb, err := structpb.NewStruct(in)
if err != nil {
t.Fatalf("createXDSTypedStructFailed during structpb.NewStruct: %v", err)
}
typedStruct := &v3xdsxdstypepb.TypedStruct{
TypeUrl: typeURLPrefix + name,
Value: pb,
}
customConfig, err := anypb.New(typedStruct)
if err != nil {
t.Fatalf("createXDSTypedStructFailed during anypb.New: %v", err)
}
return customConfig
}