google.golang.org/grpc@v1.62.1/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 223 // The connection is needed in order to find the destination address and 224 // port of the incoming RPC Call. 225 conn := getConnection(ctx) 226 if conn == nil { 227 return nil, errors.New("missing connection in incoming context") 228 } 229 _, dPort, err := net.SplitHostPort(conn.LocalAddr().String()) 230 if err != nil { 231 return nil, fmt.Errorf("error parsing local address: %v", err) 232 } 233 dp, err := strconv.ParseUint(dPort, 10, 32) 234 if err != nil { 235 return nil, fmt.Errorf("error parsing local address: %v", err) 236 } 237 238 var authType string 239 var peerCertificates []*x509.Certificate 240 if pi.AuthInfo != nil { 241 tlsInfo, ok := pi.AuthInfo.(credentials.TLSInfo) 242 if ok { 243 authType = pi.AuthInfo.AuthType() 244 peerCertificates = tlsInfo.State.PeerCertificates 245 } 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 && rpcData.peerInfo.AuthInfo != nil && rpcData.peerInfo.AuthInfo.AuthType() == "tls" { 285 // If AuthType = tls, then we can cast AuthInfo to TLSInfo. 286 tlsInfo := rpcData.peerInfo.AuthInfo.(credentials.TLSInfo) 287 if tlsInfo.SPIFFEID != nil { 288 principal = tlsInfo.SPIFFEID.String() 289 } 290 } 291 292 //TODO(gtcooke94) check if we need to log before creating the event 293 event := &audit.Event{ 294 FullMethodName: rpcData.fullMethod, 295 Principal: principal, 296 PolicyName: e.policyName, 297 MatchedRule: rule, 298 Authorized: authorized, 299 } 300 for _, logger := range e.auditLoggers { 301 switch e.auditCondition { 302 case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY: 303 if !authorized { 304 logger.Log(event) 305 } 306 case v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW: 307 if authorized { 308 logger.Log(event) 309 } 310 case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY_AND_ALLOW: 311 logger.Log(event) 312 } 313 } 314 } 315 316 // This is used when converting a custom config from raw JSON to a TypedStruct. 317 // The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>". 318 const typeURLPrefix = "grpc.authz.audit_logging/"