sessions: support `ResultsFilteredByACLs` flag/header (#11606)

This commit is contained in:
Dan Upton 2021-12-03 20:43:43 +00:00 committed by GitHub
parent d92f0d84c6
commit bf1e2ca551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 110 deletions

View File

@ -1380,9 +1380,12 @@ func (f *aclFilter) filterDatacenterCheckServiceNodes(datacenterNodes *map[strin
*datacenterNodes = out
}
// filterSessions is used to filter a set of sessions based on ACLs.
func (f *aclFilter) filterSessions(sessions *structs.Sessions) {
// filterSessions is used to filter a set of sessions based on ACLs. Returns
// true if any elements were removed.
func (f *aclFilter) filterSessions(sessions *structs.Sessions) bool {
s := *sessions
var removed bool
for i := 0; i < len(s); i++ {
session := s[i]
@ -1392,11 +1395,13 @@ func (f *aclFilter) filterSessions(sessions *structs.Sessions) {
if f.allowSession(session.Node, &entCtx) {
continue
}
removed = true
f.logger.Debug("dropping session from result due to ACLs", "session", session.ID)
s = append(s[:i], s[i+1:]...)
i--
}
*sessions = s
return removed
}
// filterCoordinates is used to filter nodes in a coordinate dump based on ACL
@ -1852,7 +1857,7 @@ func filterACLWithAuthorizer(logger hclog.Logger, authorizer acl.Authorizer, sub
filt.filterServices(v.Services, &v.EnterpriseMeta)
case *structs.IndexedSessions:
filt.filterSessions(&v.Sessions)
v.QueryMeta.ResultsFilteredByACLs = filt.filterSessions(&v.Sessions)
case *structs.IndexedPreparedQueries:
filt.filterPreparedQueries(&v.Queries)

View File

@ -2796,29 +2796,57 @@ func TestACL_filterCoordinates(t *testing.T) {
func TestACL_filterSessions(t *testing.T) {
t.Parallel()
// Create a session list.
sessions := structs.Sessions{
&structs.Session{
Node: "foo",
},
&structs.Session{
Node: "bar",
logger := hclog.NewNullLogger()
makeList := func() *structs.IndexedSessions {
return &structs.IndexedSessions{
Sessions: structs.Sessions{
{Node: "foo"},
{Node: "bar"},
},
}
// Try permissive filtering.
filt := newACLFilter(acl.AllowAll(), nil)
filt.filterSessions(&sessions)
if len(sessions) != 2 {
t.Fatalf("bad: %#v", sessions)
}
// Try restrictive filtering
filt = newACLFilter(acl.DenyAll(), nil)
filt.filterSessions(&sessions)
if len(sessions) != 0 {
t.Fatalf("bad: %#v", sessions)
t.Run("all allowed", func(t *testing.T) {
require := require.New(t)
list := makeList()
filterACLWithAuthorizer(logger, acl.AllowAll(), list)
require.Len(list.Sessions, 2)
require.False(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
})
t.Run("just one node's sessions allowed", func(t *testing.T) {
require := require.New(t)
policy, err := acl.NewPolicyFromSource(`
session "foo" {
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.Sessions, 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.Sessions)
require.True(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
}
func TestACL_filterNodeDump(t *testing.T) {

View File

@ -6,6 +6,7 @@ import (
"time"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
@ -377,6 +378,7 @@ func TestSession_Get_List_NodeSessions_ACLFilter(t *testing.T) {
}
t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.PrimaryDatacenter = "dc1"
c.ACLsEnabled = true
@ -391,12 +393,17 @@ func TestSession_Get_List_NodeSessions_ACLFilter(t *testing.T) {
testrpc.WaitForLeader(t, s1.RPC, "dc1", testrpc.WithToken("root"))
rules := `
session "foo" {
deniedToken := createTokenWithPolicyName(t, codec, "denied", `
session "foo" {
policy = "deny"
}
`, "root")
allowedToken := createTokenWithPolicyName(t, codec, "allowed", `
session "foo" {
policy = "read"
}
`
token := createToken(t, codec, rules)
}
`, "root")
// Create a node and a session.
s1.fsm.State().EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})
@ -409,95 +416,94 @@ session "foo" {
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out string
if err := msgpackrpc.CallWithCodec(codec, "Session.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
err := msgpackrpc.CallWithCodec(codec, "Session.Apply", &arg, &out)
require.NoError(t, err)
// Perform all the read operations, and make sure everything is empty.
getR := structs.SessionSpecificRequest{
t.Run("Get", func(t *testing.T) {
require := require.New(t)
req := &structs.SessionSpecificRequest{
Datacenter: "dc1",
SessionID: out,
}
{
var sessions structs.IndexedSessions
if err := msgpackrpc.CallWithCodec(codec, "Session.Get", &getR, &sessions); err != nil {
t.Fatalf("err: %v", err)
}
if len(sessions.Sessions) != 0 {
t.Fatalf("bad: %v", sessions.Sessions)
}
}
listR := structs.DCSpecificRequest{
Datacenter: "dc1",
}
{
req.Token = deniedToken
// ACL-restricted results filtered out.
var sessions structs.IndexedSessions
if err := msgpackrpc.CallWithCodec(codec, "Session.List", &listR, &sessions); err != nil {
t.Fatalf("err: %v", err)
}
if len(sessions.Sessions) != 0 {
t.Fatalf("bad: %v", sessions.Sessions)
}
}
nodeR := structs.NodeSpecificRequest{
Datacenter: "dc1",
Node: "foo",
}
{
var sessions structs.IndexedSessions
if err := msgpackrpc.CallWithCodec(codec, "Session.NodeSessions", &nodeR, &sessions); err != nil {
t.Fatalf("err: %v", err)
}
if len(sessions.Sessions) != 0 {
t.Fatalf("bad: %v", sessions.Sessions)
}
}
err := msgpackrpc.CallWithCodec(codec, "Session.Get", req, &sessions)
require.NoError(err)
require.Empty(sessions.Sessions)
require.True(sessions.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
// Finally, supply the token and make sure the reads are allowed.
getR.Token = token
{
var sessions structs.IndexedSessions
if err := msgpackrpc.CallWithCodec(codec, "Session.Get", &getR, &sessions); err != nil {
t.Fatalf("err: %v", err)
}
if len(sessions.Sessions) != 1 {
t.Fatalf("bad: %v", sessions.Sessions)
}
}
listR.Token = token
{
var sessions structs.IndexedSessions
if err := msgpackrpc.CallWithCodec(codec, "Session.List", &listR, &sessions); err != nil {
t.Fatalf("err: %v", err)
}
if len(sessions.Sessions) != 1 {
t.Fatalf("bad: %v", sessions.Sessions)
}
}
nodeR.Token = token
{
var sessions structs.IndexedSessions
if err := msgpackrpc.CallWithCodec(codec, "Session.NodeSessions", &nodeR, &sessions); err != nil {
t.Fatalf("err: %v", err)
}
if len(sessions.Sessions) != 1 {
t.Fatalf("bad: %v", sessions.Sessions)
}
}
// ACL-restricted results included.
req.Token = allowedToken
err = msgpackrpc.CallWithCodec(codec, "Session.Get", req, &sessions)
require.NoError(err)
require.Len(sessions.Sessions, 1)
require.False(sessions.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
// Try to get a session that doesn't exist to make sure that's handled
// correctly by the filter (it will get passed a nil slice).
getR.SessionID = "adf4238a-882b-9ddc-4a9d-5b6758e4159e"
{
req.SessionID = "adf4238a-882b-9ddc-4a9d-5b6758e4159e"
err = msgpackrpc.CallWithCodec(codec, "Session.Get", req, &sessions)
require.NoError(err)
require.Empty(sessions.Sessions)
require.False(sessions.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
})
t.Run("List", func(t *testing.T) {
require := require.New(t)
req := &structs.DCSpecificRequest{
Datacenter: "dc1",
}
req.Token = deniedToken
// ACL-restricted results filtered out.
var sessions structs.IndexedSessions
if err := msgpackrpc.CallWithCodec(codec, "Session.Get", &getR, &sessions); err != nil {
t.Fatalf("err: %v", err)
}
if len(sessions.Sessions) != 0 {
t.Fatalf("bad: %v", sessions.Sessions)
}
err := msgpackrpc.CallWithCodec(codec, "Session.List", req, &sessions)
require.NoError(err)
require.Empty(sessions.Sessions)
require.True(sessions.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
// ACL-restricted results included.
req.Token = allowedToken
err = msgpackrpc.CallWithCodec(codec, "Session.List", req, &sessions)
require.NoError(err)
require.Len(sessions.Sessions, 1)
require.False(sessions.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
})
t.Run("NodeSessions", func(t *testing.T) {
require := require.New(t)
req := &structs.NodeSpecificRequest{
Datacenter: "dc1",
Node: "foo",
}
req.Token = deniedToken
// ACL-restricted results filtered out.
var sessions structs.IndexedSessions
err := msgpackrpc.CallWithCodec(codec, "Session.NodeSessions", req, &sessions)
require.NoError(err)
require.Empty(sessions.Sessions)
require.True(sessions.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
// ACL-restricted results included.
req.Token = allowedToken
err = msgpackrpc.CallWithCodec(codec, "Session.NodeSessions", req, &sessions)
require.NoError(err)
require.Len(sessions.Sessions, 1)
require.False(sessions.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
})
}
func TestSession_ApplyTimers(t *testing.T) {