package agent import ( "context" "fmt" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" cachetype "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/structs" ) // TODO(rb/intentions): this should move back into the agent endpoint since // there is no ext_authz implementation anymore. // // ConnectAuthorize implements the core authorization logic for Connect. It's in // a separate agent method here because we need to re-use this both in our own // HTTP API authz endpoint and in the gRPX xDS/ext_authz API for envoy. // // NOTE: This treats any L7 intentions as DENY. // // The ACL token and the auth request are provided and the auth decision (true // means authorized) and reason string are returned. // // If the request input is invalid the error returned will be a BadRequestError, // if the token doesn't grant necessary access then an acl.ErrPermissionDenied // error is returned, otherwise error indicates an unexpected server failure. If // access is denied, no error is returned but the first return value is false. func (a *Agent) ConnectAuthorize(token string, req *structs.ConnectAuthorizeRequest) (allowed bool, reason string, m *cache.ResultMeta, err error) { // Helper to make the error cases read better without resorting to named // returns which get messy and prone to mistakes in a method this long. returnErr := func(err error) (bool, string, *cache.ResultMeta, error) { return false, "", nil, err } if req == nil { return returnErr(BadRequestError{"Invalid request"}) } // We need to have a target to check intentions if req.Target == "" { return returnErr(BadRequestError{"Target service must be specified"}) } // Parse the certificate URI from the client ID uri, err := connect.ParseCertURIFromString(req.ClientCertURI) if err != nil { return returnErr(BadRequestError{"ClientCertURI not a valid Connect identifier"}) } uriService, ok := uri.(*connect.SpiffeIDService) if !ok { return returnErr(BadRequestError{"ClientCertURI not a valid Service identifier"}) } // We need to verify service:write permissions for the given token. // We do this manually here since the RPC request below only verifies // service:read. var authzContext acl.AuthorizerContext authz, err := a.delegate.ResolveTokenAndDefaultMeta(token, &req.EnterpriseMeta, &authzContext) if err != nil { return returnErr(err) } if authz != nil && authz.ServiceWrite(req.Target, &authzContext) != acl.Allow { return returnErr(acl.ErrPermissionDenied) } // Note that we DON'T explicitly validate the trust-domain matches ours. See // the PR for this change for details. // TODO(banks): Implement revocation list checking here. // Get the intentions for this target service. args := &structs.IntentionQueryRequest{ Datacenter: a.config.Datacenter, Match: &structs.IntentionQueryMatch{ Type: structs.IntentionMatchDestination, Entries: []structs.IntentionMatchEntry{ { Namespace: req.TargetNamespace(), Name: req.Target, }, }, }, QueryOptions: structs.QueryOptions{Token: token}, } raw, meta, err := a.cache.Get(context.TODO(), cachetype.IntentionMatchName, args) if err != nil { return returnErr(err) } reply, ok := raw.(*structs.IndexedIntentionMatches) if !ok { return returnErr(fmt.Errorf("internal error: response type not correct")) } if len(reply.Matches) != 1 { return returnErr(fmt.Errorf("Internal error loading matches")) } // Figure out which source matches this request. var ixnMatch *structs.Intention for _, ixn := range reply.Matches[0] { if _, ok := uriService.Authorize(ixn); ok { ixnMatch = ixn break } } if ixnMatch != nil { if len(ixnMatch.Permissions) == 0 { // This is an L4 intention. reason = fmt.Sprintf("Matched L4 intention: %s", ixnMatch.String()) auth := ixnMatch.Action == structs.IntentionActionAllow return auth, reason, &meta, nil } // This is an L7 intention, so DENY. reason = fmt.Sprintf("Matched L7 intention: %s", ixnMatch.String()) return false, reason, &meta, nil } // No match, we need to determine the default behavior. We do this by // fetching the default intention behavior from the resolved authorizer. The // default behavior if ACLs are disabled is to allow connections to mimic the // behavior of Consul itself: everything is allowed if ACLs are disabled. if authz == nil { // ACLs not enabled at all, the default is allow all. return true, "ACLs disabled, access is allowed by default", &meta, nil } reason = "Default behavior configured by ACLs" return authz.IntentionDefaultAllow(nil) == acl.Allow, reason, &meta, nil }