blob: 970c560af72214e00804d8ee2ab2185dfbe529d8 [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 (
"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
}