intention: support `ResultsFilteredByACLs` flag/header (#11612)

This commit is contained in:
Dan Upton 2021-12-03 20:35:54 +00:00 committed by GitHub
parent 0c4633a231
commit c314be2ff9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 48 deletions

View File

@ -1420,11 +1420,14 @@ func (f *aclFilter) filterCoordinates(coords *structs.Coordinates) {
// filterIntentions is used to filter intentions based on ACL rules. // filterIntentions is used to filter intentions based on ACL rules.
// We prune entries the user doesn't have access to, and we redact any tokens // We prune entries the user doesn't have access to, and we redact any tokens
// if the user doesn't have a management token. // if the user doesn't have a management token. Returns true if any elements
func (f *aclFilter) filterIntentions(ixns *structs.Intentions) { // were removed.
func (f *aclFilter) filterIntentions(ixns *structs.Intentions) bool {
ret := make(structs.Intentions, 0, len(*ixns)) ret := make(structs.Intentions, 0, len(*ixns))
var removed bool
for _, ixn := range *ixns { for _, ixn := range *ixns {
if !ixn.CanRead(f.authorizer) { if !ixn.CanRead(f.authorizer) {
removed = true
f.logger.Debug("dropping intention from result due to ACLs", "intention", ixn.ID) f.logger.Debug("dropping intention from result due to ACLs", "intention", ixn.ID)
continue continue
} }
@ -1433,6 +1436,7 @@ func (f *aclFilter) filterIntentions(ixns *structs.Intentions) {
} }
*ixns = ret *ixns = ret
return removed
} }
// filterNodeDump is used to filter through all parts of a node dump and // filterNodeDump is used to filter through all parts of a node dump and
@ -1824,7 +1828,7 @@ func filterACLWithAuthorizer(logger hclog.Logger, authorizer acl.Authorizer, sub
v.QueryMeta.ResultsFilteredByACLs = filt.filterHealthChecks(&v.HealthChecks) v.QueryMeta.ResultsFilteredByACLs = filt.filterHealthChecks(&v.HealthChecks)
case *structs.IndexedIntentions: case *structs.IndexedIntentions:
filt.filterIntentions(&v.Intentions) v.QueryMeta.ResultsFilteredByACLs = filt.filterIntentions(&v.Intentions)
case *structs.IndexedNodeDump: case *structs.IndexedNodeDump:
filt.filterNodeDump(&v.Dump) filt.filterNodeDump(&v.Dump)

View File

@ -2243,54 +2243,63 @@ func TestACL_filterHealthChecks(t *testing.T) {
func TestACL_filterIntentions(t *testing.T) { func TestACL_filterIntentions(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t)
fill := func() structs.Intentions { logger := hclog.NewNullLogger()
return structs.Intentions{
&structs.Intention{ makeList := func() *structs.IndexedIntentions {
ID: "f004177f-2c28-83b7-4229-eacc25fe55d1", return &structs.IndexedIntentions{
DestinationName: "bar", Intentions: structs.Intentions{
}, &structs.Intention{
&structs.Intention{ ID: "f004177f-2c28-83b7-4229-eacc25fe55d1",
ID: "f004177f-2c28-83b7-4229-eacc25fe55d2", DestinationName: "bar",
DestinationName: "foo", },
&structs.Intention{
ID: "f004177f-2c28-83b7-4229-eacc25fe55d2",
DestinationName: "foo",
},
}, },
} }
} }
// Try permissive filtering. t.Run("allowed", func(t *testing.T) {
{ require := require.New(t)
ixns := fill()
filt := newACLFilter(acl.AllowAll(), nil)
filt.filterIntentions(&ixns)
assert.Len(ixns, 2)
}
// Try restrictive filtering. list := makeList()
{ filterACLWithAuthorizer(logger, acl.AllowAll(), list)
ixns := fill()
filt := newACLFilter(acl.DenyAll(), nil)
filt.filterIntentions(&ixns)
assert.Len(ixns, 0)
}
// Policy to see one require.Len(list.Intentions, 2)
policy, err := acl.NewPolicyFromSource(` require.False(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
service "foo" { })
policy = "read"
}
`, acl.SyntaxLegacy, nil, nil)
assert.Nil(err)
perms, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
assert.Nil(err)
// Filter t.Run("allowed to read 1", func(t *testing.T) {
{ require := require.New(t)
ixns := fill()
filt := newACLFilter(perms, nil) policy, err := acl.NewPolicyFromSource(`
filt.filterIntentions(&ixns) service "foo" {
assert.Len(ixns, 1) policy = "read"
} }
`, acl.SyntaxLegacy, 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.Len(list.Intentions, 1)
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.Intentions)
require.True(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
} }
func TestACL_filterServices(t *testing.T) { func TestACL_filterServices(t *testing.T) {

View File

@ -550,17 +550,19 @@ func (s *Intention) List(args *structs.IntentionListRequest, reply *structs.Inde
} else { } else {
reply.DataOrigin = structs.IntentionDataOriginLegacy reply.DataOrigin = structs.IntentionDataOriginLegacy
} }
if err := s.srv.filterACL(args.Token, reply); err != nil {
return err
}
raw, err := filter.Execute(reply.Intentions) raw, err := filter.Execute(reply.Intentions)
if err != nil { if err != nil {
return err return err
} }
reply.Intentions = raw.(structs.Intentions) reply.Intentions = raw.(structs.Intentions)
// Note: we filter the results with ACLs *after* applying the user-supplied
// bexpr filter, to ensure QueryMeta.ResultsFilteredByACLs does not include
// results that would be filtered out even if the user did have permission.
if err := s.srv.filterACL(args.Token, reply); err != nil {
return err
}
return nil return nil
}, },
) )

View File

@ -1635,6 +1635,7 @@ func TestIntentionList_acl(t *testing.T) {
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
require.Len(t, resp.Intentions, 0) require.Len(t, resp.Intentions, 0)
require.False(t, resp.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
}) })
// Test with management token // Test with management token
@ -1646,6 +1647,7 @@ func TestIntentionList_acl(t *testing.T) {
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
require.Len(t, resp.Intentions, 3) require.Len(t, resp.Intentions, 3)
require.False(t, resp.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
}) })
// Test with user token // Test with user token
@ -1657,6 +1659,7 @@ func TestIntentionList_acl(t *testing.T) {
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
require.Len(t, resp.Intentions, 1) require.Len(t, resp.Intentions, 1)
require.True(t, resp.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
}) })
t.Run("filtered", func(t *testing.T) { t.Run("filtered", func(t *testing.T) {
@ -1671,6 +1674,7 @@ func TestIntentionList_acl(t *testing.T) {
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
require.Len(t, resp.Intentions, 1) require.Len(t, resp.Intentions, 1)
require.False(t, resp.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
}) })
} }