github.com/hxx258456/ccgo@v0.0.5-0.20230213014102-48b35f46f66f/grpc/internal/xds/rbac/rbac_engine.go (about)

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