fedstate: support `ResultsFilteredByACLs` in `ListMeshGateways` endpoint (#11644)

This commit is contained in:
Dan Upton 2021-12-03 20:56:55 +00:00 committed by GitHub
parent 361d9c2862
commit 047aa2ffb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 121 additions and 104 deletions

View File

@ -1379,17 +1379,22 @@ func (f *aclFilter) filterServiceTopology(topology *structs.ServiceTopology) boo
} }
// filterDatacenterCheckServiceNodes is used to filter nodes based on ACL rules. // filterDatacenterCheckServiceNodes is used to filter nodes based on ACL rules.
func (f *aclFilter) filterDatacenterCheckServiceNodes(datacenterNodes *map[string]structs.CheckServiceNodes) { // Returns true if any elements are removed.
func (f *aclFilter) filterDatacenterCheckServiceNodes(datacenterNodes *map[string]structs.CheckServiceNodes) bool {
dn := *datacenterNodes dn := *datacenterNodes
out := make(map[string]structs.CheckServiceNodes) out := make(map[string]structs.CheckServiceNodes)
var removed bool
for dc := range dn { for dc := range dn {
nodes := dn[dc] nodes := dn[dc]
f.filterCheckServiceNodes(&nodes) if f.filterCheckServiceNodes(&nodes) {
removed = true
}
if len(nodes) > 0 { if len(nodes) > 0 {
out[dc] = nodes out[dc] = nodes
} }
} }
*datacenterNodes = out *datacenterNodes = out
return removed
} }
// filterSessions is used to filter a set of sessions based on ACLs. Returns // filterSessions is used to filter a set of sessions based on ACLs. Returns
@ -1850,7 +1855,7 @@ func filterACLWithAuthorizer(logger hclog.Logger, authorizer acl.Authorizer, sub
} }
case *structs.DatacenterIndexedCheckServiceNodes: case *structs.DatacenterIndexedCheckServiceNodes:
filt.filterDatacenterCheckServiceNodes(&v.DatacenterNodes) v.QueryMeta.ResultsFilteredByACLs = filt.filterDatacenterCheckServiceNodes(&v.DatacenterNodes)
case *structs.IndexedCoordinates: case *structs.IndexedCoordinates:
v.QueryMeta.ResultsFilteredByACLs = filt.filterCoordinates(&v.Coordinates) v.QueryMeta.ResultsFilteredByACLs = filt.filterCoordinates(&v.Coordinates)

View File

@ -12,7 +12,6 @@ import (
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-uuid" "github.com/hashicorp/go-uuid"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc" msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/mitchellh/copystructure"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -3158,102 +3157,105 @@ func TestACL_filterNodes(t *testing.T) {
func TestACL_filterDatacenterCheckServiceNodes(t *testing.T) { func TestACL_filterDatacenterCheckServiceNodes(t *testing.T) {
t.Parallel() t.Parallel()
// Create some data.
fixture := map[string]structs.CheckServiceNodes{ logger := hclog.NewNullLogger()
"dc1": []structs.CheckServiceNode{
newTestMeshGatewayNode( makeList := func() *structs.DatacenterIndexedCheckServiceNodes {
"dc1", "gateway1a", "1.2.3.4", 5555, map[string]string{structs.MetaWANFederationKey: "1"}, api.HealthPassing, return &structs.DatacenterIndexedCheckServiceNodes{
), DatacenterNodes: map[string]structs.CheckServiceNodes{
newTestMeshGatewayNode( "dc1": []structs.CheckServiceNode{
"dc1", "gateway2a", "4.3.2.1", 9999, map[string]string{structs.MetaWANFederationKey: "1"}, api.HealthPassing, newTestMeshGatewayNode(
), "dc1", "gateway1a", "1.2.3.4", 5555, map[string]string{structs.MetaWANFederationKey: "1"}, api.HealthPassing,
}, ),
"dc2": []structs.CheckServiceNode{ newTestMeshGatewayNode(
newTestMeshGatewayNode( "dc1", "gateway2a", "4.3.2.1", 9999, map[string]string{structs.MetaWANFederationKey: "1"}, api.HealthPassing,
"dc2", "gateway1b", "5.6.7.8", 9999, map[string]string{structs.MetaWANFederationKey: "1"}, api.HealthPassing, ),
), },
newTestMeshGatewayNode( "dc2": []structs.CheckServiceNode{
"dc2", "gateway2b", "8.7.6.5", 1111, map[string]string{structs.MetaWANFederationKey: "1"}, api.HealthPassing, newTestMeshGatewayNode(
), "dc2", "gateway1b", "5.6.7.8", 9999, map[string]string{structs.MetaWANFederationKey: "1"}, api.HealthPassing,
}, ),
newTestMeshGatewayNode(
"dc2", "gateway2b", "8.7.6.5", 1111, map[string]string{structs.MetaWANFederationKey: "1"}, api.HealthPassing,
),
},
},
}
} }
fill := func(t *testing.T) map[string]structs.CheckServiceNodes { t.Run("allowed", func(t *testing.T) {
t.Helper() require := require.New(t)
dup, err := copystructure.Copy(fixture)
require.NoError(t, err)
return dup.(map[string]structs.CheckServiceNodes)
}
// Try permissive filtering. policy, err := acl.NewPolicyFromSource(`
{ node_prefix "" {
dcNodes := fill(t) policy = "read"
filt := newACLFilter(acl.AllowAll(), nil) }
filt.filterDatacenterCheckServiceNodes(&dcNodes) service_prefix "" {
require.Len(t, dcNodes, 2) policy = "read"
require.Equal(t, fill(t), dcNodes) }
} `, acl.SyntaxCurrent, nil, nil)
require.NoError(err)
// Try restrictive filtering. authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
{ require.NoError(err)
dcNodes := fill(t)
filt := newACLFilter(acl.DenyAll(), nil)
filt.filterDatacenterCheckServiceNodes(&dcNodes)
require.Len(t, dcNodes, 0)
}
var ( list := makeList()
policy *acl.Policy filterACLWithAuthorizer(logger, authz, list)
err error
perms acl.Authorizer
)
// Allowed to see the service but not the node.
policy, err = acl.NewPolicyFromSource(`
service_prefix "" { policy = "read" }
`, acl.SyntaxCurrent, nil, nil)
require.NoError(t, err)
perms, err = acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
require.NoError(t, err)
{ require.Len(list.DatacenterNodes["dc1"], 2)
dcNodes := fill(t) require.Len(list.DatacenterNodes["dc2"], 2)
filt := newACLFilter(perms, nil) require.False(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
filt.filterDatacenterCheckServiceNodes(&dcNodes) })
require.Len(t, dcNodes, 0)
}
// Allowed to see the node but not the service. t.Run("allowed to read the service, but not the node", func(t *testing.T) {
policy, err = acl.NewPolicyFromSource(` require := require.New(t)
node_prefix "" { policy = "read" }
`, acl.SyntaxCurrent, nil, nil)
require.NoError(t, err)
perms, err = acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
require.NoError(t, err)
{ policy, err := acl.NewPolicyFromSource(`
dcNodes := fill(t) service_prefix "" {
filt := newACLFilter(perms, nil) policy = "read"
filt.filterDatacenterCheckServiceNodes(&dcNodes) }
require.Len(t, dcNodes, 0) `, acl.SyntaxCurrent, nil, nil)
} require.NoError(err)
// Allowed to see the service AND the node authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
policy, err = acl.NewPolicyFromSource(` require.NoError(err)
service_prefix "" { policy = "read" }
node_prefix "" { policy = "read" }
`, acl.SyntaxCurrent, nil, nil)
require.NoError(t, err)
_, err = acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
require.NoError(t, err)
// Now it should go through. list := makeList()
{ filterACLWithAuthorizer(logger, authz, list)
dcNodes := fill(t)
filt := newACLFilter(acl.AllowAll(), nil) require.Empty(list.DatacenterNodes)
filt.filterDatacenterCheckServiceNodes(&dcNodes) require.True(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
require.Len(t, dcNodes, 2) })
require.Equal(t, fill(t), dcNodes)
} t.Run("allowed to read the node, but not the service", func(t *testing.T) {
require := require.New(t)
policy, err := acl.NewPolicyFromSource(`
node_prefix "" {
policy = "read"
}
`, acl.SyntaxCurrent, nil, nil)
require.NoError(err)
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
require.NoError(err)
list := makeList()
filterACLWithAuthorizer(logger, authz, list)
require.Empty(list.DatacenterNodes)
require.True(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
t.Run("denied", func(t *testing.T) {
require := require.New(t)
list := makeList()
filterACLWithAuthorizer(logger, acl.DenyAll(), list)
require.Empty(list.DatacenterNodes)
require.True(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
} }
func TestACL_redactPreparedQueryTokens(t *testing.T) { func TestACL_redactPreparedQueryTokens(t *testing.T) {

View File

@ -502,9 +502,10 @@ func TestFederationState_List_ACLDeny(t *testing.T) {
type tcase struct { type tcase struct {
token string token string
listDenied bool listDenied bool
listEmpty bool listEmpty bool
gwListEmpty bool gwListEmpty bool
gwFilteredByACLs bool
} }
cases := map[string]tcase{ cases := map[string]tcase{
@ -514,27 +515,31 @@ func TestFederationState_List_ACLDeny(t *testing.T) {
gwListEmpty: true, gwListEmpty: true,
}, },
"no perms": { "no perms": {
token: nadaToken.SecretID, token: nadaToken.SecretID,
listDenied: true, listDenied: true,
gwListEmpty: true, gwListEmpty: true,
gwFilteredByACLs: true,
}, },
"service:read": { "service:read": {
token: svcReadToken.SecretID, token: svcReadToken.SecretID,
listDenied: true, listDenied: true,
gwListEmpty: true, gwListEmpty: true,
gwFilteredByACLs: true,
}, },
"node:read": { "node:read": {
token: nodeReadToken.SecretID, token: nodeReadToken.SecretID,
listDenied: true, listDenied: true,
gwListEmpty: true, gwListEmpty: true,
gwFilteredByACLs: true,
}, },
"service:read and node:read": { "service:read and node:read": {
token: svcAndNodeReadToken.SecretID, token: svcAndNodeReadToken.SecretID,
listDenied: true, listDenied: true,
}, },
"operator:read": { "operator:read": {
token: opReadToken.SecretID, token: opReadToken.SecretID,
gwListEmpty: true, gwListEmpty: true,
gwFilteredByACLs: true,
}, },
"master token": { "master token": {
token: "root", token: "root",
@ -585,6 +590,11 @@ func TestFederationState_List_ACLDeny(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedMeshGateways.DatacenterNodes, out.DatacenterNodes) require.Equal(t, expectedMeshGateways.DatacenterNodes, out.DatacenterNodes)
} }
require.Equal(t,
tc.gwFilteredByACLs,
out.QueryMeta.ResultsFilteredByACLs,
"ResultsFilteredByACLs should be %v", tc.gwFilteredByACLs,
)
}) })
}) })
} }