mirror of https://github.com/grpc/grpc-go.git
security/authorization: Added CEL-based authorization engine (#3707)
* Add CEL-based authorization engine in a new module under security/authorization
This commit is contained in:
parent
7419b444ee
commit
d3e3e7a46f
1
Makefile
1
Makefile
|
@ -21,6 +21,7 @@ test: testdeps
|
|||
|
||||
testsubmodule: testdeps
|
||||
cd security/advancedtls && go test -cpu 1,4 -timeout 7m google.golang.org/grpc/security/advancedtls/...
|
||||
cd security/authorization && go test -cpu 1,4 -timeout 7m google.golang.org/grpc/security/authorization/...
|
||||
|
||||
testdeps:
|
||||
go get -d -v -t google.golang.org/grpc/...
|
||||
|
|
|
@ -0,0 +1,374 @@
|
|||
/*
|
||||
* 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"
|
||||
cel "github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/checker/decls"
|
||||
types "github.com/google/cel-go/common/types"
|
||||
interpreter "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("channelz")
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
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
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
* 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 (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
pb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v2"
|
||||
cel "github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
interpreter "github.com/google/cel-go/interpreter"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type programMock struct {
|
||||
out ref.Val
|
||||
err error
|
||||
}
|
||||
|
||||
func (mock programMock) Eval(vars interface{}) (ref.Val, *cel.EvalDetails, error) {
|
||||
return mock.out, nil, mock.err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var (
|
||||
emptyActivation = interpreter.EmptyActivation()
|
||||
unsuccessfulProgram = programMock{out: nil, err: status.Errorf(codes.InvalidArgument, "Unsuccessful program evaluation")}
|
||||
errProgram = programMock{out: valMock{"missing attributes"}, err: status.Errorf(codes.InvalidArgument, "Successful program evaluation to an error result -- missing attributes")}
|
||||
trueProgram = programMock{out: valMock{true}, err: nil}
|
||||
falseProgram = programMock{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 TestNewAuthorizationEngine(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
allow *pb.RBAC
|
||||
deny *pb.RBAC
|
||||
wantErr string
|
||||
errStr string
|
||||
}{
|
||||
"too few rbacs": {
|
||||
allow: nil,
|
||||
deny: nil,
|
||||
wantErr: "at least one of allow, deny must be non-nil",
|
||||
errStr: "Expected error: at least one of allow, deny must be non-nil",
|
||||
},
|
||||
"one rbac allow": {
|
||||
allow: &pb.RBAC{Action: pb.RBAC_ALLOW},
|
||||
deny: nil,
|
||||
wantErr: "",
|
||||
errStr: "Expected 1 ALLOW RBAC to be successful",
|
||||
},
|
||||
"one rbac deny": {
|
||||
allow: nil,
|
||||
deny: &pb.RBAC{Action: pb.RBAC_DENY},
|
||||
wantErr: "",
|
||||
errStr: "Expected 1 DENY RBAC to be successful",
|
||||
},
|
||||
"two rbacs": {
|
||||
allow: &pb.RBAC{Action: pb.RBAC_ALLOW},
|
||||
deny: &pb.RBAC{Action: pb.RBAC_DENY},
|
||||
wantErr: "",
|
||||
errStr: "Expected 2 RBACs (DENY + ALLOW) to be successful",
|
||||
},
|
||||
"wrong rbac actions": {
|
||||
allow: &pb.RBAC{Action: pb.RBAC_DENY},
|
||||
deny: nil,
|
||||
wantErr: "allow must have action ALLOW, deny must have action DENY",
|
||||
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 tc.wantErr == "" && gotErr == nil {
|
||||
return
|
||||
}
|
||||
if gotErr == nil || gotErr.Error() != tc.wantErr {
|
||||
t.Errorf(tc.errStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func 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{}},
|
||||
match: false,
|
||||
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{}},
|
||||
match: false,
|
||||
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.Errorf("Expected %v, instead got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func 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 || !reflect.DeepEqual(gotPolicyNames, tc.wantPolicyNames) {
|
||||
t.Errorf("Expected (%v, %v), instead got (%v, %v)", tc.wantDecision, tc.wantPolicyNames, gotDecision, gotPolicyNames)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizationEngineEvaluate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
engine *AuthorizationEngine
|
||||
authArgs *AuthorizationArgs
|
||||
wantAuthDecision *AuthorizationDecision
|
||||
wantErr error
|
||||
}{
|
||||
"allow match": {
|
||||
engine: &AuthorizationEngine{allow: allowMatchEngine},
|
||||
authArgs: &AuthorizationArgs{},
|
||||
wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{"allow match policy2"}},
|
||||
wantErr: nil,
|
||||
},
|
||||
"deny fail": {
|
||||
engine: &AuthorizationEngine{deny: denyFailEngine},
|
||||
authArgs: &AuthorizationArgs{},
|
||||
wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{}},
|
||||
wantErr: nil,
|
||||
},
|
||||
"first engine unknown": {
|
||||
engine: &AuthorizationEngine{allow: allowMatchEngine, deny: denyUnknownEngine},
|
||||
authArgs: &AuthorizationArgs{},
|
||||
wantAuthDecision: &AuthorizationDecision{decision: DecisionUnknown, policyNames: []string{"deny unknown policy2", "deny unknown policy3"}},
|
||||
wantErr: nil,
|
||||
},
|
||||
"second engine match": {
|
||||
engine: &AuthorizationEngine{allow: allowMatchEngine, deny: denyFailEngine},
|
||||
authArgs: &AuthorizationArgs{},
|
||||
wantAuthDecision: &AuthorizationDecision{decision: DecisionAllow, policyNames: []string{"allow match policy2"}},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gotAuthDecision, gotErr := tc.engine.Evaluate(tc.authArgs)
|
||||
sort.Strings(gotAuthDecision.policyNames)
|
||||
if tc.wantErr != nil && (gotErr == nil || gotErr.Error() != tc.wantErr.Error()) {
|
||||
t.Errorf("Expected error to be %v, instead got %v", tc.wantErr, gotErr)
|
||||
} else if tc.wantErr == nil && (gotErr != nil || gotAuthDecision.decision != tc.wantAuthDecision.decision || !reflect.DeepEqual(gotAuthDecision.policyNames, tc.wantAuthDecision.policyNames)) {
|
||||
t.Errorf("Expected authorization decision to be (%v, %v), instead got (%v, %v)", tc.wantAuthDecision.decision, tc.wantAuthDecision.policyNames, gotAuthDecision.decision, gotAuthDecision.policyNames)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
module google.golang.org/grpc/security/authorization
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/envoyproxy/go-control-plane v0.9.5
|
||||
github.com/google/cel-go v0.5.1
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013
|
||||
google.golang.org/grpc v1.31.0
|
||||
google.golang.org/protobuf v1.25.0
|
||||
)
|
|
@ -0,0 +1,104 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f h1:0cEys61Sr2hUBEXfNV8eyQP01oZuBgoMeHunebPirK8=
|
||||
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533 h1:8wZizuKuZVu5COB7EsBYxBQz8nRcXXn5d4Gt91eJLvU=
|
||||
github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.5 h1:lRJIqDD8yjV1YyPRqecMdytjDLs2fTXq363aCib5xPU=
|
||||
github.com/envoyproxy/go-control-plane v0.9.5/go.mod h1:OXl5to++W0ctG+EHWTFUjiypVxC/Y4VLc/KFU+al13s=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/google/cel-go v0.5.1 h1:oDsbtAwlwFPEcC8dMoRWNuVzWJUDeDZeHjoet9rXjTs=
|
||||
github.com/google/cel-go v0.5.1/go.mod h1:9SvtVVTtZV4DTB1/RuAD1D2HhuqEIdmZEE/r/lrFyKE=
|
||||
github.com/google/cel-spec v0.4.0/go.mod h1:2pBM5cU4UKjbPDXBgwWkiwBsVgnxknuEJ7C5TDWwORQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171 h1:xes2Q2k+d/+YNXVw0FpZkIDJiaux4OVrRKXRAzH6A0U=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
Loading…
Reference in New Issue