1/*
2 * Copyright 2020 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 engine
18
19import (
20	"reflect"
21	"sort"
22	"testing"
23
24	pb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v2"
25	cel "github.com/google/cel-go/cel"
26	"github.com/google/cel-go/common/types"
27	"github.com/google/cel-go/common/types/ref"
28	interpreter "github.com/google/cel-go/interpreter"
29	"google.golang.org/grpc/codes"
30	"google.golang.org/grpc/status"
31)
32
33type programMock struct {
34	out ref.Val
35	err error
36}
37
38func (mock programMock) Eval(vars interface{}) (ref.Val, *cel.EvalDetails, error) {
39	return mock.out, nil, mock.err
40}
41
42type valMock struct {
43	val interface{}
44}
45
46func (mock valMock) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
47	return nil, nil
48}
49
50func (mock valMock) ConvertToType(typeValue ref.Type) ref.Val {
51	return nil
52}
53
54func (mock valMock) Equal(other ref.Val) ref.Val {
55	return nil
56}
57
58func (mock valMock) Type() ref.Type {
59	if mock.val == true || mock.val == false {
60		return types.BoolType
61	}
62	return nil
63}
64
65func (mock valMock) Value() interface{} {
66	return mock.val
67}
68
69var (
70	emptyActivation     = interpreter.EmptyActivation()
71	unsuccessfulProgram = programMock{out: nil, err: status.Errorf(codes.InvalidArgument, "Unsuccessful program evaluation")}
72	errProgram          = programMock{out: valMock{"missing attributes"}, err: status.Errorf(codes.InvalidArgument, "Successful program evaluation to an error result -- missing attributes")}
73	trueProgram         = programMock{out: valMock{true}, err: nil}
74	falseProgram        = programMock{out: valMock{false}, err: nil}
75
76	allowMatchEngine = &policyEngine{action: pb.RBAC_ALLOW, programs: map[string]cel.Program{
77		"allow match policy1": unsuccessfulProgram,
78		"allow match policy2": trueProgram,
79		"allow match policy3": falseProgram,
80		"allow match policy4": errProgram,
81	}}
82	denyFailEngine = &policyEngine{action: pb.RBAC_DENY, programs: map[string]cel.Program{
83		"deny fail policy1": falseProgram,
84		"deny fail policy2": falseProgram,
85		"deny fail policy3": falseProgram,
86	}}
87	denyUnknownEngine = &policyEngine{action: pb.RBAC_DENY, programs: map[string]cel.Program{
88		"deny unknown policy1": falseProgram,
89		"deny unknown policy2": unsuccessfulProgram,
90		"deny unknown policy3": errProgram,
91		"deny unknown policy4": falseProgram,
92	}}
93)
94
95func TestNewAuthorizationEngine(t *testing.T) {
96	tests := map[string]struct {
97		allow   *pb.RBAC
98		deny    *pb.RBAC
99		wantErr string
100		errStr  string
101	}{
102		"too few rbacs": {
103			allow:   nil,
104			deny:    nil,
105			wantErr: "at least one of allow, deny must be non-nil",
106			errStr:  "Expected error: at least one of allow, deny must be non-nil",
107		},
108		"one rbac allow": {
109			allow:   &pb.RBAC{Action: pb.RBAC_ALLOW},
110			deny:    nil,
111			wantErr: "",
112			errStr:  "Expected 1 ALLOW RBAC to be successful",
113		},
114		"one rbac deny": {
115			allow:   nil,
116			deny:    &pb.RBAC{Action: pb.RBAC_DENY},
117			wantErr: "",
118			errStr:  "Expected 1 DENY RBAC to be successful",
119		},
120		"two rbacs": {
121			allow:   &pb.RBAC{Action: pb.RBAC_ALLOW},
122			deny:    &pb.RBAC{Action: pb.RBAC_DENY},
123			wantErr: "",
124			errStr:  "Expected 2 RBACs (DENY + ALLOW) to be successful",
125		},
126		"wrong rbac actions": {
127			allow:   &pb.RBAC{Action: pb.RBAC_DENY},
128			deny:    nil,
129			wantErr: "allow must have action ALLOW, deny must have action DENY",
130			errStr:  "Expected error: allow must have action ALLOW, deny must have action DENY",
131		},
132	}
133
134	for name, tc := range tests {
135		t.Run(name, func(t *testing.T) {
136			_, gotErr := NewAuthorizationEngine(tc.allow, tc.deny)
137			if tc.wantErr == "" && gotErr == nil {
138				return
139			}
140			if gotErr == nil || gotErr.Error() != tc.wantErr {
141				t.Errorf(tc.errStr)
142			}
143		})
144	}
145}
146
147func TestGetDecision(t *testing.T) {
148	tests := map[string]struct {
149		engine *policyEngine
150		match  bool
151		want   Decision
152	}{
153		"ALLOW engine match": {
154			engine: &policyEngine{action: pb.RBAC_ALLOW, programs: map[string]cel.Program{}},
155			match:  true,
156			want:   DecisionAllow,
157		},
158		"ALLOW engine fail": {
159			engine: &policyEngine{action: pb.RBAC_ALLOW, programs: map[string]cel.Program{}},
160			match:  false,
161			want:   DecisionDeny,
162		},
163		"DENY engine match": {
164			engine: &policyEngine{action: pb.RBAC_DENY, programs: map[string]cel.Program{}},
165			match:  true,
166			want:   DecisionDeny,
167		},
168		"DENY engine fail": {
169			engine: &policyEngine{action: pb.RBAC_DENY, programs: map[string]cel.Program{}},
170			match:  false,
171			want:   DecisionAllow,
172		},
173	}
174
175	for name, tc := range tests {
176		t.Run(name, func(t *testing.T) {
177			if got := getDecision(tc.engine, tc.match); got != tc.want {
178				t.Errorf("Expected %v, instead got %v", tc.want, got)
179			}
180		})
181	}
182}
183
184func TestPolicyEngineEvaluate(t *testing.T) {
185	tests := map[string]struct {
186		engine          *policyEngine
187		activation      interpreter.Activation
188		wantDecision    Decision
189		wantPolicyNames []string
190	}{
191		"no policies": {
192			engine:          &policyEngine{},
193			activation:      emptyActivation,
194			wantDecision:    DecisionDeny,
195			wantPolicyNames: []string{},
196		},
197		"match succeed": {
198			engine:          allowMatchEngine,
199			activation:      emptyActivation,
200			wantDecision:    DecisionAllow,
201			wantPolicyNames: []string{"allow match policy2"},
202		},
203		"match fail": {
204			engine:          denyFailEngine,
205			activation:      emptyActivation,
206			wantDecision:    DecisionAllow,
207			wantPolicyNames: []string{},
208		},
209		"unknown": {
210			engine:          denyUnknownEngine,
211			activation:      emptyActivation,
212			wantDecision:    DecisionUnknown,
213			wantPolicyNames: []string{"deny unknown policy2", "deny unknown policy3"},
214		},
215	}
216
217	for name, tc := range tests {
218		t.Run(name, func(t *testing.T) {
219			gotDecision, gotPolicyNames := tc.engine.evaluate(tc.activation)
220			sort.Strings(gotPolicyNames)
221			if gotDecision != tc.wantDecision || !reflect.DeepEqual(gotPolicyNames, tc.wantPolicyNames) {
222				t.Errorf("Expected (%v, %v), instead got (%v, %v)", tc.wantDecision, tc.wantPolicyNames, gotDecision, gotPolicyNames)
223			}
224		})
225	}
226}
227
228func TestAuthorizationEngineEvaluate(t *testing.T) {
229	tests := map[string]struct {
230		engine           *AuthorizationEngine
231		authArgs         *AuthorizationArgs
232		wantAuthDecision *AuthorizationDecision
233		wantErr          error
234	}{
235		"allow match": {
236			engine:           &AuthorizationEngine{allow: allowMatchEngine},
237			authArgs:         &AuthorizationArgs{},
238			wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{"allow match policy2"}},
239			wantErr:          nil,
240		},
241		"deny fail": {
242			engine:           &AuthorizationEngine{deny: denyFailEngine},
243			authArgs:         &AuthorizationArgs{},
244			wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{}},
245			wantErr:          nil,
246		},
247		"first engine unknown": {
248			engine:           &AuthorizationEngine{allow: allowMatchEngine, deny: denyUnknownEngine},
249			authArgs:         &AuthorizationArgs{},
250			wantAuthDecision: &AuthorizationDecision{decision: DecisionUnknown, policyNames: []string{"deny unknown policy2", "deny unknown policy3"}},
251			wantErr:          nil,
252		},
253		"second engine match": {
254			engine:           &AuthorizationEngine{allow: allowMatchEngine, deny: denyFailEngine},
255			authArgs:         &AuthorizationArgs{},
256			wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{"allow match policy2"}},
257			wantErr:          nil,
258		},
259	}
260
261	for name, tc := range tests {
262		t.Run(name, func(t *testing.T) {
263			gotAuthDecision, gotErr := tc.engine.Evaluate(tc.authArgs)
264			sort.Strings(gotAuthDecision.policyNames)
265			if tc.wantErr != nil && (gotErr == nil || gotErr.Error() != tc.wantErr.Error()) {
266				t.Errorf("Expected error to be %v, instead got %v", tc.wantErr, gotErr)
267			} else if tc.wantErr == nil && (gotErr != nil || gotAuthDecision.decision != tc.wantAuthDecision.decision || !reflect.DeepEqual(gotAuthDecision.policyNames, tc.wantAuthDecision.policyNames)) {
268				t.Errorf("Expected authorization decision to be (%v, %v), instead got (%v, %v)", tc.wantAuthDecision.decision, tc.wantAuthDecision.policyNames, gotAuthDecision.decision, gotAuthDecision.policyNames)
269			}
270		})
271	}
272}
273