query: support `ResultsFilteredByACLs` in query list endpoint (#11620)

This commit is contained in:
Dan Upton 2021-12-03 23:04:09 +00:00 committed by GitHub
parent ce326b6074
commit c8204330ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 245 additions and 65 deletions

View File

@ -1592,8 +1592,10 @@ func (f *aclFilter) redactPreparedQueryTokens(query **structs.PreparedQuery) {
// filterPreparedQueries is used to filter prepared queries based on ACL rules.
// We prune entries the user doesn't have access to, and we redact any tokens
// if the user doesn't have a management token.
func (f *aclFilter) filterPreparedQueries(queries *structs.PreparedQueries) {
// if the user doesn't have a management token. Returns true if any (named)
// queries were removed - un-named queries are meant to be ephemeral and can
// only be enumerated by a management token
func (f *aclFilter) filterPreparedQueries(queries *structs.PreparedQueries) bool {
var authzContext acl.AuthorizerContext
structs.DefaultEnterpriseMetaInDefaultPartition().FillAuthzContext(&authzContext)
// Management tokens can see everything with no filtering.
@ -1601,17 +1603,22 @@ func (f *aclFilter) filterPreparedQueries(queries *structs.PreparedQueries) {
// the 1.4 ACL rewrite. The global-management token will provide unrestricted query privileges
// so asking for ACLWrite should be unnecessary.
if f.authorizer.ACLWrite(&authzContext) == acl.Allow {
return
return false
}
// Otherwise, we need to see what the token has access to.
var namedQueriesRemoved bool
ret := make(structs.PreparedQueries, 0, len(*queries))
for _, query := range *queries {
// If no prefix ACL applies to this query then filter it, since
// we know at this point the user doesn't have a management
// token, otherwise see what the policy says.
prefix, ok := query.GetACLPrefix()
if !ok || f.authorizer.PreparedQueryRead(prefix, &authzContext) != acl.Allow {
prefix, hasName := query.GetACLPrefix()
switch {
case hasName && f.authorizer.PreparedQueryRead(prefix, &authzContext) != acl.Allow:
namedQueriesRemoved = true
fallthrough
case !hasName:
f.logger.Debug("dropping prepared query from result due to ACLs", "query", query.ID)
continue
}
@ -1623,6 +1630,7 @@ func (f *aclFilter) filterPreparedQueries(queries *structs.PreparedQueries) {
ret = append(ret, final)
}
*queries = ret
return namedQueriesRemoved
}
func (f *aclFilter) filterToken(token **structs.ACLToken) {
@ -1847,6 +1855,9 @@ func filterACLWithAuthorizer(logger hclog.Logger, authorizer acl.Authorizer, sub
case *structs.IndexedCheckServiceNodes:
v.QueryMeta.ResultsFilteredByACLs = filt.filterCheckServiceNodes(&v.Nodes)
case *structs.PreparedQueryExecuteResponse:
v.QueryMeta.ResultsFilteredByACLs = filt.filterCheckServiceNodes(&v.Nodes)
case *structs.IndexedServiceTopology:
filtered := filt.filterServiceTopology(v.ServiceTopology)
if filtered {
@ -1891,7 +1902,7 @@ func filterACLWithAuthorizer(logger hclog.Logger, authorizer acl.Authorizer, sub
v.QueryMeta.ResultsFilteredByACLs = filt.filterSessions(&v.Sessions)
case *structs.IndexedPreparedQueries:
filt.filterPreparedQueries(&v.Queries)
v.QueryMeta.ResultsFilteredByACLs = filt.filterPreparedQueries(&v.Queries)
case **structs.PreparedQuery:
filt.redactPreparedQueryTokens(v)

View File

@ -2752,6 +2752,108 @@ func TestACL_filterCheckServiceNodes(t *testing.T) {
})
}
func TestACL_filterPreparedQueryExecuteResponse(t *testing.T) {
t.Parallel()
logger := hclog.NewNullLogger()
makeList := func() *structs.PreparedQueryExecuteResponse {
return &structs.PreparedQueryExecuteResponse{
Nodes: structs.CheckServiceNodes{
{
Node: &structs.Node{
Node: "node1",
},
Service: &structs.NodeService{
ID: "foo",
Service: "foo",
},
Checks: structs.HealthChecks{
{
Node: "node1",
CheckID: "check1",
ServiceName: "foo",
},
},
},
},
}
}
t.Run("allowed", func(t *testing.T) {
require := require.New(t)
policy, err := acl.NewPolicyFromSource(`
service "foo" {
policy = "read"
}
node "node1" {
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.Nodes, 1)
require.False(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
})
t.Run("allowed to read the service, but not the node", func(t *testing.T) {
require := require.New(t)
policy, err := acl.NewPolicyFromSource(`
service "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.Empty(list.Nodes)
require.True(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
t.Run("allowed to read the node, but not the service", func(t *testing.T) {
require := require.New(t)
policy, err := acl.NewPolicyFromSource(`
node "node1" {
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.Empty(list.Nodes)
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.Nodes)
require.True(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
}
func TestACL_filterServiceTopology(t *testing.T) {
t.Parallel()
// Create some nodes.
@ -3353,70 +3455,97 @@ func TestFilterACL_redactTokenSecrets(t *testing.T) {
func TestACL_filterPreparedQueries(t *testing.T) {
t.Parallel()
queries := structs.PreparedQueries{
&structs.PreparedQuery{
ID: "f004177f-2c28-83b7-4229-eacc25fe55d1",
},
&structs.PreparedQuery{
ID: "f004177f-2c28-83b7-4229-eacc25fe55d2",
Name: "query-with-no-token",
},
&structs.PreparedQuery{
ID: "f004177f-2c28-83b7-4229-eacc25fe55d3",
Name: "query-with-a-token",
Token: "root",
},
logger := hclog.NewNullLogger()
makeList := func() *structs.IndexedPreparedQueries {
return &structs.IndexedPreparedQueries{
Queries: structs.PreparedQueries{
{ID: "f004177f-2c28-83b7-4229-eacc25fe55d1"},
{
ID: "f004177f-2c28-83b7-4229-eacc25fe55d2",
Name: "query-with-no-token",
},
{
ID: "f004177f-2c28-83b7-4229-eacc25fe55d3",
Name: "query-with-a-token",
Token: "root",
},
},
}
}
expected := structs.PreparedQueries{
&structs.PreparedQuery{
ID: "f004177f-2c28-83b7-4229-eacc25fe55d1",
},
&structs.PreparedQuery{
ID: "f004177f-2c28-83b7-4229-eacc25fe55d2",
Name: "query-with-no-token",
},
&structs.PreparedQuery{
ID: "f004177f-2c28-83b7-4229-eacc25fe55d3",
Name: "query-with-a-token",
Token: "root",
},
}
t.Run("management token", func(t *testing.T) {
require := require.New(t)
// Try permissive filtering with a management token. This will allow the
// embedded token to be seen.
filt := newACLFilter(acl.ManageAll(), nil)
filt.filterPreparedQueries(&queries)
if !reflect.DeepEqual(queries, expected) {
t.Fatalf("bad: %#v", queries)
}
list := makeList()
filterACLWithAuthorizer(logger, acl.ManageAll(), list)
// Hang on to the entry with a token, which needs to survive the next
// operation.
original := queries[2]
// Check we get the un-named query.
require.Len(list.Queries, 3)
// Now try permissive filtering with a client token, which should cause
// the embedded token to get redacted, and the query with no name to get
// filtered out.
filt = newACLFilter(acl.AllowAll(), nil)
filt.filterPreparedQueries(&queries)
expected[2].Token = redactedToken
expected = append(structs.PreparedQueries{}, expected[1], expected[2])
if !reflect.DeepEqual(queries, expected) {
t.Fatalf("bad: %#v", queries)
}
// Check we get the un-redacted token.
require.Equal("root", list.Queries[2].Token)
// Make sure that the original object didn't lose its token.
if original.Token != "root" {
t.Fatalf("bad token: %s", original.Token)
}
require.False(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
})
// Now try restrictive filtering.
filt = newACLFilter(acl.DenyAll(), nil)
filt.filterPreparedQueries(&queries)
if len(queries) != 0 {
t.Fatalf("bad: %#v", queries)
}
t.Run("permissive filtering", func(t *testing.T) {
require := require.New(t)
list := makeList()
queryWithToken := list.Queries[2]
filterACLWithAuthorizer(logger, acl.AllowAll(), list)
// Check the un-named query is filtered out.
require.Len(list.Queries, 2)
// Check the token is redacted.
require.Equal(redactedToken, list.Queries[1].Token)
// Check the original object is unmodified.
require.Equal("root", queryWithToken.Token)
// ResultsFilteredByACLs should not include un-named queries, which are only
// readable by a management token.
require.False(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
})
t.Run("limited access", func(t *testing.T) {
require := require.New(t)
policy, err := acl.NewPolicyFromSource(`
query "query-with-a-token" {
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)
// Check we only get the query we have access to.
require.Len(list.Queries, 1)
// Check the token is redacted.
require.Equal(redactedToken, list.Queries[0].Token)
require.True(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
t.Run("restrictive filtering", func(t *testing.T) {
require := require.New(t)
list := makeList()
filterACLWithAuthorizer(logger, acl.DenyAll(), list)
require.Empty(list.Queries)
require.True(list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
}
func TestACL_filterServiceList(t *testing.T) {

View File

@ -373,7 +373,7 @@ func (p *PreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest,
if query.Token != "" {
token = query.Token
}
if err := p.srv.filterACL(token, &reply.Nodes); err != nil {
if err := p.srv.filterACL(token, reply); err != nil {
return err
}
@ -500,7 +500,7 @@ func (p *PreparedQuery) ExecuteRemote(args *structs.PreparedQueryExecuteRemoteRe
if args.Query.Token != "" {
token = args.Query.Token
}
if err := p.srv.filterACL(token, &reply.Nodes); err != nil {
if err := p.srv.filterACL(token, reply); err != nil {
return err
}

View File

@ -1167,6 +1167,31 @@ func TestPreparedQuery_List(t *testing.T) {
}
}
// Same for a token without access to the query.
{
token := createTokenWithPolicyName(t, codec, "deny-queries", `
query_prefix "" {
policy = "deny"
}
`, "root")
req := &structs.DCSpecificRequest{
Datacenter: "dc1",
QueryOptions: structs.QueryOptions{Token: token},
}
var resp structs.IndexedPreparedQueries
if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.List", req, &resp); err != nil {
t.Fatalf("err: %v", err)
}
if len(resp.Queries) != 0 {
t.Fatalf("bad: %v", resp)
}
if !resp.QueryMeta.ResultsFilteredByACLs {
t.Fatal("ResultsFilteredByACLs should be true")
}
}
// But a management token should work, and be able to see the captured
// token.
query.Query.Token = "le-token"
@ -2124,6 +2149,7 @@ func TestPreparedQuery_Execute(t *testing.T) {
require.NoError(t, msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply))
expectNodes(t, &query, &reply, 0)
require.True(t, reply.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
t.Run("normal operation again with exec token", func(t *testing.T) {
@ -2246,6 +2272,20 @@ func TestPreparedQuery_Execute(t *testing.T) {
expectFailoverNodes(t, &query, &reply, 0)
})
t.Run("nodes in response from dc2 are filtered by ACL token", func(t *testing.T) {
req := structs.PreparedQueryExecuteRequest{
Datacenter: "dc1",
QueryIDOrName: query.Query.ID,
QueryOptions: structs.QueryOptions{Token: execNoNodesToken},
}
var reply structs.PreparedQueryExecuteResponse
require.NoError(t, msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply))
expectFailoverNodes(t, &query, &reply, 0)
require.True(t, reply.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
})
// Bake the exec token into the query.
query.Query.Token = execToken
require.NoError(t, msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Apply", &query, &query.Query.ID))