/*
 * 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 any) (ref.Val, *cel.EvalDetails, error) {
	return fake.out, nil, fake.err
}

func (fake fakeProgram) ContextEval(ctx context.Context, vars any) (ref.Val, *cel.EvalDetails, error) {
	return fake.Eval(vars)
}

type valMock struct {
	val any
}

func (mock valMock) ConvertToNative(typeDesc reflect.Type) (any, 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() any {
	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)
			}
		})
	}
}
