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