| /* |
| * Copyright 2021 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 rbac provides service-level and method-level access control for a |
| // service. See |
| // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#role-based-access-control-rbac |
| // for documentation. |
| package rbac |
| |
| import ( |
| "context" |
| "crypto/x509" |
| "errors" |
| "fmt" |
| "net" |
| "strconv" |
| |
| v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" |
| "google.golang.org/grpc" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/credentials" |
| "google.golang.org/grpc/grpclog" |
| "google.golang.org/grpc/internal/transport" |
| "google.golang.org/grpc/metadata" |
| "google.golang.org/grpc/peer" |
| "google.golang.org/grpc/status" |
| ) |
| |
| var logger = grpclog.Component("rbac") |
| |
| var getConnection = transport.GetConnection |
| |
| // ChainEngine represents a chain of RBAC Engines, used to make authorization |
| // decisions on incoming RPCs. |
| type ChainEngine struct { |
| chainedEngines []*engine |
| } |
| |
| // NewChainEngine returns a chain of RBAC engines, used to make authorization |
| // decisions on incoming RPCs. Returns a non-nil error for invalid policies. |
| func NewChainEngine(policies []*v3rbacpb.RBAC) (*ChainEngine, error) { |
| engines := make([]*engine, 0, len(policies)) |
| for _, policy := range policies { |
| engine, err := newEngine(policy) |
| if err != nil { |
| return nil, err |
| } |
| engines = append(engines, engine) |
| } |
| return &ChainEngine{chainedEngines: engines}, nil |
| } |
| |
| func (cre *ChainEngine) logRequestDetails(rpcData *rpcData) { |
| if logger.V(2) { |
| logger.Infof("checking request: url path=%s", rpcData.fullMethod) |
| if len(rpcData.certs) > 0 { |
| cert := rpcData.certs[0] |
| logger.Infof("uri sans=%q, dns sans=%q, subject=%v", cert.URIs, cert.DNSNames, cert.Subject) |
| } |
| } |
| } |
| |
| // IsAuthorized determines if an incoming RPC is authorized based on the chain of RBAC |
| // engines and their associated actions. |
| // |
| // Errors returned by this function are compatible with the status package. |
| func (cre *ChainEngine) IsAuthorized(ctx context.Context) error { |
| // This conversion step (i.e. pulling things out of ctx) can be done once, |
| // and then be used for the whole chain of RBAC Engines. |
| rpcData, err := newRPCData(ctx) |
| if err != nil { |
| logger.Errorf("newRPCData: %v", err) |
| return status.Errorf(codes.Internal, "gRPC RBAC: %v", err) |
| } |
| for _, engine := range cre.chainedEngines { |
| matchingPolicyName, ok := engine.findMatchingPolicy(rpcData) |
| if logger.V(2) && ok { |
| logger.Infof("incoming RPC matched to policy %v in engine with action %v", matchingPolicyName, engine.action) |
| } |
| |
| switch { |
| case engine.action == v3rbacpb.RBAC_ALLOW && !ok: |
| cre.logRequestDetails(rpcData) |
| return status.Errorf(codes.PermissionDenied, "incoming RPC did not match an allow policy") |
| case engine.action == v3rbacpb.RBAC_DENY && ok: |
| cre.logRequestDetails(rpcData) |
| return status.Errorf(codes.PermissionDenied, "incoming RPC matched a deny policy %q", matchingPolicyName) |
| } |
| // Every policy in the engine list must be queried. Thus, iterate to the |
| // next policy. |
| } |
| // If the incoming RPC gets through all of the engines successfully (i.e. |
| // doesn't not match an allow or match a deny engine), the RPC is authorized |
| // to proceed. |
| return nil |
| } |
| |
| // engine is used for matching incoming RPCs to policies. |
| type engine struct { |
| policies map[string]*policyMatcher |
| // action must be ALLOW or DENY. |
| action v3rbacpb.RBAC_Action |
| } |
| |
| // newEngine creates an RBAC Engine based on the contents of policy. Returns a |
| // non-nil error if the policy is invalid. |
| func newEngine(config *v3rbacpb.RBAC) (*engine, error) { |
| a := config.GetAction() |
| if a != v3rbacpb.RBAC_ALLOW && a != v3rbacpb.RBAC_DENY { |
| return nil, fmt.Errorf("unsupported action %s", config.Action) |
| } |
| |
| policies := make(map[string]*policyMatcher, len(config.GetPolicies())) |
| for name, policy := range config.GetPolicies() { |
| matcher, err := newPolicyMatcher(policy) |
| if err != nil { |
| return nil, err |
| } |
| policies[name] = matcher |
| } |
| return &engine{ |
| policies: policies, |
| action: a, |
| }, nil |
| } |
| |
| // findMatchingPolicy determines if an incoming RPC matches a policy. On a |
| // successful match, it returns the name of the matching policy and a true bool |
| // to specify that there was a matching policy found. It returns false in |
| // the case of not finding a matching policy. |
| func (r *engine) findMatchingPolicy(rpcData *rpcData) (string, bool) { |
| for policy, matcher := range r.policies { |
| if matcher.match(rpcData) { |
| return policy, true |
| } |
| } |
| return "", false |
| } |
| |
| // newRPCData takes an incoming context (should be a context representing state |
| // needed for server RPC Call with metadata, peer info (used for source ip/port |
| // and TLS information) and connection (used for destination ip/port) piped into |
| // it) and the method name of the Service being called server side and populates |
| // an rpcData struct ready to be passed to the RBAC Engine to find a matching |
| // policy. |
| func newRPCData(ctx context.Context) (*rpcData, error) { |
| // The caller should populate all of these fields (i.e. for empty headers, |
| // pipe an empty md into context). |
| md, ok := metadata.FromIncomingContext(ctx) |
| if !ok { |
| return nil, errors.New("missing metadata in incoming context") |
| } |
| // ":method can be hard-coded to POST if unavailable" - A41 |
| md[":method"] = []string{"POST"} |
| // "If the transport exposes TE in Metadata, then RBAC must special-case the |
| // header to treat it as not present." - A41 |
| delete(md, "TE") |
| |
| pi, ok := peer.FromContext(ctx) |
| if !ok { |
| return nil, errors.New("missing peer info in incoming context") |
| } |
| |
| // The methodName will be available in the passed in ctx from a unary or streaming |
| // interceptor, as grpc.Server pipes in a transport stream which contains the methodName |
| // into contexts available in both unary or streaming interceptors. |
| mn, ok := grpc.Method(ctx) |
| if !ok { |
| return nil, errors.New("missing method in incoming context") |
| } |
| |
| // The connection is needed in order to find the destination address and |
| // port of the incoming RPC Call. |
| conn := getConnection(ctx) |
| if conn == nil { |
| return nil, errors.New("missing connection in incoming context") |
| } |
| _, dPort, err := net.SplitHostPort(conn.LocalAddr().String()) |
| if err != nil { |
| return nil, fmt.Errorf("error parsing local address: %v", err) |
| } |
| dp, err := strconv.ParseUint(dPort, 10, 32) |
| if err != nil { |
| return nil, fmt.Errorf("error parsing local address: %v", err) |
| } |
| |
| var authType string |
| var peerCertificates []*x509.Certificate |
| if pi.AuthInfo != nil { |
| tlsInfo, ok := pi.AuthInfo.(credentials.TLSInfo) |
| if ok { |
| authType = pi.AuthInfo.AuthType() |
| peerCertificates = tlsInfo.State.PeerCertificates |
| } |
| } |
| |
| return &rpcData{ |
| md: md, |
| peerInfo: pi, |
| fullMethod: mn, |
| destinationPort: uint32(dp), |
| localAddr: conn.LocalAddr(), |
| authType: authType, |
| certs: peerCertificates, |
| }, nil |
| } |
| |
| // rpcData wraps data pulled from an incoming RPC that the RBAC engine needs to |
| // find a matching policy. |
| type rpcData struct { |
| // md is the HTTP Headers that are present in the incoming RPC. |
| md metadata.MD |
| // peerInfo is information about the downstream peer. |
| peerInfo *peer.Peer |
| // fullMethod is the method name being called on the upstream service. |
| fullMethod string |
| // destinationPort is the port that the RPC is being sent to on the |
| // server. |
| destinationPort uint32 |
| // localAddr is the address that the RPC is being sent to. |
| localAddr net.Addr |
| // authType is the type of authentication e.g. "tls". |
| authType string |
| // certs are the certificates presented by the peer during a TLS |
| // handshake. |
| certs []*x509.Certificate |
| } |