google.golang.org/grpc@v1.72.2/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  	"crypto/x509"
    26  	"errors"
    27  	"fmt"
    28  	"net"
    29  	"strconv"
    30  
    31  	"google.golang.org/grpc"
    32  	"google.golang.org/grpc/authz/audit"
    33  	"google.golang.org/grpc/codes"
    34  	"google.golang.org/grpc/credentials"
    35  	"google.golang.org/grpc/grpclog"
    36  	"google.golang.org/grpc/internal/transport"
    37  	"google.golang.org/grpc/metadata"
    38  	"google.golang.org/grpc/peer"
    39  	"google.golang.org/grpc/status"
    40  
    41  	v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
    42  )
    43  
    44  var logger = grpclog.Component("rbac")
    45  
    46  var getConnection = transport.GetConnection
    47  
    48  // ChainEngine represents a chain of RBAC Engines, used to make authorization
    49  // decisions on incoming RPCs.
    50  type ChainEngine struct {
    51  	chainedEngines []*engine
    52  }
    53  
    54  // NewChainEngine returns a chain of RBAC engines, used to make authorization
    55  // decisions on incoming RPCs. Returns a non-nil error for invalid policies.
    56  func NewChainEngine(policies []*v3rbacpb.RBAC, policyName string) (*ChainEngine, error) {
    57  	engines := make([]*engine, 0, len(policies))
    58  	for _, policy := range policies {
    59  		engine, err := newEngine(policy, policyName)
    60  		if err != nil {
    61  			return nil, err
    62  		}
    63  		engines = append(engines, engine)
    64  	}
    65  	return &ChainEngine{chainedEngines: engines}, nil
    66  }
    67  
    68  func (cre *ChainEngine) logRequestDetails(rpcData *rpcData) {
    69  	if logger.V(2) {
    70  		logger.Infof("checking request: url path=%s", rpcData.fullMethod)
    71  		if len(rpcData.certs) > 0 {
    72  			cert := rpcData.certs[0]
    73  			logger.Infof("uri sans=%q, dns sans=%q, subject=%v", cert.URIs, cert.DNSNames, cert.Subject)
    74  		}
    75  	}
    76  }
    77  
    78  // IsAuthorized determines if an incoming RPC is authorized based on the chain of RBAC
    79  // engines and their associated actions.
    80  //
    81  // Errors returned by this function are compatible with the status package.
    82  func (cre *ChainEngine) IsAuthorized(ctx context.Context) error {
    83  	// This conversion step (i.e. pulling things out of ctx) can be done once,
    84  	// and then be used for the whole chain of RBAC Engines.
    85  	rpcData, err := newRPCData(ctx)
    86  	if err != nil {
    87  		logger.Errorf("newRPCData: %v", err)
    88  		return status.Errorf(codes.Internal, "gRPC RBAC: %v", err)
    89  	}
    90  	for _, engine := range cre.chainedEngines {
    91  		matchingPolicyName, ok := engine.findMatchingPolicy(rpcData)
    92  		if logger.V(2) && ok {
    93  			logger.Infof("incoming RPC matched to policy %v in engine with action %v", matchingPolicyName, engine.action)
    94  		}
    95  
    96  		switch {
    97  		case engine.action == v3rbacpb.RBAC_ALLOW && !ok:
    98  			cre.logRequestDetails(rpcData)
    99  			engine.doAuditLogging(rpcData, matchingPolicyName, false)
   100  			return status.Errorf(codes.PermissionDenied, "incoming RPC did not match an allow policy")
   101  		case engine.action == v3rbacpb.RBAC_DENY && ok:
   102  			cre.logRequestDetails(rpcData)
   103  			engine.doAuditLogging(rpcData, matchingPolicyName, false)
   104  			return status.Errorf(codes.PermissionDenied, "incoming RPC matched a deny policy %q", matchingPolicyName)
   105  		}
   106  		// Every policy in the engine list must be queried. Thus, iterate to the
   107  		// next policy.
   108  		engine.doAuditLogging(rpcData, matchingPolicyName, true)
   109  	}
   110  	// If the incoming RPC gets through all of the engines successfully (i.e.
   111  	// doesn't not match an allow or match a deny engine), the RPC is authorized
   112  	// to proceed.
   113  	return nil
   114  }
   115  
   116  // engine is used for matching incoming RPCs to policies.
   117  type engine struct {
   118  	// TODO(gtcooke94) - differentiate between `policyName`, `policies`, and `rules`
   119  	policyName string
   120  	policies   map[string]*policyMatcher
   121  	// action must be ALLOW or DENY.
   122  	action         v3rbacpb.RBAC_Action
   123  	auditLoggers   []audit.Logger
   124  	auditCondition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition
   125  }
   126  
   127  // newEngine creates an RBAC Engine based on the contents of a policy. Returns a
   128  // non-nil error if the policy is invalid.
   129  func newEngine(config *v3rbacpb.RBAC, policyName string) (*engine, error) {
   130  	a := config.GetAction()
   131  	if a != v3rbacpb.RBAC_ALLOW && a != v3rbacpb.RBAC_DENY {
   132  		return nil, fmt.Errorf("unsupported action %s", config.Action)
   133  	}
   134  
   135  	policies := make(map[string]*policyMatcher, len(config.GetPolicies()))
   136  	for name, policy := range config.GetPolicies() {
   137  		matcher, err := newPolicyMatcher(policy)
   138  		if err != nil {
   139  			return nil, err
   140  		}
   141  		policies[name] = matcher
   142  	}
   143  
   144  	auditLoggers, auditCondition, err := parseAuditOptions(config.GetAuditLoggingOptions())
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	return &engine{
   149  		policyName:     policyName,
   150  		policies:       policies,
   151  		action:         a,
   152  		auditLoggers:   auditLoggers,
   153  		auditCondition: auditCondition,
   154  	}, nil
   155  }
   156  
   157  func parseAuditOptions(opts *v3rbacpb.RBAC_AuditLoggingOptions) ([]audit.Logger, v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition, error) {
   158  	if opts == nil {
   159  		return nil, v3rbacpb.RBAC_AuditLoggingOptions_NONE, nil
   160  	}
   161  	var auditLoggers []audit.Logger
   162  	for _, logger := range opts.LoggerConfigs {
   163  		auditLogger, err := buildLogger(logger)
   164  		if err != nil {
   165  			return nil, v3rbacpb.RBAC_AuditLoggingOptions_NONE, err
   166  		}
   167  		if auditLogger == nil {
   168  			// This occurs when the audit logger is not registered but also
   169  			// marked optional.
   170  			continue
   171  		}
   172  		auditLoggers = append(auditLoggers, auditLogger)
   173  	}
   174  	return auditLoggers, opts.GetAuditCondition(), nil
   175  
   176  }
   177  
   178  // findMatchingPolicy determines if an incoming RPC matches a policy. On a
   179  // successful match, it returns the name of the matching policy and a true bool
   180  // to specify that there was a matching policy found.  It returns false in
   181  // the case of not finding a matching policy.
   182  func (e *engine) findMatchingPolicy(rpcData *rpcData) (string, bool) {
   183  	for policy, matcher := range e.policies {
   184  		if matcher.match(rpcData) {
   185  			return policy, true
   186  		}
   187  	}
   188  	return "", false
   189  }
   190  
   191  // newRPCData takes an incoming context (should be a context representing state
   192  // needed for server RPC Call with metadata, peer info (used for source ip/port
   193  // and TLS information) and connection (used for destination ip/port) piped into
   194  // it) and the method name of the Service being called server side and populates
   195  // an rpcData struct ready to be passed to the RBAC Engine to find a matching
   196  // policy.
   197  func newRPCData(ctx context.Context) (*rpcData, error) {
   198  	// The caller should populate all of these fields (i.e. for empty headers,
   199  	// pipe an empty md into context).
   200  	md, ok := metadata.FromIncomingContext(ctx)
   201  	if !ok {
   202  		return nil, errors.New("missing metadata in incoming context")
   203  	}
   204  	// ":method can be hard-coded to POST if unavailable" - A41
   205  	md[":method"] = []string{"POST"}
   206  	// "If the transport exposes TE in Metadata, then RBAC must special-case the
   207  	// header to treat it as not present." - A41
   208  	delete(md, "TE")
   209  
   210  	pi, ok := peer.FromContext(ctx)
   211  	if !ok {
   212  		return nil, errors.New("missing peer info in incoming context")
   213  	}
   214  
   215  	// The methodName will be available in the passed in ctx from a unary or streaming
   216  	// interceptor, as grpc.Server pipes in a transport stream which contains the methodName
   217  	// into contexts available in both unary or streaming interceptors.
   218  	mn, ok := grpc.Method(ctx)
   219  	if !ok {
   220  		return nil, errors.New("missing method in incoming context")
   221  	}
   222  	// gRPC-Go strips :path from the headers given to the application, but RBAC should be
   223  	// able to match against it.
   224  	md[":path"] = []string{mn}
   225  
   226  	// The connection is needed in order to find the destination address and
   227  	// port of the incoming RPC Call.
   228  	conn := getConnection(ctx)
   229  	if conn == nil {
   230  		return nil, errors.New("missing connection in incoming context")
   231  	}
   232  	_, dPort, err := net.SplitHostPort(conn.LocalAddr().String())
   233  	if err != nil {
   234  		return nil, fmt.Errorf("error parsing local address: %v", err)
   235  	}
   236  	dp, err := strconv.ParseUint(dPort, 10, 32)
   237  	if err != nil {
   238  		return nil, fmt.Errorf("error parsing local address: %v", err)
   239  	}
   240  
   241  	var authType string
   242  	var peerCertificates []*x509.Certificate
   243  	if tlsInfo, ok := pi.AuthInfo.(credentials.TLSInfo); ok {
   244  		authType = pi.AuthInfo.AuthType()
   245  		peerCertificates = tlsInfo.State.PeerCertificates
   246  	}
   247  
   248  	return &rpcData{
   249  		md:              md,
   250  		peerInfo:        pi,
   251  		fullMethod:      mn,
   252  		destinationPort: uint32(dp),
   253  		localAddr:       conn.LocalAddr(),
   254  		authType:        authType,
   255  		certs:           peerCertificates,
   256  	}, nil
   257  }
   258  
   259  // rpcData wraps data pulled from an incoming RPC that the RBAC engine needs to
   260  // find a matching policy.
   261  type rpcData struct {
   262  	// md is the HTTP Headers that are present in the incoming RPC.
   263  	md metadata.MD
   264  	// peerInfo is information about the downstream peer.
   265  	peerInfo *peer.Peer
   266  	// fullMethod is the method name being called on the upstream service.
   267  	fullMethod string
   268  	// destinationPort is the port that the RPC is being sent to on the
   269  	// server.
   270  	destinationPort uint32
   271  	// localAddr is the address that the RPC is being sent to.
   272  	localAddr net.Addr
   273  	// authType is the type of authentication e.g. "tls".
   274  	authType string
   275  	// certs are the certificates presented by the peer during a TLS
   276  	// handshake.
   277  	certs []*x509.Certificate
   278  }
   279  
   280  func (e *engine) doAuditLogging(rpcData *rpcData, rule string, authorized bool) {
   281  	// In the RBAC world, we need to have a SPIFFE ID as the principal for this
   282  	// to be meaningful
   283  	principal := ""
   284  	if rpcData.peerInfo != nil {
   285  		// If AuthType = tls, then we can cast AuthInfo to TLSInfo.
   286  		if tlsInfo, ok := rpcData.peerInfo.AuthInfo.(credentials.TLSInfo); ok {
   287  			if tlsInfo.SPIFFEID != nil {
   288  				principal = tlsInfo.SPIFFEID.String()
   289  			}
   290  		}
   291  	}
   292  
   293  	// TODO(gtcooke94) check if we need to log before creating the event
   294  	event := &audit.Event{
   295  		FullMethodName: rpcData.fullMethod,
   296  		Principal:      principal,
   297  		PolicyName:     e.policyName,
   298  		MatchedRule:    rule,
   299  		Authorized:     authorized,
   300  	}
   301  	for _, logger := range e.auditLoggers {
   302  		switch e.auditCondition {
   303  		case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY:
   304  			if !authorized {
   305  				logger.Log(event)
   306  			}
   307  		case v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW:
   308  			if authorized {
   309  				logger.Log(event)
   310  			}
   311  		case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY_AND_ALLOW:
   312  			logger.Log(event)
   313  		}
   314  	}
   315  }
   316  
   317  // This is used when converting a custom config from raw JSON to a TypedStruct.
   318  // The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>".
   319  const typeURLPrefix = "grpc.authz.audit_logging/"