mirror of https://github.com/hashicorp/consul
144 lines
4.9 KiB
Go
144 lines
4.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"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 BadRequest HTTPError,
|
|
// 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(HTTPError{StatusCode: http.StatusBadRequest, Reason: "Invalid request"})
|
|
}
|
|
|
|
// We need to have a target to check intentions
|
|
if req.Target == "" {
|
|
return returnErr(HTTPError{StatusCode: http.StatusBadRequest, Reason: "Target service must be specified"})
|
|
}
|
|
|
|
// Parse the certificate URI from the client ID
|
|
uri, err := connect.ParseCertURIFromString(req.ClientCertURI)
|
|
if err != nil {
|
|
return returnErr(HTTPError{StatusCode: http.StatusBadRequest, Reason: "ClientCertURI not a valid Connect identifier"})
|
|
}
|
|
|
|
uriService, ok := uri.(*connect.SpiffeIDService)
|
|
if !ok {
|
|
return returnErr(HTTPError{StatusCode: http.StatusBadRequest, Reason: "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 err := authz.ToAllowAuthorizer().ServiceWriteAllowed(req.Target, &authzContext); err != nil {
|
|
return returnErr(err)
|
|
}
|
|
|
|
if !uriService.MatchesPartition(req.TargetPartition()) {
|
|
reason = fmt.Sprintf("Mismatched partitions: %q != %q",
|
|
uriService.PartitionOrDefault(),
|
|
acl.PartitionOrDefault(req.TargetPartition()))
|
|
return false, reason, nil, nil
|
|
}
|
|
|
|
// 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(),
|
|
Partition: req.TargetPartition(),
|
|
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] {
|
|
// We match on the intention source because the uriService is the source of the connection to authorize.
|
|
if _, ok := connect.AuthorizeIntentionTarget(
|
|
uriService.Service, uriService.Namespace, uriService.Partition, "", ixn, structs.IntentionMatchSource); 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
|
|
}
|
|
|
|
reason = "Default behavior configured by ACLs"
|
|
return authz.IntentionDefaultAllow(nil) == acl.Allow, reason, &meta, nil
|
|
}
|