// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
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
}