dubbo.apache.org/dubbo-go/v3@v3.1.1/xds/utils/rbac/rbac_engine.go (about)

     1  /*
     2   * Licensed to the Apache Software Foundation (ASF) under one or more
     3   * contributor license agreements.  See the NOTICE file distributed with
     4   * this work for additional information regarding copyright ownership.
     5   * The ASF licenses this file to You under the Apache License, Version 2.0
     6   * (the "License"); you may not use this file except in compliance with
     7   * the License.  You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  /*
    19   *
    20   * Copyright 2021 gRPC authors.
    21   *
    22   */
    23  
    24  // Package rbac provides service-level and method-level access control for a
    25  // service. See
    26  // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#role-based-access-control-rbac
    27  // for documentation.
    28  package rbac
    29  
    30  import (
    31  	"context"
    32  	"crypto/x509"
    33  	"errors"
    34  	"fmt"
    35  	"net"
    36  	"strconv"
    37  )
    38  
    39  import (
    40  	v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
    41  
    42  	"google.golang.org/grpc"
    43  	"google.golang.org/grpc/codes"
    44  	"google.golang.org/grpc/credentials"
    45  	"google.golang.org/grpc/grpclog"
    46  	"google.golang.org/grpc/metadata"
    47  	"google.golang.org/grpc/peer"
    48  	"google.golang.org/grpc/status"
    49  )
    50  
    51  import (
    52  	"dubbo.apache.org/dubbo-go/v3/xds/utils/transport"
    53  )
    54  
    55  const logLevel = 2
    56  
    57  var logger = grpclog.Component("rbac")
    58  
    59  var getConnection = transport.GetConnection
    60  
    61  // ChainEngine represents a chain of RBAC Engines, used to make authorization
    62  // decisions on incoming RPCs.
    63  type ChainEngine struct {
    64  	chainedEngines []*engine
    65  }
    66  
    67  // NewChainEngine returns a chain of RBAC engines, used to make authorization
    68  // decisions on incoming RPCs. Returns a non-nil error for invalid policies.
    69  func NewChainEngine(policies []*v3rbacpb.RBAC) (*ChainEngine, error) {
    70  	engines := make([]*engine, 0, len(policies))
    71  	for _, policy := range policies {
    72  		engine, err := newEngine(policy)
    73  		if err != nil {
    74  			return nil, err
    75  		}
    76  		engines = append(engines, engine)
    77  	}
    78  	return &ChainEngine{chainedEngines: engines}, nil
    79  }
    80  
    81  // IsAuthorized determines if an incoming RPC is authorized based on the chain of RBAC
    82  // engines and their associated actions.
    83  //
    84  // Errors returned by this function are compatible with the status package.
    85  func (cre *ChainEngine) IsAuthorized(ctx context.Context) error {
    86  	// This conversion step (i.e. pulling things out of ctx) can be done once,
    87  	// and then be used for the whole chain of RBAC Engines.
    88  	rpcData, err := newRPCData(ctx)
    89  	if err != nil {
    90  		logger.Errorf("newRPCData: %v", err)
    91  		return status.Errorf(codes.Internal, "gRPC RBAC: %v", err)
    92  	}
    93  	for _, engine := range cre.chainedEngines {
    94  		matchingPolicyName, ok := engine.findMatchingPolicy(rpcData)
    95  		if logger.V(logLevel) && ok {
    96  			logger.Infof("incoming RPC matched to policy %v in engine with action %v", matchingPolicyName, engine.action)
    97  		}
    98  
    99  		switch {
   100  		case engine.action == v3rbacpb.RBAC_ALLOW && !ok:
   101  			return status.Errorf(codes.PermissionDenied, "incoming RPC did not match an allow policy")
   102  		case engine.action == v3rbacpb.RBAC_DENY && ok:
   103  			return status.Errorf(codes.PermissionDenied, "incoming RPC matched a deny policy %q", matchingPolicyName)
   104  		}
   105  		// Every policy in the engine list must be queried. Thus, iterate to the
   106  		// next policy.
   107  	}
   108  	// If the incoming RPC gets through all of the engines successfully (i.e.
   109  	// doesn't not match an allow or match a deny engine), the RPC is authorized
   110  	// to proceed.
   111  	return nil
   112  }
   113  
   114  // engine is used for matching incoming RPCs to policies.
   115  type engine struct {
   116  	policies map[string]*policyMatcher
   117  	// action must be ALLOW or DENY.
   118  	action v3rbacpb.RBAC_Action
   119  }
   120  
   121  // newEngine creates an RBAC Engine based on the contents of policy. Returns a
   122  // non-nil error if the policy is invalid.
   123  func newEngine(config *v3rbacpb.RBAC) (*engine, error) {
   124  	a := config.GetAction()
   125  	if a != v3rbacpb.RBAC_ALLOW && a != v3rbacpb.RBAC_DENY {
   126  		return nil, fmt.Errorf("unsupported action %s", config.Action)
   127  	}
   128  
   129  	policies := make(map[string]*policyMatcher, len(config.GetPolicies()))
   130  	for name, policy := range config.GetPolicies() {
   131  		matcher, err := newPolicyMatcher(policy)
   132  		if err != nil {
   133  			return nil, err
   134  		}
   135  		policies[name] = matcher
   136  	}
   137  	return &engine{
   138  		policies: policies,
   139  		action:   a,
   140  	}, nil
   141  }
   142  
   143  // findMatchingPolicy determines if an incoming RPC matches a policy. On a
   144  // successful match, it returns the name of the matching policy and a true bool
   145  // to specify that there was a matching policy found.  It returns false in
   146  // the case of not finding a matching policy.
   147  func (r *engine) findMatchingPolicy(rpcData *rpcData) (string, bool) {
   148  	for policy, matcher := range r.policies {
   149  		if matcher.match(rpcData) {
   150  			return policy, true
   151  		}
   152  	}
   153  	return "", false
   154  }
   155  
   156  // newRPCData takes an incoming context (should be a context representing state
   157  // needed for server RPC Call with metadata, peer info (used for source ip/port
   158  // and TLS information) and connection (used for destination ip/port) piped into
   159  // it) and the method name of the Service being called server side and populates
   160  // an rpcData struct ready to be passed to the RBAC Engine to find a matching
   161  // policy.
   162  func newRPCData(ctx context.Context) (*rpcData, error) {
   163  	// The caller should populate all of these fields (i.e. for empty headers,
   164  	// pipe an empty md into context).
   165  	md, ok := metadata.FromIncomingContext(ctx)
   166  	if !ok {
   167  		return nil, errors.New("missing metadata in incoming context")
   168  	}
   169  	// ":method can be hard-coded to POST if unavailable" - A41
   170  	md[":method"] = []string{"POST"}
   171  	// "If the transport exposes TE in Metadata, then RBAC must special-case the
   172  	// header to treat it as not present." - A41
   173  	delete(md, "TE")
   174  
   175  	pi, ok := peer.FromContext(ctx)
   176  	if !ok {
   177  		return nil, errors.New("missing peer info in incoming context")
   178  	}
   179  
   180  	// The methodName will be available in the passed in ctx from a unary or streaming
   181  	// interceptor, as grpc.Server pipes in a transport stream which contains the methodName
   182  	// into contexts available in both unary or streaming interceptors.
   183  	mn, ok := grpc.Method(ctx)
   184  	if !ok {
   185  		return nil, errors.New("missing method in incoming context")
   186  	}
   187  
   188  	// The connection is needed in order to find the destination address and
   189  	// port of the incoming RPC Call.
   190  	conn := getConnection(ctx)
   191  	if conn == nil {
   192  		return nil, errors.New("missing connection in incoming context")
   193  	}
   194  	_, dPort, err := net.SplitHostPort(conn.LocalAddr().String())
   195  	if err != nil {
   196  		return nil, fmt.Errorf("error parsing local address: %v", err)
   197  	}
   198  	dp, err := strconv.ParseUint(dPort, 10, 32)
   199  	if err != nil {
   200  		return nil, fmt.Errorf("error parsing local address: %v", err)
   201  	}
   202  
   203  	var authType string
   204  	var peerCertificates []*x509.Certificate
   205  	if pi.AuthInfo != nil {
   206  		tlsInfo, ok := pi.AuthInfo.(credentials.TLSInfo)
   207  		if ok {
   208  			authType = pi.AuthInfo.AuthType()
   209  			peerCertificates = tlsInfo.State.PeerCertificates
   210  		}
   211  	}
   212  
   213  	return &rpcData{
   214  		md:              md,
   215  		peerInfo:        pi,
   216  		fullMethod:      mn,
   217  		destinationPort: uint32(dp),
   218  		localAddr:       conn.LocalAddr(),
   219  		authType:        authType,
   220  		certs:           peerCertificates,
   221  	}, nil
   222  }
   223  
   224  // rpcData wraps data pulled from an incoming RPC that the RBAC engine needs to
   225  // find a matching policy.
   226  type rpcData struct {
   227  	// md is the HTTP Headers that are present in the incoming RPC.
   228  	md metadata.MD
   229  	// peerInfo is information about the downstream peer.
   230  	peerInfo *peer.Peer
   231  	// fullMethod is the method name being called on the upstream service.
   232  	fullMethod string
   233  	// destinationPort is the port that the RPC is being sent to on the
   234  	// server.
   235  	destinationPort uint32
   236  	// localAddr is the address that the RPC is being sent to.
   237  	localAddr net.Addr
   238  	// authType is the type of authentication e.g. "tls".
   239  	authType string
   240  	// certs are the certificates presented by the peer during a TLS
   241  	// handshake.
   242  	certs []*x509.Certificate
   243  }