blob: 1fcff698aa2b8a7debd1b6be3d380c6610b04c6c [file] [log] [blame]
/*
* Copyright 2020 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 engine
import (
"context"
"reflect"
"sort"
"testing"
pb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v2"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter"
"github.com/google/go-cmp/cmp"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
type s struct {
grpctest.Tester
}
type fakeProgram struct {
out ref.Val
err error
}
func (fake fakeProgram) Eval(vars interface{}) (ref.Val, *cel.EvalDetails, error) {
return fake.out, nil, fake.err
}
func (fake fakeProgram) ContextEval(ctx context.Context, vars interface{}) (ref.Val, *cel.EvalDetails, error) {
return fake.Eval(vars)
}
type valMock struct {
val interface{}
}
func (mock valMock) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
return nil, nil
}
func (mock valMock) ConvertToType(typeValue ref.Type) ref.Val {
return nil
}
func (mock valMock) Equal(other ref.Val) ref.Val {
return nil
}
func (mock valMock) Type() ref.Type {
if mock.val == true || mock.val == false {
return types.BoolType
}
return nil
}
func (mock valMock) Value() interface{} {
return mock.val
}
type addrMock struct {
addr string
}
func (mock addrMock) Network() string {
return "tcp"
}
func (mock addrMock) String() string {
return mock.addr
}
var (
emptyActivation = interpreter.EmptyActivation()
unsuccessfulProgram = fakeProgram{out: nil, err: status.Errorf(codes.InvalidArgument, "Unsuccessful program evaluation")}
errProgram = fakeProgram{out: valMock{"missing attributes"}, err: status.Errorf(codes.InvalidArgument, "Successful program evaluation to an error result -- missing attributes")}
trueProgram = fakeProgram{out: valMock{true}, err: nil}
falseProgram = fakeProgram{out: valMock{false}, err: nil}
allowMatchEngine = &policyEngine{action: pb.RBAC_ALLOW, programs: map[string]cel.Program{
"allow match policy1": unsuccessfulProgram,
"allow match policy2": trueProgram,
"allow match policy3": falseProgram,
"allow match policy4": errProgram,
}}
denyFailEngine = &policyEngine{action: pb.RBAC_DENY, programs: map[string]cel.Program{
"deny fail policy1": falseProgram,
"deny fail policy2": falseProgram,
"deny fail policy3": falseProgram,
}}
denyUnknownEngine = &policyEngine{action: pb.RBAC_DENY, programs: map[string]cel.Program{
"deny unknown policy1": falseProgram,
"deny unknown policy2": unsuccessfulProgram,
"deny unknown policy3": errProgram,
"deny unknown policy4": falseProgram,
}}
)
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func (s) TestNewAuthorizationEngine(t *testing.T) {
tests := map[string]struct {
allow *pb.RBAC
deny *pb.RBAC
wantErr bool
errStr string
}{
"too few rbacs": {
wantErr: true,
errStr: "Expected error: at least one of allow, deny must be non-nil",
},
"one rbac allow": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW},
errStr: "Expected 1 ALLOW RBAC to be successful",
},
"one rbac deny": {
deny: &pb.RBAC{Action: pb.RBAC_DENY},
errStr: "Expected 1 DENY RBAC to be successful",
},
"two rbacs": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW},
deny: &pb.RBAC{Action: pb.RBAC_DENY},
errStr: "Expected 2 RBACs (DENY + ALLOW) to be successful",
},
"wrong rbac actions": {
allow: &pb.RBAC{Action: pb.RBAC_DENY},
wantErr: true,
errStr: "Expected error: allow must have action ALLOW, deny must have action DENY",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
_, gotErr := NewAuthorizationEngine(tc.allow, tc.deny)
if (gotErr != nil) != tc.wantErr {
t.Fatal(tc.errStr)
}
})
}
}
func (s) TestGetDecision(t *testing.T) {
tests := map[string]struct {
engine *policyEngine
match bool
want Decision
}{
"ALLOW engine match": {
engine: &policyEngine{action: pb.RBAC_ALLOW, programs: map[string]cel.Program{}},
match: true,
want: DecisionAllow,
},
"ALLOW engine fail": {
engine: &policyEngine{action: pb.RBAC_ALLOW, programs: map[string]cel.Program{}},
want: DecisionDeny,
},
"DENY engine match": {
engine: &policyEngine{action: pb.RBAC_DENY, programs: map[string]cel.Program{}},
match: true,
want: DecisionDeny,
},
"DENY engine fail": {
engine: &policyEngine{action: pb.RBAC_DENY, programs: map[string]cel.Program{}},
want: DecisionAllow,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
if got := getDecision(tc.engine, tc.match); got != tc.want {
t.Fatalf("getDecision(%v, %v) = (%v), want (%v)", tc.engine, tc.match, got, tc.want)
}
})
}
}
func (s) TestPolicyEngineEvaluate(t *testing.T) {
tests := map[string]struct {
engine *policyEngine
activation interpreter.Activation
wantDecision Decision
wantPolicyNames []string
}{
"no policies": {
engine: &policyEngine{},
activation: emptyActivation,
wantDecision: DecisionDeny,
wantPolicyNames: []string{},
},
"match succeed": {
engine: allowMatchEngine,
activation: emptyActivation,
wantDecision: DecisionAllow,
wantPolicyNames: []string{"allow match policy2"},
},
"match fail": {
engine: denyFailEngine,
activation: emptyActivation,
wantDecision: DecisionAllow,
wantPolicyNames: []string{},
},
"unknown": {
engine: denyUnknownEngine,
activation: emptyActivation,
wantDecision: DecisionUnknown,
wantPolicyNames: []string{"deny unknown policy2", "deny unknown policy3"},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
gotDecision, gotPolicyNames := tc.engine.evaluate(tc.activation)
sort.Strings(gotPolicyNames)
if gotDecision != tc.wantDecision || !cmp.Equal(gotPolicyNames, tc.wantPolicyNames) {
t.Fatalf("policyEngine.evaluate(%v, %v) = (%v, %v), want (%v, %v)", tc.engine, tc.activation, gotDecision, gotPolicyNames, tc.wantDecision, tc.wantPolicyNames)
}
})
}
}
func (s) TestAuthorizationEngineEvaluate(t *testing.T) {
tests := map[string]struct {
engine *AuthorizationEngine
authArgs *AuthorizationArgs
wantAuthDecision *AuthorizationDecision
}{
"allow match": {
engine: &AuthorizationEngine{allow: allowMatchEngine},
authArgs: &AuthorizationArgs{},
wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{"allow match policy2"}},
},
"deny fail": {
engine: &AuthorizationEngine{deny: denyFailEngine},
authArgs: &AuthorizationArgs{},
wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{}},
},
"first engine unknown": {
engine: &AuthorizationEngine{allow: allowMatchEngine, deny: denyUnknownEngine},
authArgs: &AuthorizationArgs{},
wantAuthDecision: &AuthorizationDecision{decision: DecisionUnknown, policyNames: []string{"deny unknown policy2", "deny unknown policy3"}},
},
"second engine match": {
engine: &AuthorizationEngine{allow: allowMatchEngine, deny: denyFailEngine},
authArgs: &AuthorizationArgs{},
wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{"allow match policy2"}},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
gotAuthDecision, gotErr := tc.engine.Evaluate(tc.authArgs)
sort.Strings(gotAuthDecision.policyNames)
if gotErr != nil || gotAuthDecision.decision != tc.wantAuthDecision.decision || !cmp.Equal(gotAuthDecision.policyNames, tc.wantAuthDecision.policyNames) {
t.Fatalf("AuthorizationEngine.Evaluate(%v, %v) = (%v, %v), want (%v, %v)", tc.engine, tc.authArgs, gotAuthDecision, gotErr, tc.wantAuthDecision, nil)
}
})
}
}
func (s) TestIntegration(t *testing.T) {
declarations := []*expr.Decl{
decls.NewVar("request.url_path", decls.String),
decls.NewVar("request.host", decls.String),
decls.NewVar("request.method", decls.String),
decls.NewVar("request.headers", decls.NewMapType(decls.String, decls.String)),
decls.NewVar("source.address", decls.String),
decls.NewVar("source.port", decls.Int),
decls.NewVar("destination.address", decls.String),
decls.NewVar("destination.port", decls.Int),
decls.NewVar("connection.uri_san_peer_certificate", decls.String),
decls.NewVar("source.principal", decls.String),
}
tests := map[string]struct {
allow *pb.RBAC
deny *pb.RBAC
authArgs *AuthorizationArgs
wantAuthDecision *AuthorizationDecision
}{
"ALLOW engine: DecisionAllow": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW, Policies: map[string]*pb.Policy{
"url_path starts with": {Condition: compileStringToExpr("request.url_path.startsWith('/pkg.service/test')", declarations)},
}},
authArgs: &AuthorizationArgs{fullMethod: "/pkg.service/test/method"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{"url_path starts with"}},
},
"ALLOW engine: DecisionUnknown": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW, Policies: map[string]*pb.Policy{
"url_path and uri_san_peer_certificate": {Condition: compileStringToExpr("request.url_path == '/pkg.service/test' && connection.uri_san_peer_certificate == 'cluster/ns/default/sa/admin'", declarations)},
"source port": {Condition: compileStringToExpr("source.port == 8080", declarations)},
}},
authArgs: &AuthorizationArgs{peerInfo: &peer.Peer{Addr: addrMock{addr: "192.0.2.1:25"}}, fullMethod: "/pkg.service/test"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionUnknown, policyNames: []string{"url_path and uri_san_peer_certificate"}},
},
"ALLOW engine: DecisionDeny": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW, Policies: map[string]*pb.Policy{
"url_path": {Condition: compileStringToExpr("request.url_path == '/pkg.service/test'", declarations)},
}},
authArgs: &AuthorizationArgs{fullMethod: "/pkg.service/test/method"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionDeny, policyNames: []string{}},
},
"DENY engine: DecisionAllow": {
deny: &pb.RBAC{Action: pb.RBAC_DENY, Policies: map[string]*pb.Policy{
"url_path and uri_san_peer_certificate": {Condition: compileStringToExpr("request.url_path == '/pkg.service/test' && connection.uri_san_peer_certificate == 'cluster/ns/default/sa/admin'", declarations)},
}},
authArgs: &AuthorizationArgs{fullMethod: "/pkg.service/test/method"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{}},
},
"DENY engine: DecisionUnknown": {
deny: &pb.RBAC{Action: pb.RBAC_DENY, Policies: map[string]*pb.Policy{
"destination address": {Condition: compileStringToExpr("destination.address == '192.0.3.1'", declarations)},
"source port": {Condition: compileStringToExpr("source.port == 8080", declarations)},
}},
authArgs: &AuthorizationArgs{peerInfo: &peer.Peer{Addr: addrMock{addr: "192.0.2.1:25"}}, fullMethod: "/pkg.service/test"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionUnknown, policyNames: []string{"destination address"}},
},
"DENY engine: DecisionDeny": {
deny: &pb.RBAC{Action: pb.RBAC_DENY, Policies: map[string]*pb.Policy{
"destination address": {Condition: compileStringToExpr("destination.address == '192.0.3.1'", declarations)},
"source address or source port": {Condition: compileStringToExpr("source.address == '192.0.4.1' || source.port == 8080", declarations)},
}},
authArgs: &AuthorizationArgs{peerInfo: &peer.Peer{Addr: addrMock{addr: "192.0.2.1:8080"}}, fullMethod: "/pkg.service/test"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionDeny, policyNames: []string{"source address or source port"}},
},
"DENY ALLOW engine: DecisionDeny from DENY policy": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW, Policies: map[string]*pb.Policy{
"url_path starts with": {Condition: compileStringToExpr("request.url_path.startsWith('/pkg.service/test')", declarations)},
}},
deny: &pb.RBAC{Action: pb.RBAC_DENY, Policies: map[string]*pb.Policy{
"destination address": {Condition: compileStringToExpr("destination.address == '192.0.3.1'", declarations)},
"source address or source port": {Condition: compileStringToExpr("source.address == '192.0.4.1' || source.port == 8080", declarations)},
}},
authArgs: &AuthorizationArgs{peerInfo: &peer.Peer{Addr: addrMock{addr: "192.0.2.1:8080"}}, fullMethod: "/pkg.service/test"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionDeny, policyNames: []string{"source address or source port"}},
},
"DENY ALLOW engine: DecisionUnknown from DENY policy": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW, Policies: map[string]*pb.Policy{
"url_path starts with": {Condition: compileStringToExpr("request.url_path.startsWith('/pkg.service/test')", declarations)},
}},
deny: &pb.RBAC{Action: pb.RBAC_DENY, Policies: map[string]*pb.Policy{
"destination address": {Condition: compileStringToExpr("destination.address == '192.0.3.1'", declarations)},
"source port and destination port": {Condition: compileStringToExpr("source.port == 8080 && destination.port == 1234", declarations)},
}},
authArgs: &AuthorizationArgs{peerInfo: &peer.Peer{Addr: addrMock{addr: "192.0.2.1:8080"}}, fullMethod: "/pkg.service/test/method"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionUnknown, policyNames: []string{"destination address", "source port and destination port"}},
},
"DENY ALLOW engine: DecisionAllow from ALLOW policy": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW, Policies: map[string]*pb.Policy{
"method or url_path starts with": {Condition: compileStringToExpr("request.method == 'POST' || request.url_path.startsWith('/pkg.service/test')", declarations)},
}},
deny: &pb.RBAC{Action: pb.RBAC_DENY, Policies: map[string]*pb.Policy{
"source address": {Condition: compileStringToExpr("source.address == '192.0.3.1'", declarations)},
"source port and url_path": {Condition: compileStringToExpr("source.port == 8080 && request.url_path == 'pkg.service/test'", declarations)},
}},
authArgs: &AuthorizationArgs{peerInfo: &peer.Peer{Addr: addrMock{addr: "192.0.2.1:8080"}}, fullMethod: "/pkg.service/test/method"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{"method or url_path starts with"}},
},
"DENY ALLOW engine: DecisionUnknown from ALLOW policy": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW, Policies: map[string]*pb.Policy{
"url_path starts with and method": {Condition: compileStringToExpr("request.url_path.startsWith('/pkg.service/test') && request.method == 'POST'", declarations)},
}},
deny: &pb.RBAC{Action: pb.RBAC_DENY, Policies: map[string]*pb.Policy{
"source address": {Condition: compileStringToExpr("source.address == '192.0.3.1'", declarations)},
"source port and url_path": {Condition: compileStringToExpr("source.port == 8080 && request.url_path == 'pkg.service/test'", declarations)},
}},
authArgs: &AuthorizationArgs{peerInfo: &peer.Peer{Addr: addrMock{addr: "192.0.2.1:8080"}}, fullMethod: "/pkg.service/test/method"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionUnknown, policyNames: []string{"url_path starts with and method"}},
},
"DENY ALLOW engine: DecisionDeny from ALLOW policy": {
allow: &pb.RBAC{Action: pb.RBAC_ALLOW, Policies: map[string]*pb.Policy{
"url_path starts with and source port": {Condition: compileStringToExpr("request.url_path.startsWith('/pkg.service/test') && source.port == 1234", declarations)},
}},
deny: &pb.RBAC{Action: pb.RBAC_DENY, Policies: map[string]*pb.Policy{
"source address": {Condition: compileStringToExpr("source.address == '192.0.3.1'", declarations)},
"source port and url_path": {Condition: compileStringToExpr("source.port == 8080 && request.url_path == 'pkg.service/test'", declarations)},
}},
authArgs: &AuthorizationArgs{peerInfo: &peer.Peer{Addr: addrMock{addr: "192.0.2.1:8080"}}, fullMethod: "/pkg.service/test/method"},
wantAuthDecision: &AuthorizationDecision{decision: DecisionDeny, policyNames: []string{}},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
engine, err := NewAuthorizationEngine(tc.allow, tc.deny)
if err != nil {
t.Fatalf("Error constructing authorization engine: %v", err)
}
gotAuthDecision, gotErr := engine.Evaluate(tc.authArgs)
sort.Strings(gotAuthDecision.policyNames)
if gotErr != nil || gotAuthDecision.decision != tc.wantAuthDecision.decision || !cmp.Equal(gotAuthDecision.policyNames, tc.wantAuthDecision.policyNames) {
t.Fatalf("NewAuthorizationEngine(%v, %v).Evaluate(%v) = (%v, %v), want (%v, %v)", tc.allow, tc.deny, tc.authArgs, gotAuthDecision, gotErr, tc.wantAuthDecision, nil)
}
})
}
}