/*
 * 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 (
	"fmt"
	"net"
	"strconv"

	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/interpreter"
	expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
	"google.golang.org/grpc/grpclog"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/peer"
	"google.golang.org/protobuf/proto"
)

var logger = grpclog.Component("authorization")

var stringAttributeMap = map[string]func(*AuthorizationArgs) (string, error){
	"request.url_path":                    (*AuthorizationArgs).getRequestURLPath,
	"request.host":                        (*AuthorizationArgs).getRequestHost,
	"request.method":                      (*AuthorizationArgs).getRequestMethod,
	"source.address":                      (*AuthorizationArgs).getSourceAddress,
	"destination.address":                 (*AuthorizationArgs).getDestinationAddress,
	"connection.uri_san_peer_certificate": (*AuthorizationArgs).getURISanPeerCertificate,
	"source.principal":                    (*AuthorizationArgs).getSourcePrincipal,
}

var intAttributeMap = map[string]func(*AuthorizationArgs) (int, error){
	"source.port":      (*AuthorizationArgs).getSourcePort,
	"destination.port": (*AuthorizationArgs).getDestinationPort,
}

// activationImpl is an implementation of interpreter.Activation.
// An Activation is the primary mechanism by which a caller supplies input into a CEL program.
type activationImpl struct {
	dict map[string]interface{}
}

// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (activation activationImpl) ResolveName(name string) (interface{}, bool) {
	result, ok := activation.dict[name]
	return result, ok
}

// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (activation activationImpl) Parent() interpreter.Activation {
	return activationImpl{}
}

// AuthorizationArgs is the input of the CEL-based authorization engine.
type AuthorizationArgs struct {
	md         metadata.MD
	peerInfo   *peer.Peer
	fullMethod string
}

// newActivation converts AuthorizationArgs into the activation for CEL.
func newActivation(args *AuthorizationArgs) interpreter.Activation {
	// Fill out evaluation map, only adding the attributes that can be extracted.
	evalMap := make(map[string]interface{})
	for key, function := range stringAttributeMap {
		val, err := function(args)
		if err == nil {
			evalMap[key] = val
		}
	}
	for key, function := range intAttributeMap {
		val, err := function(args)
		if err == nil {
			evalMap[key] = val
		}
	}
	val, err := args.getRequestHeaders()
	if err == nil {
		evalMap["request.headers"] = val
	}
	// Convert evaluation map to activation.
	return activationImpl{dict: evalMap}
}

func (args *AuthorizationArgs) getRequestURLPath() (string, error) {
	if args.fullMethod == "" {
		return "", fmt.Errorf("authorization args doesn't have a valid request url path")
	}
	return args.fullMethod, nil
}

func (args *AuthorizationArgs) getRequestHost() (string, error) {
	// TODO(@zhenlian): fill out attribute extraction for request.host
	return "", fmt.Errorf("authorization args doesn't have a valid request host")
}

func (args *AuthorizationArgs) getRequestMethod() (string, error) {
	// TODO(@zhenlian): fill out attribute extraction for request.method
	return "", fmt.Errorf("authorization args doesn't have a valid request method")
}

func (args *AuthorizationArgs) getRequestHeaders() (map[string]string, error) {
	// TODO(@zhenlian): fill out attribute extraction for request.headers
	return nil, fmt.Errorf("authorization args doesn't have valid request headers")
}

func (args *AuthorizationArgs) getSourceAddress() (string, error) {
	if args.peerInfo == nil {
		return "", fmt.Errorf("authorization args doesn't have a valid source address")
	}
	addr := args.peerInfo.Addr.String()
	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		return "", err
	}
	return host, nil
}

func (args *AuthorizationArgs) getSourcePort() (int, error) {
	if args.peerInfo == nil {
		return 0, fmt.Errorf("authorization args doesn't have a valid source port")
	}
	addr := args.peerInfo.Addr.String()
	_, port, err := net.SplitHostPort(addr)
	if err != nil {
		return 0, err
	}
	return strconv.Atoi(port)
}

func (args *AuthorizationArgs) getDestinationAddress() (string, error) {
	// TODO(@zhenlian): fill out attribute extraction for destination.address
	return "", fmt.Errorf("authorization args doesn't have a valid destination address")
}

func (args *AuthorizationArgs) getDestinationPort() (int, error) {
	// TODO(@zhenlian): fill out attribute extraction for destination.port
	return 0, fmt.Errorf("authorization args doesn't have a valid destination port")
}

func (args *AuthorizationArgs) getURISanPeerCertificate() (string, error) {
	// TODO(@zhenlian): fill out attribute extraction for connection.uri_san_peer_certificate
	return "", fmt.Errorf("authorization args doesn't have a valid URI in SAN field of the peer certificate")
}

func (args *AuthorizationArgs) getSourcePrincipal() (string, error) {
	// TODO(@zhenlian): fill out attribute extraction for source.principal
	return "", fmt.Errorf("authorization args doesn't have a valid source principal")
}

// Decision represents different authorization decisions a CEL-based
// authorization engine can return.
type Decision int32

const (
	// DecisionAllow indicates allowing the RPC to go through.
	DecisionAllow Decision = iota
	// DecisionDeny indicates denying the RPC from going through.
	DecisionDeny
	// DecisionUnknown indicates that there is insufficient information to
	// determine whether or not an RPC call is authorized.
	DecisionUnknown
)

// String returns the string representation of a Decision object.
func (d Decision) String() string {
	return [...]string{"DecisionAllow", "DecisionDeny", "DecisionUnknown"}[d]
}

// AuthorizationDecision is the output of CEL-based authorization engines.
// If decision is allow or deny, policyNames will either contain the names of
// all the policies matched in the engine that permitted the action, or be
// empty as the decision was made after all conditions evaluated to false.
// If decision is unknown, policyNames will contain the list of policies that
// evaluated to unknown.
type AuthorizationDecision struct {
	decision    Decision
	policyNames []string
}

// Converts an expression to a parsed expression, with SourceInfo nil.
func exprToParsedExpr(condition *expr.Expr) *expr.ParsedExpr {
	return &expr.ParsedExpr{Expr: condition}
}

// Converts an expression to a CEL program.
func exprToProgram(condition *expr.Expr, env *cel.Env) (cel.Program, error) {
	// Converts condition to ParsedExpr by setting SourceInfo empty.
	pexpr := exprToParsedExpr(condition)
	// pretend cel.ExprToAst exists
	ast, iss := env.Check(cel.ParsedExprToAst(pexpr))
	if iss.Err() != nil {
		return nil, iss.Err()
	}
	// Check that the expression will evaluate to a boolean.
	if !proto.Equal(ast.ResultType(), decls.Bool) {
		return nil, fmt.Errorf("expected boolean condition")
	}
	// Build the program plan.
	return env.Program(ast,
		cel.EvalOptions(cel.OptOptimize),
	)
}

// policyEngine is the struct for an engine created from one RBAC proto.
type policyEngine struct {
	action   pb.RBAC_Action
	programs map[string]cel.Program
}

// Creates a new policyEngine from an RBAC policy proto.
func newPolicyEngine(rbac *pb.RBAC, env *cel.Env) (*policyEngine, error) {
	if rbac == nil {
		return nil, nil
	}
	action := rbac.Action
	programs := make(map[string]cel.Program)
	for policyName, policy := range rbac.Policies {
		prg, err := exprToProgram(policy.Condition, env)
		if err != nil {
			return &policyEngine{}, fmt.Errorf("failed to create CEL program from condition: %v", err)
		}
		programs[policyName] = prg
	}
	return &policyEngine{action, programs}, nil
}

// Returns the decision of an engine based on whether or not AuthorizationArgs is a match,
// i.e. if engine's action is ALLOW and match is true, we will return DecisionAllow;
// if engine's action is ALLOW and match is false, we will return DecisionDeny.
func getDecision(engine *policyEngine, match bool) Decision {
	if engine.action == pb.RBAC_ALLOW && match || engine.action == pb.RBAC_DENY && !match {
		return DecisionAllow
	}
	return DecisionDeny
}

// Returns the authorization decision of a single policy engine based on
// activation.  If any policy matches, the decision matches the engine's
// action, and the first matching policy name will be returned.
//
// Else if any policy is missing attributes, the decision is unknown, and the
// list of policy names that can't be evaluated due to missing attributes will
// be returned.
//
// Else, the decision is the opposite of the engine's action, i.e. an ALLOW
// engine will return DecisionDeny, and vice versa.
func (engine *policyEngine) evaluate(activation interpreter.Activation) (Decision, []string) {
	unknownPolicyNames := []string{}
	for policyName, program := range engine.programs {
		// Evaluate program against activation.
		var match bool
		out, _, err := program.Eval(activation)
		if err != nil {
			if out == nil {
				// Unsuccessful evaluation, typically the result of a series of incompatible
				// `EnvOption` or `ProgramOption` values used in the creation of the evaluation
				// environment or executable program.
				logger.Warning("Unsuccessful evaluation encountered during AuthorizationEngine.Evaluate: %s", err.Error())
			}
			// Unsuccessful evaluation or successful evaluation to an error result, i.e. missing attributes.
			match = false
		} else {
			// Successful evaluation to a non-error result.
			if !types.IsBool(out) {
				logger.Warning("'Successful evaluation', but output isn't a boolean: %v", out)
				match = false
			} else {
				match = out.Value().(bool)
			}
		}

		// Process evaluation results.
		if err != nil {
			unknownPolicyNames = append(unknownPolicyNames, policyName)
		} else if match {
			return getDecision(engine, true), []string{policyName}
		}
	}
	if len(unknownPolicyNames) > 0 {
		return DecisionUnknown, unknownPolicyNames
	}
	return getDecision(engine, false), []string{}
}

// AuthorizationEngine is the struct for the CEL-based authorization engine.
type AuthorizationEngine struct {
	allow *policyEngine
	deny  *policyEngine
}

// NewAuthorizationEngine builds a CEL evaluation engine from at most one allow and one deny Envoy RBAC.
func NewAuthorizationEngine(allow, deny *pb.RBAC) (*AuthorizationEngine, error) {
	if allow == nil && deny == nil {
		return &AuthorizationEngine{}, fmt.Errorf("at least one of allow, deny must be non-nil")
	}
	if allow != nil && allow.Action != pb.RBAC_ALLOW || deny != nil && deny.Action != pb.RBAC_DENY {
		return nil, fmt.Errorf("allow must have action ALLOW, deny must have action DENY")
	}
	// Note: env can be shared across multiple Checks / Program constructions.
	env, err := cel.NewEnv(
		cel.Declarations(
			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),
		),
	)
	if err != nil {
		return &AuthorizationEngine{}, fmt.Errorf("failed to create CEL Env: %v", err)
	}
	// create policy engines
	allowEngine, err := newPolicyEngine(allow, env)
	if err != nil {
		return &AuthorizationEngine{}, err
	}
	denyEngine, err := newPolicyEngine(deny, env)
	if err != nil {
		return &AuthorizationEngine{}, err
	}
	return &AuthorizationEngine{allow: allowEngine, deny: denyEngine}, nil
}

// Evaluate is the core function that evaluates whether an RPC is authorized.
//
// ALLOW policy. If one of the RBAC conditions is evaluated as true, then the
// CEL-based authorization engine evaluation returns allow. If all of the RBAC
// conditions are evaluated as false, then it returns deny. Otherwise, some
// conditions are false and some are unknown, it returns undecided.
//
// DENY policy. If one of the RBAC conditions is evaluated as true, then the
// CEL-based authorization engine evaluation returns deny. If all of the RBAC
// conditions are evaluated as false, then it returns allow. Otherwise, some
// conditions are false and some are unknown, it returns undecided.
//
// DENY policy + ALLOW policy. Evaluation is in the following order: If one
// of the expressions in the DENY policy is true, the authorization engine
// returns deny. If one of the expressions in the DENY policy is unknown, it
// returns undecided. Now all the expressions in the DENY policy are false,
// it returns the evaluation of the ALLOW policy.
func (authorizationEngine *AuthorizationEngine) Evaluate(args *AuthorizationArgs) (AuthorizationDecision, error) {
	activation := newActivation(args)
	decision := DecisionAllow
	var policyNames []string
	// Evaluate the deny engine, if it exists.
	if authorizationEngine.deny != nil {
		decision, policyNames = authorizationEngine.deny.evaluate(activation)
	}
	// Evaluate the allow engine, if it exists and if the deny engine doesn't exist or is unmatched.
	if authorizationEngine.allow != nil && decision == DecisionAllow {
		decision, policyNames = authorizationEngine.allow.evaluate(activation)
	}
	return AuthorizationDecision{decision, policyNames}, nil
}
