mirror of https://github.com/grpc/grpc-go.git
379 lines
14 KiB
Go
379 lines
14 KiB
Go
/*
|
|
* 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
|
|
}
|