/* * 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 }