blob: 19bc4e8ca891b5b598f1da8fe3b6cc051c49c2db [file] [log] [blame]
/*
* 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"
"net"
"net/url"
"testing"
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"
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
"google.golang.org/grpc"
"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"
)
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
}{
{
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: "MatcherToNotPrinicipal",
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)}}}}}},
},
},
},
},
},
},
// PrinicpalProductViewer 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"}}}},
}}},
},
},
},
},
},
},
},
{
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}},
},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := NewChainEngine(test.policies); (err != nil) != test.wantErr {
t.Fatalf("NewChainEngine(%+v) returned err: %v, wantErr: %v", test.policies, err, test.wantErr)
}
})
}
}
// 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 []struct {
rpcData *rpcData
wantStatusCode codes.Code
}
}{
// 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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
{
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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
// 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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
// 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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
// 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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
// 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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
// 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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
// 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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
// 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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
// 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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
{
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: []struct {
rpcData *rpcData
wantStatusCode codes.Code
}{
{
rpcData: &rpcData{
fullMethod: "some method",
peerInfo: &peer.Peer{
Addr: &addr{ipAddress: "0.0.0.0"},
},
},
wantStatusCode: codes.OK,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Instantiate the chainedRBACEngine with different configurations that are
// interesting to test and to query.
cre, err := NewChainEngine(test.rbacConfigs)
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.
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(context.Background(), 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)
}
}()
}
})
}
}
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
}