mirror of https://github.com/hashicorp/consul
Internal endpoint to query intentions associated with a gateway (#8400)
parent
ed0fa4b3b1
commit
875816d0d3
|
@ -205,6 +205,90 @@ func (m *Internal) GatewayServiceDump(args *structs.ServiceSpecificRequest, repl
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Match returns the set of intentions that match the given source/destination.
|
||||||
|
func (m *Internal) GatewayIntentions(args *structs.IntentionQueryRequest, reply *structs.IndexedIntentions) error {
|
||||||
|
// Forward if necessary
|
||||||
|
if done, err := m.srv.ForwardRPC("Internal.GatewayIntentions", args, args, reply); done {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args.Match.Entries) > 1 {
|
||||||
|
return fmt.Errorf("Expected 1 gateway name, got %d", len(args.Match.Entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the ACL token for the request for the checks below.
|
||||||
|
var entMeta structs.EnterpriseMeta
|
||||||
|
var authzContext acl.AuthorizerContext
|
||||||
|
|
||||||
|
authz, err := m.srv.ResolveTokenAndDefaultMeta(args.Token, &entMeta, &authzContext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Match.Entries[0].Namespace == "" {
|
||||||
|
args.Match.Entries[0].Namespace = entMeta.NamespaceOrDefault()
|
||||||
|
}
|
||||||
|
if err := m.srv.validateEnterpriseIntentionNamespace(args.Match.Entries[0].Namespace, true); err != nil {
|
||||||
|
return fmt.Errorf("Invalid match entry namespace %q: %v", args.Match.Entries[0].Namespace, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need read access to the gateway we're trying to find intentions for, so check that first.
|
||||||
|
if authz != nil && authz.ServiceRead(args.Match.Entries[0].Name, &authzContext) != acl.Allow {
|
||||||
|
return acl.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.srv.blockingQuery(
|
||||||
|
&args.QueryOptions,
|
||||||
|
&reply.QueryMeta,
|
||||||
|
func(ws memdb.WatchSet, state *state.Store) error {
|
||||||
|
var maxIdx uint64
|
||||||
|
idx, gatewayServices, err := state.GatewayServices(ws, args.Match.Entries[0].Name, &entMeta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if idx > maxIdx {
|
||||||
|
maxIdx = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over the gateway <-> serviceName mappings and fetch all intentions for each
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
result := make(structs.Intentions, 0)
|
||||||
|
|
||||||
|
for _, gs := range gatewayServices {
|
||||||
|
entry := structs.IntentionMatchEntry{
|
||||||
|
Namespace: gs.Service.NamespaceOrDefault(),
|
||||||
|
Name: gs.Service.Name,
|
||||||
|
}
|
||||||
|
idx, intentions, err := state.IntentionMatchOne(ws, entry, structs.IntentionMatchDestination)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if idx > maxIdx {
|
||||||
|
maxIdx = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate wildcard intentions
|
||||||
|
for _, ixn := range intentions {
|
||||||
|
if !seen[ixn.ID] {
|
||||||
|
result = append(result, ixn)
|
||||||
|
seen[ixn.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Index, reply.Intentions = maxIdx, result
|
||||||
|
if reply.Intentions == nil {
|
||||||
|
reply.Intentions = make(structs.Intentions, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.srv.filterACL(args.Token, reply); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// EventFire is a bit of an odd endpoint, but it allows for a cross-DC RPC
|
// EventFire is a bit of an odd endpoint, but it allows for a cross-DC RPC
|
||||||
// call to fire an event. The primary use case is to enable user events being
|
// call to fire an event. The primary use case is to enable user events being
|
||||||
// triggered in a remote DC.
|
// triggered in a remote DC.
|
||||||
|
|
|
@ -1329,3 +1329,231 @@ func TestInternal_GatewayServiceDump_Ingress_ACL(t *testing.T) {
|
||||||
require.Equal(t, nodes[0].Service.Service, "db")
|
require.Equal(t, nodes[0].Service.Service, "db")
|
||||||
require.Equal(t, nodes[0].Checks[0].Status, api.HealthWarning)
|
require.Equal(t, nodes[0].Checks[0].Status, api.HealthWarning)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInternal_GatewayIntentions(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir1, s1 := testServer(t)
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
|
testrpc.WaitForTestAgent(t, s1.RPC, "dc1")
|
||||||
|
|
||||||
|
// Register terminating gateway and config entry linking it to postgres + redis
|
||||||
|
{
|
||||||
|
arg := structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "terminating-gateway",
|
||||||
|
Service: "terminating-gateway",
|
||||||
|
Kind: structs.ServiceKindTerminatingGateway,
|
||||||
|
Port: 443,
|
||||||
|
},
|
||||||
|
Check: &structs.HealthCheck{
|
||||||
|
Name: "terminating connect",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "terminating-gateway",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var regOutput struct{}
|
||||||
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, ®Output))
|
||||||
|
|
||||||
|
args := &structs.TerminatingGatewayConfigEntry{
|
||||||
|
Name: "terminating-gateway",
|
||||||
|
Kind: structs.TerminatingGateway,
|
||||||
|
Services: []structs.LinkedService{
|
||||||
|
{
|
||||||
|
Name: "postgres",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "redis",
|
||||||
|
CAFile: "/etc/certs/ca.pem",
|
||||||
|
CertFile: "/etc/certs/cert.pem",
|
||||||
|
KeyFile: "/etc/certs/key.pem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := structs.ConfigEntryRequest{
|
||||||
|
Op: structs.ConfigEntryUpsert,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: args,
|
||||||
|
}
|
||||||
|
var configOutput bool
|
||||||
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &configOutput))
|
||||||
|
require.True(t, configOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create some symmetric intentions to ensure we are only matching on destination
|
||||||
|
{
|
||||||
|
for _, v := range []string{"*", "mysql", "redis", "postgres"} {
|
||||||
|
req := structs.IntentionRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: structs.IntentionOpCreate,
|
||||||
|
Intention: structs.TestIntention(t),
|
||||||
|
}
|
||||||
|
req.Intention.SourceName = "api"
|
||||||
|
req.Intention.DestinationName = v
|
||||||
|
|
||||||
|
var reply string
|
||||||
|
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &reply))
|
||||||
|
|
||||||
|
req = structs.IntentionRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: structs.IntentionOpCreate,
|
||||||
|
Intention: structs.TestIntention(t),
|
||||||
|
}
|
||||||
|
req.Intention.SourceName = v
|
||||||
|
req.Intention.DestinationName = "api"
|
||||||
|
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &reply))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request intentions matching the gateway named "terminating-gateway"
|
||||||
|
req := structs.IntentionQueryRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Match: &structs.IntentionQueryMatch{
|
||||||
|
Type: structs.IntentionMatchDestination,
|
||||||
|
Entries: []structs.IntentionMatchEntry{
|
||||||
|
{
|
||||||
|
Namespace: structs.IntentionDefaultNamespace,
|
||||||
|
Name: "terminating-gateway",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var reply structs.IndexedIntentions
|
||||||
|
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.GatewayIntentions", &req, &reply))
|
||||||
|
assert.Len(t, reply.Intentions, 3)
|
||||||
|
|
||||||
|
// Only intentions with linked services as a destination should be returned, and wildcard matches should be deduped
|
||||||
|
expected := []string{"postgres", "*", "redis"}
|
||||||
|
actual := []string{
|
||||||
|
reply.Intentions[0].DestinationName,
|
||||||
|
reply.Intentions[1].DestinationName,
|
||||||
|
reply.Intentions[2].DestinationName,
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternal_GatewayIntentions_aclDeny(t *testing.T) {
|
||||||
|
dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil))
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
|
testrpc.WaitForTestAgent(t, s1.RPC, "dc1", testrpc.WithToken(TestDefaultMasterToken))
|
||||||
|
|
||||||
|
// Register terminating gateway and config entry linking it to postgres + redis
|
||||||
|
{
|
||||||
|
arg := structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "terminating-gateway",
|
||||||
|
Service: "terminating-gateway",
|
||||||
|
Kind: structs.ServiceKindTerminatingGateway,
|
||||||
|
Port: 443,
|
||||||
|
},
|
||||||
|
Check: &structs.HealthCheck{
|
||||||
|
Name: "terminating connect",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "terminating-gateway",
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||||
|
}
|
||||||
|
var regOutput struct{}
|
||||||
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, ®Output))
|
||||||
|
|
||||||
|
args := &structs.TerminatingGatewayConfigEntry{
|
||||||
|
Name: "terminating-gateway",
|
||||||
|
Kind: structs.TerminatingGateway,
|
||||||
|
Services: []structs.LinkedService{
|
||||||
|
{
|
||||||
|
Name: "postgres",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "redis",
|
||||||
|
CAFile: "/etc/certs/ca.pem",
|
||||||
|
CertFile: "/etc/certs/cert.pem",
|
||||||
|
KeyFile: "/etc/certs/key.pem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := structs.ConfigEntryRequest{
|
||||||
|
Op: structs.ConfigEntryUpsert,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: args,
|
||||||
|
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||||
|
}
|
||||||
|
var configOutput bool
|
||||||
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &configOutput))
|
||||||
|
require.True(t, configOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create some symmetric intentions to ensure we are only matching on destination
|
||||||
|
{
|
||||||
|
for _, v := range []string{"*", "mysql", "redis", "postgres"} {
|
||||||
|
req := structs.IntentionRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: structs.IntentionOpCreate,
|
||||||
|
Intention: structs.TestIntention(t),
|
||||||
|
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||||
|
}
|
||||||
|
req.Intention.SourceName = "api"
|
||||||
|
req.Intention.DestinationName = v
|
||||||
|
|
||||||
|
var reply string
|
||||||
|
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &reply))
|
||||||
|
|
||||||
|
req = structs.IntentionRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: structs.IntentionOpCreate,
|
||||||
|
Intention: structs.TestIntention(t),
|
||||||
|
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||||
|
}
|
||||||
|
req.Intention.SourceName = v
|
||||||
|
req.Intention.DestinationName = "api"
|
||||||
|
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &reply))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `
|
||||||
|
service_prefix "redis" { policy = "read" }
|
||||||
|
service_prefix "terminating-gateway" { policy = "read" }
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Request intentions matching the gateway named "terminating-gateway"
|
||||||
|
req := structs.IntentionQueryRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Match: &structs.IntentionQueryMatch{
|
||||||
|
Type: structs.IntentionMatchDestination,
|
||||||
|
Entries: []structs.IntentionMatchEntry{
|
||||||
|
{
|
||||||
|
Namespace: structs.IntentionDefaultNamespace,
|
||||||
|
Name: "terminating-gateway",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QueryOptions: structs.QueryOptions{Token: userToken.SecretID},
|
||||||
|
}
|
||||||
|
var reply structs.IndexedIntentions
|
||||||
|
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.GatewayIntentions", &req, &reply))
|
||||||
|
assert.Len(t, reply.Intentions, 2)
|
||||||
|
|
||||||
|
// Only intentions for redis should be returned, due to ACLs
|
||||||
|
expected := []string{"*", "redis"}
|
||||||
|
actual := []string{
|
||||||
|
reply.Intentions[0].DestinationName,
|
||||||
|
reply.Intentions[1].DestinationName,
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
|
@ -338,31 +338,11 @@ func (s *Store) IntentionMatch(ws memdb.WatchSet, args *structs.IntentionQueryMa
|
||||||
// Make all the calls and accumulate the results
|
// Make all the calls and accumulate the results
|
||||||
results := make([]structs.Intentions, len(args.Entries))
|
results := make([]structs.Intentions, len(args.Entries))
|
||||||
for i, entry := range args.Entries {
|
for i, entry := range args.Entries {
|
||||||
// Each search entry may require multiple queries to memdb, so this
|
ixns, err := s.intentionMatchOneTxn(tx, ws, entry, args.Type)
|
||||||
// returns the arguments for each necessary Get. Note on performance:
|
|
||||||
// this is not the most optimal set of queries since we repeat some
|
|
||||||
// many times (such as */*). We can work on improving that in the
|
|
||||||
// future, the test cases shouldn't have to change for that.
|
|
||||||
getParams, err := s.intentionMatchGetParams(entry)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform each call and accumulate the result.
|
|
||||||
var ixns structs.Intentions
|
|
||||||
for _, params := range getParams {
|
|
||||||
iter, err := tx.Get(intentionsTableName, string(args.Type), params...)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, fmt.Errorf("failed intention lookup: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.Add(iter.WatchCh())
|
|
||||||
|
|
||||||
for ixn := iter.Next(); ixn != nil; ixn = iter.Next() {
|
|
||||||
ixns = append(ixns, ixn.(*structs.Intention))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the results by precedence
|
// Sort the results by precedence
|
||||||
sort.Sort(structs.IntentionPrecedenceSorter(ixns))
|
sort.Sort(structs.IntentionPrecedenceSorter(ixns))
|
||||||
|
|
||||||
|
@ -373,9 +353,66 @@ func (s *Store) IntentionMatch(ws memdb.WatchSet, args *structs.IntentionQueryMa
|
||||||
return idx, results, nil
|
return idx, results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IntentionMatchOne returns the list of intentions that match the namespace and
|
||||||
|
// name for a single source or destination. This applies the resolution rules
|
||||||
|
// so wildcards will match any value.
|
||||||
|
//
|
||||||
|
// The returned intentions are sorted based on the intention precedence rules.
|
||||||
|
// i.e. result[0] is the highest precedent rule to match
|
||||||
|
func (s *Store) IntentionMatchOne(ws memdb.WatchSet,
|
||||||
|
entry structs.IntentionMatchEntry, matchType structs.IntentionMatchType) (uint64, structs.Intentions, error) {
|
||||||
|
tx := s.db.Txn(false)
|
||||||
|
defer tx.Abort()
|
||||||
|
|
||||||
|
// Get the table index.
|
||||||
|
idx := maxIndexTxn(tx, intentionsTableName)
|
||||||
|
if idx < 1 {
|
||||||
|
idx = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := s.intentionMatchOneTxn(tx, ws, entry, matchType)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(structs.IntentionPrecedenceSorter(results))
|
||||||
|
|
||||||
|
return idx, results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) intentionMatchOneTxn(tx ReadTxn, ws memdb.WatchSet,
|
||||||
|
entry structs.IntentionMatchEntry, matchType structs.IntentionMatchType) (structs.Intentions, error) {
|
||||||
|
|
||||||
|
// Each search entry may require multiple queries to memdb, so this
|
||||||
|
// returns the arguments for each necessary Get. Note on performance:
|
||||||
|
// this is not the most optimal set of queries since we repeat some
|
||||||
|
// many times (such as */*). We can work on improving that in the
|
||||||
|
// future, the test cases shouldn't have to change for that.
|
||||||
|
getParams, err := intentionMatchGetParams(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform each call and accumulate the result.
|
||||||
|
var result structs.Intentions
|
||||||
|
for _, params := range getParams {
|
||||||
|
iter, err := tx.Get(intentionsTableName, string(matchType), params...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed intention lookup: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.Add(iter.WatchCh())
|
||||||
|
|
||||||
|
for ixn := iter.Next(); ixn != nil; ixn = iter.Next() {
|
||||||
|
result = append(result, ixn.(*structs.Intention))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// intentionMatchGetParams returns the tx.Get parameters to find all the
|
// intentionMatchGetParams returns the tx.Get parameters to find all the
|
||||||
// intentions for a certain entry.
|
// intentions for a certain entry.
|
||||||
func (s *Store) intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}, error) {
|
func intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}, error) {
|
||||||
// We always query for "*/*" so include that. If the namespace is a
|
// We always query for "*/*" so include that. If the namespace is a
|
||||||
// wildcard, then we're actually done.
|
// wildcard, then we're actually done.
|
||||||
result := make([][]interface{}, 0, 3)
|
result := make([][]interface{}, 0, 3)
|
||||||
|
|
|
@ -463,6 +463,141 @@ func TestStore_IntentionMatch_table(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equivalent to TestStore_IntentionMatch_table but for IntentionMatchOne which matches a single service
|
||||||
|
func TestStore_IntentionMatchOne_table(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
Name string
|
||||||
|
Insert [][]string // List of intentions to insert
|
||||||
|
Query []string // List of intentions to match
|
||||||
|
Expected [][]string // List of matches, where each match is a list of intentions
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []testCase{
|
||||||
|
{
|
||||||
|
"single exact namespace/name",
|
||||||
|
[][]string{
|
||||||
|
{"foo", "*"},
|
||||||
|
{"foo", "bar"},
|
||||||
|
{"foo", "baz"}, // shouldn't match
|
||||||
|
{"bar", "bar"}, // shouldn't match
|
||||||
|
{"bar", "*"}, // shouldn't match
|
||||||
|
{"*", "*"},
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"foo", "bar",
|
||||||
|
},
|
||||||
|
[][]string{
|
||||||
|
{"foo", "bar"},
|
||||||
|
{"foo", "*"},
|
||||||
|
{"*", "*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"single exact namespace/name with duplicate destinations",
|
||||||
|
[][]string{
|
||||||
|
// 4-tuple specifies src and destination to test duplicate destinations
|
||||||
|
// with different sources. We flip them around to test in both
|
||||||
|
// directions. The first pair are the ones searched on in both cases so
|
||||||
|
// the duplicates need to be there.
|
||||||
|
{"foo", "bar", "foo", "*"},
|
||||||
|
{"foo", "bar", "bar", "*"},
|
||||||
|
{"*", "*", "*", "*"},
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"foo", "bar",
|
||||||
|
},
|
||||||
|
[][]string{
|
||||||
|
// Note the first two have the same precedence so we rely on arbitrary
|
||||||
|
// lexicographical tie-break behavior.
|
||||||
|
{"foo", "bar", "bar", "*"},
|
||||||
|
{"foo", "bar", "foo", "*"},
|
||||||
|
{"*", "*", "*", "*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testRunner := func(t *testing.T, tc testCase, typ structs.IntentionMatchType) {
|
||||||
|
// Insert the set
|
||||||
|
assert := assert.New(t)
|
||||||
|
s := testStateStore(t)
|
||||||
|
var idx uint64 = 1
|
||||||
|
for _, v := range tc.Insert {
|
||||||
|
ixn := &structs.Intention{ID: testUUID()}
|
||||||
|
switch typ {
|
||||||
|
case structs.IntentionMatchDestination:
|
||||||
|
ixn.DestinationNS = v[0]
|
||||||
|
ixn.DestinationName = v[1]
|
||||||
|
if len(v) == 4 {
|
||||||
|
ixn.SourceNS = v[2]
|
||||||
|
ixn.SourceName = v[3]
|
||||||
|
}
|
||||||
|
case structs.IntentionMatchSource:
|
||||||
|
ixn.SourceNS = v[0]
|
||||||
|
ixn.SourceName = v[1]
|
||||||
|
if len(v) == 4 {
|
||||||
|
ixn.DestinationNS = v[2]
|
||||||
|
ixn.DestinationName = v[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(s.IntentionSet(idx, ixn))
|
||||||
|
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the arguments and match
|
||||||
|
entry := structs.IntentionMatchEntry{
|
||||||
|
Namespace: tc.Query[0],
|
||||||
|
Name: tc.Query[1],
|
||||||
|
}
|
||||||
|
_, matches, err := s.IntentionMatchOne(nil, entry, typ)
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
// Should have equal lengths
|
||||||
|
require.Len(t, matches, len(tc.Expected))
|
||||||
|
|
||||||
|
// Verify matches
|
||||||
|
var actual [][]string
|
||||||
|
for _, ixn := range matches {
|
||||||
|
switch typ {
|
||||||
|
case structs.IntentionMatchDestination:
|
||||||
|
if len(tc.Expected) > 1 && len(tc.Expected[0]) == 4 {
|
||||||
|
actual = append(actual, []string{
|
||||||
|
ixn.DestinationNS,
|
||||||
|
ixn.DestinationName,
|
||||||
|
ixn.SourceNS,
|
||||||
|
ixn.SourceName,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
actual = append(actual, []string{ixn.DestinationNS, ixn.DestinationName})
|
||||||
|
}
|
||||||
|
case structs.IntentionMatchSource:
|
||||||
|
if len(tc.Expected) > 1 && len(tc.Expected[0]) == 4 {
|
||||||
|
actual = append(actual, []string{
|
||||||
|
ixn.SourceNS,
|
||||||
|
ixn.SourceName,
|
||||||
|
ixn.DestinationNS,
|
||||||
|
ixn.DestinationName,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
actual = append(actual, []string{ixn.SourceNS, ixn.SourceName})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(tc.Expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.Name+" (destination)", func(t *testing.T) {
|
||||||
|
testRunner(t, tc, structs.IntentionMatchDestination)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run(tc.Name+" (source)", func(t *testing.T) {
|
||||||
|
testRunner(t, tc, structs.IntentionMatchSource)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStore_Intention_Snapshot_Restore(t *testing.T) {
|
func TestStore_Intention_Snapshot_Restore(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
s := testStateStore(t)
|
s := testStateStore(t)
|
||||||
|
|
|
@ -98,6 +98,7 @@ func init() {
|
||||||
registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo)
|
registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo)
|
||||||
registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPServer).UIServices)
|
registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPServer).UIServices)
|
||||||
registerEndpoint("/v1/internal/ui/gateway-services-nodes/", []string{"GET"}, (*HTTPServer).UIGatewayServicesNodes)
|
registerEndpoint("/v1/internal/ui/gateway-services-nodes/", []string{"GET"}, (*HTTPServer).UIGatewayServicesNodes)
|
||||||
|
registerEndpoint("/v1/internal/ui/gateway-intentions/", []string{"GET"}, (*HTTPServer).UIGatewayIntentions)
|
||||||
registerEndpoint("/v1/internal/acl/authorize", []string{"POST"}, (*HTTPServer).ACLAuthorize)
|
registerEndpoint("/v1/internal/acl/authorize", []string{"POST"}, (*HTTPServer).ACLAuthorize)
|
||||||
registerEndpoint("/v1/kv/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).KVSEndpoint)
|
registerEndpoint("/v1/kv/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).KVSEndpoint)
|
||||||
registerEndpoint("/v1/operator/raft/configuration", []string{"GET"}, (*HTTPServer).OperatorRaftConfiguration)
|
registerEndpoint("/v1/operator/raft/configuration", []string{"GET"}, (*HTTPServer).OperatorRaftConfiguration)
|
||||||
|
|
|
@ -339,3 +339,42 @@ func modifySummaryForGatewayService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /v1/internal/ui/gateway-intentions/:gateway
|
||||||
|
func (s *HTTPServer) UIGatewayIntentions(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
|
var args structs.IntentionQueryRequest
|
||||||
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var entMeta structs.EnterpriseMeta
|
||||||
|
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull out the service name
|
||||||
|
name := strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/gateway-intentions/")
|
||||||
|
if name == "" {
|
||||||
|
resp.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprint(resp, "Missing gateway name")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
args.Match = &structs.IntentionQueryMatch{
|
||||||
|
Type: structs.IntentionMatchDestination,
|
||||||
|
Entries: []structs.IntentionMatchEntry{
|
||||||
|
{
|
||||||
|
Namespace: entMeta.NamespaceOrEmpty(),
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply structs.IndexedIntentions
|
||||||
|
|
||||||
|
defer setMeta(resp, &reply.QueryMeta)
|
||||||
|
if err := s.agent.RPC("Internal.GatewayIntentions", args, &reply); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.Intentions, nil
|
||||||
|
}
|
||||||
|
|
|
@ -700,3 +700,101 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
||||||
}
|
}
|
||||||
assert.ElementsMatch(t, expect, dump)
|
assert.ElementsMatch(t, expect, dump)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIGatewayIntentions(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
a := NewTestAgent(t, "")
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
// Register terminating gateway and config entry linking it to postgres + redis
|
||||||
|
{
|
||||||
|
arg := structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "terminating-gateway",
|
||||||
|
Service: "terminating-gateway",
|
||||||
|
Kind: structs.ServiceKindTerminatingGateway,
|
||||||
|
Port: 443,
|
||||||
|
},
|
||||||
|
Check: &structs.HealthCheck{
|
||||||
|
Name: "terminating connect",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "terminating-gateway",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var regOutput struct{}
|
||||||
|
require.NoError(t, a.RPC("Catalog.Register", &arg, ®Output))
|
||||||
|
|
||||||
|
args := &structs.TerminatingGatewayConfigEntry{
|
||||||
|
Name: "terminating-gateway",
|
||||||
|
Kind: structs.TerminatingGateway,
|
||||||
|
Services: []structs.LinkedService{
|
||||||
|
{
|
||||||
|
Name: "postgres",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "redis",
|
||||||
|
CAFile: "/etc/certs/ca.pem",
|
||||||
|
CertFile: "/etc/certs/cert.pem",
|
||||||
|
KeyFile: "/etc/certs/key.pem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := structs.ConfigEntryRequest{
|
||||||
|
Op: structs.ConfigEntryUpsert,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: args,
|
||||||
|
}
|
||||||
|
var configOutput bool
|
||||||
|
require.NoError(t, a.RPC("ConfigEntry.Apply", &req, &configOutput))
|
||||||
|
require.True(t, configOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create some symmetric intentions to ensure we are only matching on destination
|
||||||
|
{
|
||||||
|
for _, v := range []string{"*", "mysql", "redis", "postgres"} {
|
||||||
|
req := structs.IntentionRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: structs.IntentionOpCreate,
|
||||||
|
Intention: structs.TestIntention(t),
|
||||||
|
}
|
||||||
|
req.Intention.SourceName = "api"
|
||||||
|
req.Intention.DestinationName = v
|
||||||
|
|
||||||
|
var reply string
|
||||||
|
assert.NoError(t, a.RPC("Intention.Apply", &req, &reply))
|
||||||
|
|
||||||
|
req = structs.IntentionRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: structs.IntentionOpCreate,
|
||||||
|
Intention: structs.TestIntention(t),
|
||||||
|
}
|
||||||
|
req.Intention.SourceName = v
|
||||||
|
req.Intention.DestinationName = "api"
|
||||||
|
assert.NoError(t, a.RPC("Intention.Apply", &req, &reply))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request intentions matching the gateway named "terminating-gateway"
|
||||||
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/gateway-intentions/terminating-gateway", nil)
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
obj, err := a.srv.UIGatewayIntentions(resp, req)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assertIndex(t, resp)
|
||||||
|
|
||||||
|
intentions := obj.(structs.Intentions)
|
||||||
|
assert.Len(t, intentions, 3)
|
||||||
|
|
||||||
|
// Only intentions with linked services as a destination should be returned, and wildcard matches should be deduped
|
||||||
|
expected := []string{"postgres", "*", "redis"}
|
||||||
|
actual := []string{
|
||||||
|
intentions[0].DestinationName,
|
||||||
|
intentions[1].DestinationName,
|
||||||
|
intentions[2].DestinationName,
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue