1/*
2 * Copyright 2021 gRPC authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package rbac
18
19import (
20	"context"
21	"crypto/tls"
22	"crypto/x509"
23	"crypto/x509/pkix"
24	"net"
25	"net/url"
26	"testing"
27
28	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
29	v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
30	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
31	v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
32	v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
33	wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
34	"google.golang.org/grpc"
35	"google.golang.org/grpc/codes"
36	"google.golang.org/grpc/credentials"
37	"google.golang.org/grpc/internal/grpctest"
38	"google.golang.org/grpc/metadata"
39	"google.golang.org/grpc/peer"
40	"google.golang.org/grpc/status"
41)
42
43type s struct {
44	grpctest.Tester
45}
46
47func Test(t *testing.T) {
48	grpctest.RunSubTests(t, s{})
49}
50
51type addr struct {
52	ipAddress string
53}
54
55func (addr) Network() string   { return "" }
56func (a *addr) String() string { return a.ipAddress }
57
58// TestNewChainEngine tests the construction of the ChainEngine. Due to some
59// types of RBAC configuration being logically wrong and returning an error
60// rather than successfully constructing the RBAC Engine, this test tests both
61// RBAC Configurations deemed successful and also RBAC Configurations that will
62// raise errors.
63func (s) TestNewChainEngine(t *testing.T) {
64	tests := []struct {
65		name     string
66		policies []*v3rbacpb.RBAC
67		wantErr  bool
68	}{
69		{
70			name: "SuccessCaseAnyMatchSingular",
71			policies: []*v3rbacpb.RBAC{
72				{
73					Action: v3rbacpb.RBAC_ALLOW,
74					Policies: map[string]*v3rbacpb.Policy{
75						"anyone": {
76							Permissions: []*v3rbacpb.Permission{
77								{Rule: &v3rbacpb.Permission_Any{Any: true}},
78							},
79							Principals: []*v3rbacpb.Principal{
80								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
81							},
82						},
83					},
84				},
85			},
86		},
87		{
88			name: "SuccessCaseAnyMatchMultiple",
89			policies: []*v3rbacpb.RBAC{
90				{
91					Action: v3rbacpb.RBAC_ALLOW,
92					Policies: map[string]*v3rbacpb.Policy{
93						"anyone": {
94							Permissions: []*v3rbacpb.Permission{
95								{Rule: &v3rbacpb.Permission_Any{Any: true}},
96							},
97							Principals: []*v3rbacpb.Principal{
98								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
99							},
100						},
101					},
102				},
103				{
104					Action: v3rbacpb.RBAC_DENY,
105					Policies: map[string]*v3rbacpb.Policy{
106						"anyone": {
107							Permissions: []*v3rbacpb.Permission{
108								{Rule: &v3rbacpb.Permission_Any{Any: true}},
109							},
110							Principals: []*v3rbacpb.Principal{
111								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
112							},
113						},
114					},
115				},
116			},
117		},
118		{
119			name: "SuccessCaseSimplePolicySingular",
120			policies: []*v3rbacpb.RBAC{
121				{
122					Action: v3rbacpb.RBAC_ALLOW,
123					Policies: map[string]*v3rbacpb.Policy{
124						"localhost-fan": {
125							Permissions: []*v3rbacpb.Permission{
126								{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 8080}},
127								{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
128							},
129							Principals: []*v3rbacpb.Principal{
130								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
131							},
132						},
133					},
134				},
135			},
136		},
137		// SuccessCaseSimplePolicyTwoPolicies tests the construction of the
138		// chained engines in the case where there are two policies in a list,
139		// one with an allow policy and one with a deny policy. A situation
140		// where two policies (allow and deny) is a very common use case for
141		// this API, and should successfully build.
142		{
143			name: "SuccessCaseSimplePolicyTwoPolicies",
144			policies: []*v3rbacpb.RBAC{
145				{
146					Action: v3rbacpb.RBAC_ALLOW,
147					Policies: map[string]*v3rbacpb.Policy{
148						"localhost-fan": {
149							Permissions: []*v3rbacpb.Permission{
150								{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 8080}},
151								{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
152							},
153							Principals: []*v3rbacpb.Principal{
154								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
155							},
156						},
157					},
158				},
159				{
160					Action: v3rbacpb.RBAC_DENY,
161					Policies: map[string]*v3rbacpb.Policy{
162						"localhost-fan": {
163							Permissions: []*v3rbacpb.Permission{
164								{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 8080}},
165								{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
166							},
167							Principals: []*v3rbacpb.Principal{
168								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
169							},
170						},
171					},
172				},
173			},
174		},
175		{
176			name: "SuccessCaseEnvoyExampleSingular",
177			policies: []*v3rbacpb.RBAC{
178				{
179					Action: v3rbacpb.RBAC_ALLOW,
180					Policies: map[string]*v3rbacpb.Policy{
181						"service-admin": {
182							Permissions: []*v3rbacpb.Permission{
183								{Rule: &v3rbacpb.Permission_Any{Any: true}},
184							},
185							Principals: []*v3rbacpb.Principal{
186								{Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "cluster.local/ns/default/sa/admin"}}}}},
187								{Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "cluster.local/ns/default/sa/superuser"}}}}},
188							},
189						},
190						"product-viewer": {
191							Permissions: []*v3rbacpb.Permission{
192								{Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{
193									Rules: []*v3rbacpb.Permission{
194										{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}},
195										{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/products"}}}}}},
196										{Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{
197											Rules: []*v3rbacpb.Permission{
198												{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 80}},
199												{Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 443}},
200											},
201										},
202										},
203										},
204									},
205								},
206								},
207								},
208							},
209							Principals: []*v3rbacpb.Principal{
210								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
211							},
212						},
213					},
214				},
215			},
216		},
217		{
218			name: "SourceIpMatcherSuccessSingular",
219			policies: []*v3rbacpb.RBAC{
220				{
221					Action: v3rbacpb.RBAC_ALLOW,
222					Policies: map[string]*v3rbacpb.Policy{
223						"certain-source-ip": {
224							Permissions: []*v3rbacpb.Permission{
225								{Rule: &v3rbacpb.Permission_Any{Any: true}},
226							},
227							Principals: []*v3rbacpb.Principal{
228								{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
229							},
230						},
231					},
232				},
233			},
234		},
235		{
236			name: "SourceIpMatcherFailureSingular",
237			policies: []*v3rbacpb.RBAC{
238				{
239					Action: v3rbacpb.RBAC_ALLOW,
240					Policies: map[string]*v3rbacpb.Policy{
241						"certain-source-ip": {
242							Permissions: []*v3rbacpb.Permission{
243								{Rule: &v3rbacpb.Permission_Any{Any: true}},
244							},
245							Principals: []*v3rbacpb.Principal{
246								{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "not a correct address", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
247							},
248						},
249					},
250				},
251			},
252			wantErr: true,
253		},
254		{
255			name: "DestinationIpMatcherSuccess",
256			policies: []*v3rbacpb.RBAC{
257				{
258					Action: v3rbacpb.RBAC_ALLOW,
259					Policies: map[string]*v3rbacpb.Policy{
260						"certain-destination-ip": {
261							Permissions: []*v3rbacpb.Permission{
262								{Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
263							},
264							Principals: []*v3rbacpb.Principal{
265								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
266							},
267						},
268					},
269				},
270			},
271		},
272		{
273			name: "DestinationIpMatcherFailure",
274			policies: []*v3rbacpb.RBAC{
275				{
276					Action: v3rbacpb.RBAC_ALLOW,
277					Policies: map[string]*v3rbacpb.Policy{
278						"certain-destination-ip": {
279							Permissions: []*v3rbacpb.Permission{
280								{Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "not a correct address", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
281							},
282							Principals: []*v3rbacpb.Principal{
283								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
284							},
285						},
286					},
287				},
288			},
289			wantErr: true,
290		},
291		{
292			name: "MatcherToNotPolicy",
293			policies: []*v3rbacpb.RBAC{
294				{
295					Action: v3rbacpb.RBAC_ALLOW,
296					Policies: map[string]*v3rbacpb.Policy{
297						"not-secret-content": {
298							Permissions: []*v3rbacpb.Permission{
299								{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"}}}}}}}},
300							},
301							Principals: []*v3rbacpb.Principal{
302								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
303							},
304						},
305					},
306				},
307			},
308		},
309		{
310			name: "MatcherToNotPrinicipal",
311			policies: []*v3rbacpb.RBAC{
312				{
313					Action: v3rbacpb.RBAC_ALLOW,
314					Policies: map[string]*v3rbacpb.Policy{
315						"not-from-certain-ip": {
316							Permissions: []*v3rbacpb.Permission{
317								{Rule: &v3rbacpb.Permission_Any{Any: true}},
318							},
319							Principals: []*v3rbacpb.Principal{
320								{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)}}}}}},
321							},
322						},
323					},
324				},
325			},
326		},
327		// PrinicpalProductViewer tests the construction of a chained engine
328		// with a policy that allows any downstream to send a GET request on a
329		// certain path.
330		{
331			name: "PrincipalProductViewer",
332			policies: []*v3rbacpb.RBAC{
333				{
334					Action: v3rbacpb.RBAC_ALLOW,
335					Policies: map[string]*v3rbacpb.Policy{
336						"product-viewer": {
337							Permissions: []*v3rbacpb.Permission{
338								{Rule: &v3rbacpb.Permission_Any{Any: true}},
339							},
340							Principals: []*v3rbacpb.Principal{
341								{
342									Identifier: &v3rbacpb.Principal_AndIds{AndIds: &v3rbacpb.Principal_Set{Ids: []*v3rbacpb.Principal{
343										{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}},
344										{Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{
345											Ids: []*v3rbacpb.Principal{
346												{Identifier: &v3rbacpb.Principal_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/books"}}}}}},
347												{Identifier: &v3rbacpb.Principal_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/cars"}}}}}},
348											},
349										}}},
350									}}},
351								},
352							},
353						},
354					},
355				},
356			},
357		},
358		// Certain Headers tests the construction of a chained engine with a
359		// policy that allows any downstream to send an HTTP request with
360		// certain headers.
361		{
362			name: "CertainHeaders",
363			policies: []*v3rbacpb.RBAC{
364				{
365					Policies: map[string]*v3rbacpb.Policy{
366						"certain-headers": {
367							Permissions: []*v3rbacpb.Permission{
368								{Rule: &v3rbacpb.Permission_Any{Any: true}},
369							},
370							Principals: []*v3rbacpb.Principal{
371								{
372									Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{Ids: []*v3rbacpb.Principal{
373										{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}},
374										{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "GET"}}}}},
375										{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_RangeMatch{RangeMatch: &v3typepb.Int64Range{
376											Start: 0,
377											End:   64,
378										}}}}},
379										{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PresentMatch{PresentMatch: true}}}},
380										{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "GET"}}}},
381										{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: "GET"}}}},
382										{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ContainsMatch{ContainsMatch: "GET"}}}},
383									}}},
384								},
385							},
386						},
387					},
388				},
389			},
390		},
391		{
392			name: "LogAction",
393			policies: []*v3rbacpb.RBAC{
394				{
395					Action: v3rbacpb.RBAC_LOG,
396					Policies: map[string]*v3rbacpb.Policy{
397						"anyone": {
398							Permissions: []*v3rbacpb.Permission{
399								{Rule: &v3rbacpb.Permission_Any{Any: true}},
400							},
401							Principals: []*v3rbacpb.Principal{
402								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
403							},
404						},
405					},
406				},
407			},
408			wantErr: true,
409		},
410		{
411			name: "ActionNotSpecified",
412			policies: []*v3rbacpb.RBAC{
413				{
414					Policies: map[string]*v3rbacpb.Policy{
415						"anyone": {
416							Permissions: []*v3rbacpb.Permission{
417								{Rule: &v3rbacpb.Permission_Any{Any: true}},
418							},
419							Principals: []*v3rbacpb.Principal{
420								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
421							},
422						},
423					},
424				},
425			},
426		},
427	}
428	for _, test := range tests {
429		t.Run(test.name, func(t *testing.T) {
430			if _, err := NewChainEngine(test.policies); (err != nil) != test.wantErr {
431				t.Fatalf("NewChainEngine(%+v) returned err: %v, wantErr: %v", test.policies, err, test.wantErr)
432			}
433		})
434	}
435}
436
437// TestChainEngine tests the chain of RBAC Engines by configuring the chain of
438// engines in a certain way in different scenarios. After configuring the chain
439// of engines in a certain way, this test pings the chain of engines with
440// different types of data representing incoming RPC's (piped into a context),
441// and verifies that it works as expected.
442func (s) TestChainEngine(t *testing.T) {
443	tests := []struct {
444		name        string
445		rbacConfigs []*v3rbacpb.RBAC
446		rbacQueries []struct {
447			rpcData        *rpcData
448			wantStatusCode codes.Code
449		}
450	}{
451		// SuccessCaseAnyMatch tests a single RBAC Engine instantiated with
452		// a config with a policy with any rules for both permissions and
453		// principals, meaning that any data about incoming RPC's that the RBAC
454		// Engine is queried with should match that policy.
455		{
456			name: "SuccessCaseAnyMatch",
457			rbacConfigs: []*v3rbacpb.RBAC{
458				{
459					Policies: map[string]*v3rbacpb.Policy{
460						"anyone": {
461							Permissions: []*v3rbacpb.Permission{
462								{Rule: &v3rbacpb.Permission_Any{Any: true}},
463							},
464							Principals: []*v3rbacpb.Principal{
465								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
466							},
467						},
468					},
469				},
470			},
471			rbacQueries: []struct {
472				rpcData        *rpcData
473				wantStatusCode codes.Code
474			}{
475				{
476					rpcData: &rpcData{
477						fullMethod: "some method",
478						peerInfo: &peer.Peer{
479							Addr: &addr{ipAddress: "0.0.0.0"},
480						},
481					},
482					wantStatusCode: codes.OK,
483				},
484			},
485		},
486		// SuccessCaseSimplePolicy is a test that tests a single policy
487		// that only allows an rpc to proceed if the rpc is calling with a certain
488		// path.
489		{
490			name: "SuccessCaseSimplePolicy",
491			rbacConfigs: []*v3rbacpb.RBAC{
492				{
493					Policies: map[string]*v3rbacpb.Policy{
494						"localhost-fan": {
495							Permissions: []*v3rbacpb.Permission{
496								{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
497							},
498							Principals: []*v3rbacpb.Principal{
499								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
500							},
501						},
502					},
503				},
504			},
505			rbacQueries: []struct {
506				rpcData        *rpcData
507				wantStatusCode codes.Code
508			}{
509				// This RPC should match with the local host fan policy. Thus,
510				// this RPC should be allowed to proceed.
511				{
512					rpcData: &rpcData{
513						fullMethod: "localhost-fan-page",
514						peerInfo: &peer.Peer{
515							Addr: &addr{ipAddress: "0.0.0.0"},
516						},
517					},
518					wantStatusCode: codes.OK,
519				},
520
521				// This RPC shouldn't match with the local host fan policy. Thus,
522				// this rpc shouldn't be allowed to proceed.
523				{
524					rpcData: &rpcData{
525						peerInfo: &peer.Peer{
526							Addr: &addr{ipAddress: "0.0.0.0"},
527						},
528					},
529					wantStatusCode: codes.PermissionDenied,
530				},
531			},
532		},
533		// SuccessCaseEnvoyExample is a test based on the example provided
534		// in the EnvoyProxy docs. The RBAC Config contains two policies,
535		// service admin and product viewer, that provides an example of a real
536		// RBAC Config that might be configured for a given for a given backend
537		// service.
538		{
539			name: "SuccessCaseEnvoyExample",
540			rbacConfigs: []*v3rbacpb.RBAC{
541				{
542					Policies: map[string]*v3rbacpb.Policy{
543						"service-admin": {
544							Permissions: []*v3rbacpb.Permission{
545								{Rule: &v3rbacpb.Permission_Any{Any: true}},
546							},
547							Principals: []*v3rbacpb.Principal{
548								{Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "//cluster.local/ns/default/sa/admin"}}}}},
549								{Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "//cluster.local/ns/default/sa/superuser"}}}}},
550							},
551						},
552						"product-viewer": {
553							Permissions: []*v3rbacpb.Permission{
554								{
555									Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{
556										Rules: []*v3rbacpb.Permission{
557											{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}},
558											{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/products"}}}}}},
559										},
560									},
561									},
562								},
563							},
564							Principals: []*v3rbacpb.Principal{
565								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
566							},
567						},
568					},
569				},
570			},
571			rbacQueries: []struct {
572				rpcData        *rpcData
573				wantStatusCode codes.Code
574			}{
575				// This incoming RPC Call should match with the service admin
576				// policy.
577				{
578					rpcData: &rpcData{
579						fullMethod: "some method",
580						peerInfo: &peer.Peer{
581							Addr: &addr{ipAddress: "0.0.0.0"},
582							AuthInfo: credentials.TLSInfo{
583								State: tls.ConnectionState{
584									PeerCertificates: []*x509.Certificate{
585										{
586											URIs: []*url.URL{
587												{
588													Host: "cluster.local",
589													Path: "/ns/default/sa/admin",
590												},
591											},
592										},
593									},
594								},
595							},
596						},
597					},
598					wantStatusCode: codes.OK,
599				},
600				// These incoming RPC calls should not match any policy.
601				{
602					rpcData: &rpcData{
603						peerInfo: &peer.Peer{
604							Addr: &addr{ipAddress: "0.0.0.0"},
605						},
606					},
607					wantStatusCode: codes.PermissionDenied,
608				},
609				{
610					rpcData: &rpcData{
611						fullMethod: "get-product-list",
612						peerInfo: &peer.Peer{
613							Addr: &addr{ipAddress: "0.0.0.0"},
614						},
615					},
616					wantStatusCode: codes.PermissionDenied,
617				},
618				{
619					rpcData: &rpcData{
620						peerInfo: &peer.Peer{
621							Addr: &addr{ipAddress: "0.0.0.0"},
622							AuthInfo: credentials.TLSInfo{
623								State: tls.ConnectionState{
624									PeerCertificates: []*x509.Certificate{
625										{
626											Subject: pkix.Name{
627												CommonName: "localhost",
628											},
629										},
630									},
631								},
632							},
633						},
634					},
635					wantStatusCode: codes.PermissionDenied,
636				},
637			},
638		},
639		{
640			name: "NotMatcher",
641			rbacConfigs: []*v3rbacpb.RBAC{
642				{
643					Policies: map[string]*v3rbacpb.Policy{
644						"not-secret-content": {
645							Permissions: []*v3rbacpb.Permission{
646								{
647									Rule: &v3rbacpb.Permission_NotRule{
648										NotRule: &v3rbacpb.Permission{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/secret-content"}}}}}},
649									},
650								},
651							},
652							Principals: []*v3rbacpb.Principal{
653								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
654							},
655						},
656					},
657				},
658			},
659			rbacQueries: []struct {
660				rpcData        *rpcData
661				wantStatusCode codes.Code
662			}{
663				// This incoming RPC Call should match with the not-secret-content policy.
664				{
665					rpcData: &rpcData{
666						fullMethod: "/regular-content",
667						peerInfo: &peer.Peer{
668							Addr: &addr{ipAddress: "0.0.0.0"},
669						},
670					},
671					wantStatusCode: codes.OK,
672				},
673				// This incoming RPC Call shouldn't match with the not-secret-content-policy.
674				{
675					rpcData: &rpcData{
676						fullMethod: "/secret-content",
677						peerInfo: &peer.Peer{
678							Addr: &addr{ipAddress: "0.0.0.0"},
679						},
680					},
681					wantStatusCode: codes.PermissionDenied,
682				},
683			},
684		},
685		{
686			name: "SourceIpMatcher",
687			rbacConfigs: []*v3rbacpb.RBAC{
688				{
689					Policies: map[string]*v3rbacpb.Policy{
690						"certain-source-ip": {
691							Permissions: []*v3rbacpb.Permission{
692								{Rule: &v3rbacpb.Permission_Any{Any: true}},
693							},
694							Principals: []*v3rbacpb.Principal{
695								{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
696							},
697						},
698					},
699				},
700			},
701			rbacQueries: []struct {
702				rpcData        *rpcData
703				wantStatusCode codes.Code
704			}{
705				// This incoming RPC Call should match with the certain-source-ip policy.
706				{
707					rpcData: &rpcData{
708						peerInfo: &peer.Peer{
709							Addr: &addr{ipAddress: "0.0.0.0"},
710						},
711					},
712					wantStatusCode: codes.OK,
713				},
714				// This incoming RPC Call shouldn't match with the certain-source-ip policy.
715				{
716					rpcData: &rpcData{
717						peerInfo: &peer.Peer{
718							Addr: &addr{ipAddress: "10.0.0.0"},
719						},
720					},
721					wantStatusCode: codes.PermissionDenied,
722				},
723			},
724		},
725		{
726			name: "DestinationIpMatcher",
727			rbacConfigs: []*v3rbacpb.RBAC{
728				{
729					Policies: map[string]*v3rbacpb.Policy{
730						"certain-destination-ip": {
731							Permissions: []*v3rbacpb.Permission{
732								{Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
733							},
734							Principals: []*v3rbacpb.Principal{
735								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
736							},
737						},
738					},
739				},
740			},
741			rbacQueries: []struct {
742				rpcData        *rpcData
743				wantStatusCode codes.Code
744			}{
745				// This incoming RPC Call shouldn't match with the
746				// certain-destination-ip policy, as the test listens on local
747				// host.
748				{
749					rpcData: &rpcData{
750						peerInfo: &peer.Peer{
751							Addr: &addr{ipAddress: "10.0.0.0"},
752						},
753					},
754					wantStatusCode: codes.PermissionDenied,
755				},
756			},
757		},
758		// AllowAndDenyPolicy tests a policy with an allow (on path) and
759		// deny (on port) policy chained together. This represents how a user
760		// configured interceptor would use this, and also is a potential
761		// configuration for a dynamic xds interceptor.
762		{
763			name: "AllowAndDenyPolicy",
764			rbacConfigs: []*v3rbacpb.RBAC{
765				{
766					Policies: map[string]*v3rbacpb.Policy{
767						"certain-source-ip": {
768							Permissions: []*v3rbacpb.Permission{
769								{Rule: &v3rbacpb.Permission_Any{Any: true}},
770							},
771							Principals: []*v3rbacpb.Principal{
772								{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}},
773							},
774						},
775					},
776					Action: v3rbacpb.RBAC_ALLOW,
777				},
778				{
779					Policies: map[string]*v3rbacpb.Policy{
780						"localhost-fan": {
781							Permissions: []*v3rbacpb.Permission{
782								{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}},
783							},
784							Principals: []*v3rbacpb.Principal{
785								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
786							},
787						},
788					},
789					Action: v3rbacpb.RBAC_DENY,
790				},
791			},
792			rbacQueries: []struct {
793				rpcData        *rpcData
794				wantStatusCode codes.Code
795			}{
796				// This RPC should match with the allow policy, and shouldn't
797				// match with the deny and thus should be allowed to proceed.
798				{
799					rpcData: &rpcData{
800						peerInfo: &peer.Peer{
801							Addr: &addr{ipAddress: "0.0.0.0"},
802						},
803					},
804					wantStatusCode: codes.OK,
805				},
806				// This RPC should match with both the allow policy and deny policy
807				// and thus shouldn't be allowed to proceed as matched with deny.
808				{
809					rpcData: &rpcData{
810						fullMethod: "localhost-fan-page",
811						peerInfo: &peer.Peer{
812							Addr: &addr{ipAddress: "0.0.0.0"},
813						},
814					},
815					wantStatusCode: codes.PermissionDenied,
816				},
817				// This RPC shouldn't match with either policy, and thus
818				// shouldn't be allowed to proceed as didn't match with allow.
819				{
820					rpcData: &rpcData{
821						peerInfo: &peer.Peer{
822							Addr: &addr{ipAddress: "10.0.0.0"},
823						},
824					},
825					wantStatusCode: codes.PermissionDenied,
826				},
827				// This RPC shouldn't match with allow, match with deny, and
828				// thus shouldn't be allowed to proceed.
829				{
830					rpcData: &rpcData{
831						fullMethod: "localhost-fan-page",
832						peerInfo: &peer.Peer{
833							Addr: &addr{ipAddress: "10.0.0.0"},
834						},
835					},
836					wantStatusCode: codes.PermissionDenied,
837				},
838			},
839		},
840	}
841
842	for _, test := range tests {
843		t.Run(test.name, func(t *testing.T) {
844			// Instantiate the chainedRBACEngine with different configurations that are
845			// interesting to test and to query.
846			cre, err := NewChainEngine(test.rbacConfigs)
847			if err != nil {
848				t.Fatalf("Error constructing RBAC Engine: %v", err)
849			}
850			// Query the created chain of RBAC Engines with different args to see
851			// if the chain of RBAC Engines configured as such works as intended.
852			for _, data := range test.rbacQueries {
853				func() {
854					// Construct the context with three data points that have enough
855					// information to represent incoming RPC's. This will be how a
856					// user uses this API. A user will have to put MD, PeerInfo, and
857					// the connection the RPC is sent on in the context.
858					ctx := metadata.NewIncomingContext(context.Background(), data.rpcData.md)
859
860					// Make a TCP connection with a certain destination port. The
861					// address/port of this connection will be used to populate the
862					// destination ip/port in RPCData struct. This represents what
863					// the user of ChainEngine will have to place into
864					// context, as this is only way to get destination ip and port.
865					lis, err := net.Listen("tcp", "localhost:0")
866					if err != nil {
867						t.Fatalf("Error listening: %v", err)
868					}
869					defer lis.Close()
870					connCh := make(chan net.Conn, 1)
871					go func() {
872						conn, err := lis.Accept()
873						if err != nil {
874							t.Errorf("Error accepting connection: %v", err)
875							return
876						}
877						connCh <- conn
878					}()
879					_, err = net.Dial("tcp", lis.Addr().String())
880					if err != nil {
881						t.Fatalf("Error dialing: %v", err)
882					}
883					conn := <-connCh
884					defer conn.Close()
885					ctx = SetConnection(ctx, conn)
886					ctx = peer.NewContext(ctx, data.rpcData.peerInfo)
887					stream := &ServerTransportStreamWithMethod{
888						method: data.rpcData.fullMethod,
889					}
890
891					ctx = grpc.NewContextWithServerTransportStream(ctx, stream)
892					err = cre.IsAuthorized(ctx)
893					if gotCode := status.Code(err); gotCode != data.wantStatusCode {
894						t.Fatalf("IsAuthorized(%+v, %+v) returned (%+v), want(%+v)", ctx, data.rpcData.fullMethod, gotCode, data.wantStatusCode)
895					}
896				}()
897			}
898		})
899	}
900}
901
902type ServerTransportStreamWithMethod struct {
903	method string
904}
905
906func (sts *ServerTransportStreamWithMethod) Method() string {
907	return sts.method
908}
909
910func (sts *ServerTransportStreamWithMethod) SetHeader(md metadata.MD) error {
911	return nil
912}
913
914func (sts *ServerTransportStreamWithMethod) SendHeader(md metadata.MD) error {
915	return nil
916}
917
918func (sts *ServerTransportStreamWithMethod) SetTrailer(md metadata.MD) error {
919	return nil
920}
921