139 lines
4.6 KiB
Go
139 lines
4.6 KiB
Go
|
package agent
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
|
||
|
"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"
|
||
|
)
|
||
|
|
||
|
// 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.
|
||
|
//
|
||
|
// The ACL token and the auth request are provided and the auth decision (true
|
||
|
// means authorised) 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) (authz 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.
|
||
|
rule, err := a.resolveToken(token)
|
||
|
if err != nil {
|
||
|
return returnErr(err)
|
||
|
}
|
||
|
if rule != nil && !rule.ServiceWrite(req.Target, nil) {
|
||
|
return returnErr(acl.ErrPermissionDenied)
|
||
|
}
|
||
|
|
||
|
// Validate the trust domain matches ours. Later we will support explicit
|
||
|
// external federation but not built yet.
|
||
|
rootArgs := &structs.DCSpecificRequest{Datacenter: a.config.Datacenter}
|
||
|
raw, _, err := a.cache.Get(cachetype.ConnectCARootName, rootArgs)
|
||
|
if err != nil {
|
||
|
return returnErr(err)
|
||
|
}
|
||
|
|
||
|
roots, ok := raw.(*structs.IndexedCARoots)
|
||
|
if !ok {
|
||
|
return returnErr(fmt.Errorf("internal error: roots response type not correct"))
|
||
|
}
|
||
|
if roots.TrustDomain == "" {
|
||
|
return returnErr(fmt.Errorf("Connect CA not bootstrapped yet"))
|
||
|
}
|
||
|
if roots.TrustDomain != strings.ToLower(uriService.Host) {
|
||
|
reason = fmt.Sprintf("Identity from an external trust domain: %s",
|
||
|
uriService.Host)
|
||
|
return false, reason, nil, nil
|
||
|
}
|
||
|
|
||
|
// 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: structs.IntentionDefaultNamespace,
|
||
|
Name: req.Target,
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
QueryOptions: structs.QueryOptions{Token: token},
|
||
|
}
|
||
|
|
||
|
raw, meta, err := a.cache.Get(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"))
|
||
|
}
|
||
|
|
||
|
// Test the authorization for each match
|
||
|
for _, ixn := range reply.Matches[0] {
|
||
|
if auth, ok := uriService.Authorize(ixn); ok {
|
||
|
reason = fmt.Sprintf("Matched intention: %s", ixn.String())
|
||
|
return auth, reason, &meta, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// No match, we need to determine the default behavior. We do this by
|
||
|
// specifying the anonymous token, which will get the default behavior. 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.
|
||
|
rule, err = a.resolveToken("")
|
||
|
if err != nil {
|
||
|
return returnErr(err)
|
||
|
}
|
||
|
if rule == 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 rule.IntentionDefaultAllow(), reason, &meta, nil
|
||
|
}
|