update ACLs for cluster peering (#15317)
* update ACLs for cluster peering * add changelog * Update .changelog/15317.txt Co-authored-by: Eric Haberkorn <erichaberkorn@gmail.com> Co-authored-by: Eric Haberkorn <erichaberkorn@gmail.com>
This commit is contained in:
parent
656df780ee
commit
8d2ed1999d
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvements
|
||||||
|
acl: Allow reading imported services and nodes from cluster peers with read all permissions
|
||||||
|
```
|
|
@ -20,6 +20,10 @@ type ExportFetcher interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExportedServices struct {
|
type ExportedServices struct {
|
||||||
|
// Data is a map of [namespace] -> [service] -> [list of partitions the service is exported to]
|
||||||
|
// This includes both the names of typical service instances and their corresponding sidecar proxy
|
||||||
|
// instance names. Meaning that if "web" is exported, "web-sidecar-proxy" instances will also be
|
||||||
|
// shown as exported.
|
||||||
Data map[string]map[string][]string
|
Data map[string]map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1578,7 +1578,7 @@ func TestPreparedQuery_Execute(t *testing.T) {
|
||||||
|
|
||||||
execNoNodesToken := createTokenWithPolicyName(t, codec1, "no-nodes", `service_prefix "foo" { policy = "read" }`, "root")
|
execNoNodesToken := createTokenWithPolicyName(t, codec1, "no-nodes", `service_prefix "foo" { policy = "read" }`, "root")
|
||||||
rules := `
|
rules := `
|
||||||
service_prefix "foo" { policy = "read" }
|
service_prefix "" { policy = "read" }
|
||||||
node_prefix "" { policy = "read" }
|
node_prefix "" { policy = "read" }
|
||||||
`
|
`
|
||||||
execToken := createTokenWithPolicyName(t, codec1, "with-read", rules, "root")
|
execToken := createTokenWithPolicyName(t, codec1, "with-read", rules, "root")
|
||||||
|
|
|
@ -142,6 +142,9 @@ func (f *Filter) Filter(subject any) {
|
||||||
if f.filterGatewayServices(&v.Gateways) {
|
if f.filterGatewayServices(&v.Gateways) {
|
||||||
v.QueryMeta.ResultsFilteredByACLs = true
|
v.QueryMeta.ResultsFilteredByACLs = true
|
||||||
}
|
}
|
||||||
|
if f.filterCheckServiceNodes(&v.ImportedNodes) {
|
||||||
|
v.QueryMeta.ResultsFilteredByACLs = true
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
panic(fmt.Errorf("Unhandled type passed to ACL filter: %T %#v", subject, subject))
|
panic(fmt.Errorf("Unhandled type passed to ACL filter: %T %#v", subject, subject))
|
||||||
|
@ -319,13 +322,11 @@ func (f *Filter) filterNodeServiceList(services *structs.NodeServiceList) bool {
|
||||||
// true if any elements were removed.
|
// true if any elements were removed.
|
||||||
func (f *Filter) filterCheckServiceNodes(nodes *structs.CheckServiceNodes) bool {
|
func (f *Filter) filterCheckServiceNodes(nodes *structs.CheckServiceNodes) bool {
|
||||||
csn := *nodes
|
csn := *nodes
|
||||||
var authzContext acl.AuthorizerContext
|
|
||||||
var removed bool
|
var removed bool
|
||||||
|
|
||||||
for i := 0; i < len(csn); i++ {
|
for i := 0; i < len(csn); i++ {
|
||||||
node := csn[i]
|
node := csn[i]
|
||||||
node.Service.FillAuthzContext(&authzContext)
|
if node.CanRead(f.authorizer) == acl.Allow {
|
||||||
if f.allowNode(node.Node.Node, &authzContext) && f.allowService(node.Service.Service, &authzContext) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node.Node.Node, node.Node.GetEnterpriseMeta()))
|
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node.Node.Node, node.Node.GetEnterpriseMeta()))
|
||||||
|
|
|
@ -1107,12 +1107,51 @@ func TestACL_filterIndexedNodesWithGateways(t *testing.T) {
|
||||||
{Service: structs.ServiceNameFromString("foo")},
|
{Service: structs.ServiceNameFromString("foo")},
|
||||||
{Service: structs.ServiceNameFromString("bar")},
|
{Service: structs.ServiceNameFromString("bar")},
|
||||||
},
|
},
|
||||||
|
ImportedNodes: structs.CheckServiceNodes{
|
||||||
|
{
|
||||||
|
Node: &structs.Node{
|
||||||
|
Node: "imported-node",
|
||||||
|
PeerName: "cluster-02",
|
||||||
|
},
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "zip",
|
||||||
|
Service: "zip",
|
||||||
|
PeerName: "cluster-02",
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
{
|
||||||
|
Node: "node1",
|
||||||
|
CheckID: "check1",
|
||||||
|
ServiceName: "zip",
|
||||||
|
PeerName: "cluster-02",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("allowed", func(t *testing.T) {
|
type testCase struct {
|
||||||
|
authzFn func() acl.Authorizer
|
||||||
|
expect *structs.IndexedNodesWithGateways
|
||||||
|
}
|
||||||
|
|
||||||
policy, err := acl.NewPolicyFromSource(`
|
run := func(t *testing.T, tc testCase) {
|
||||||
|
authz := tc.authzFn()
|
||||||
|
|
||||||
|
list := makeList()
|
||||||
|
New(authz, logger).Filter(list)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expect, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
tt := map[string]testCase{
|
||||||
|
"not filtered": {
|
||||||
|
authzFn: func() acl.Authorizer {
|
||||||
|
policy, err := acl.NewPolicyFromSource(`
|
||||||
|
service "baz" {
|
||||||
|
policy = "write"
|
||||||
|
}
|
||||||
service "foo" {
|
service "foo" {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
}
|
}
|
||||||
|
@ -1123,22 +1162,63 @@ func TestACL_filterIndexedNodesWithGateways(t *testing.T) {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
}
|
}
|
||||||
`, acl.SyntaxLegacy, nil, nil)
|
`, acl.SyntaxLegacy, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
return authz
|
||||||
list := makeList()
|
},
|
||||||
New(authz, logger).Filter(list)
|
expect: &structs.IndexedNodesWithGateways{
|
||||||
|
Nodes: structs.CheckServiceNodes{
|
||||||
require.Len(t, list.Nodes, 1)
|
{
|
||||||
require.Len(t, list.Gateways, 2)
|
Node: &structs.Node{
|
||||||
require.False(t, list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
|
Node: "node1",
|
||||||
})
|
},
|
||||||
|
Service: &structs.NodeService{
|
||||||
t.Run("not allowed to read the node", func(t *testing.T) {
|
ID: "foo",
|
||||||
|
Service: "foo",
|
||||||
policy, err := acl.NewPolicyFromSource(`
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
{
|
||||||
|
Node: "node1",
|
||||||
|
CheckID: "check1",
|
||||||
|
ServiceName: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Gateways: structs.GatewayServices{
|
||||||
|
{Service: structs.ServiceNameFromString("foo")},
|
||||||
|
{Service: structs.ServiceNameFromString("bar")},
|
||||||
|
},
|
||||||
|
// Service write to "bar" allows reading all imported services
|
||||||
|
ImportedNodes: structs.CheckServiceNodes{
|
||||||
|
{
|
||||||
|
Node: &structs.Node{
|
||||||
|
Node: "imported-node",
|
||||||
|
PeerName: "cluster-02",
|
||||||
|
},
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "zip",
|
||||||
|
Service: "zip",
|
||||||
|
PeerName: "cluster-02",
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
{
|
||||||
|
Node: "node1",
|
||||||
|
CheckID: "check1",
|
||||||
|
ServiceName: "zip",
|
||||||
|
PeerName: "cluster-02",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QueryMeta: structs.QueryMeta{ResultsFilteredByACLs: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"not allowed to read the node": {
|
||||||
|
authzFn: func() acl.Authorizer {
|
||||||
|
policy, err := acl.NewPolicyFromSource(`
|
||||||
service "foo" {
|
service "foo" {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
}
|
}
|
||||||
|
@ -1146,22 +1226,25 @@ func TestACL_filterIndexedNodesWithGateways(t *testing.T) {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
}
|
}
|
||||||
`, acl.SyntaxLegacy, nil, nil)
|
`, acl.SyntaxLegacy, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
return authz
|
||||||
list := makeList()
|
},
|
||||||
New(authz, logger).Filter(list)
|
expect: &structs.IndexedNodesWithGateways{
|
||||||
|
Nodes: structs.CheckServiceNodes{},
|
||||||
require.Empty(t, list.Nodes)
|
Gateways: structs.GatewayServices{
|
||||||
require.Len(t, list.Gateways, 2)
|
{Service: structs.ServiceNameFromString("foo")},
|
||||||
require.True(t, list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
|
{Service: structs.ServiceNameFromString("bar")},
|
||||||
})
|
},
|
||||||
|
ImportedNodes: structs.CheckServiceNodes{},
|
||||||
t.Run("allowed to read the node, but not the service", func(t *testing.T) {
|
QueryMeta: structs.QueryMeta{ResultsFilteredByACLs: true},
|
||||||
|
},
|
||||||
policy, err := acl.NewPolicyFromSource(`
|
},
|
||||||
|
"not allowed to read the service": {
|
||||||
|
authzFn: func() acl.Authorizer {
|
||||||
|
policy, err := acl.NewPolicyFromSource(`
|
||||||
node "node1" {
|
node "node1" {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
}
|
}
|
||||||
|
@ -1169,22 +1252,24 @@ func TestACL_filterIndexedNodesWithGateways(t *testing.T) {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
}
|
}
|
||||||
`, acl.SyntaxLegacy, nil, nil)
|
`, acl.SyntaxLegacy, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
return authz
|
||||||
list := makeList()
|
},
|
||||||
New(authz, logger).Filter(list)
|
expect: &structs.IndexedNodesWithGateways{
|
||||||
|
Nodes: structs.CheckServiceNodes{},
|
||||||
require.Empty(t, list.Nodes)
|
Gateways: structs.GatewayServices{
|
||||||
require.Len(t, list.Gateways, 1)
|
{Service: structs.ServiceNameFromString("bar")},
|
||||||
require.True(t, list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
|
},
|
||||||
})
|
ImportedNodes: structs.CheckServiceNodes{},
|
||||||
|
QueryMeta: structs.QueryMeta{ResultsFilteredByACLs: true},
|
||||||
t.Run("not allowed to read the other gatway service", func(t *testing.T) {
|
},
|
||||||
|
},
|
||||||
policy, err := acl.NewPolicyFromSource(`
|
"not allowed to read the other gateway service": {
|
||||||
|
authzFn: func() acl.Authorizer {
|
||||||
|
policy, err := acl.NewPolicyFromSource(`
|
||||||
service "foo" {
|
service "foo" {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
}
|
}
|
||||||
|
@ -1192,28 +1277,54 @@ func TestACL_filterIndexedNodesWithGateways(t *testing.T) {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
}
|
}
|
||||||
`, acl.SyntaxLegacy, nil, nil)
|
`, acl.SyntaxLegacy, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
return authz
|
||||||
|
},
|
||||||
|
expect: &structs.IndexedNodesWithGateways{
|
||||||
|
Nodes: structs.CheckServiceNodes{
|
||||||
|
{
|
||||||
|
Node: &structs.Node{
|
||||||
|
Node: "node1",
|
||||||
|
},
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "foo",
|
||||||
|
Service: "foo",
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
{
|
||||||
|
Node: "node1",
|
||||||
|
CheckID: "check1",
|
||||||
|
ServiceName: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Gateways: structs.GatewayServices{
|
||||||
|
{Service: structs.ServiceNameFromString("foo")},
|
||||||
|
},
|
||||||
|
ImportedNodes: structs.CheckServiceNodes{},
|
||||||
|
QueryMeta: structs.QueryMeta{ResultsFilteredByACLs: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"denied": {
|
||||||
|
authzFn: acl.DenyAll,
|
||||||
|
expect: &structs.IndexedNodesWithGateways{
|
||||||
|
Nodes: structs.CheckServiceNodes{},
|
||||||
|
Gateways: structs.GatewayServices{},
|
||||||
|
ImportedNodes: structs.CheckServiceNodes{},
|
||||||
|
QueryMeta: structs.QueryMeta{ResultsFilteredByACLs: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
list := makeList()
|
for name, tc := range tt {
|
||||||
New(authz, logger).Filter(list)
|
t.Run(name, func(t *testing.T) {
|
||||||
|
run(t, tc)
|
||||||
require.Len(t, list.Nodes, 1)
|
})
|
||||||
require.Len(t, list.Gateways, 1)
|
}
|
||||||
require.True(t, list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("denied", func(t *testing.T) {
|
|
||||||
|
|
||||||
list := makeList()
|
|
||||||
New(acl.DenyAll(), logger).Filter(list)
|
|
||||||
|
|
||||||
require.Empty(t, list.Nodes)
|
|
||||||
require.Empty(t, list.Gateways)
|
|
||||||
require.True(t, list.QueryMeta.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestACL_filterIndexedServiceDump(t *testing.T) {
|
func TestACL_filterIndexedServiceDump(t *testing.T) {
|
||||||
|
|
|
@ -1998,6 +1998,15 @@ func (csn *CheckServiceNode) CanRead(authz acl.Authorizer) acl.EnforcementDecisi
|
||||||
authzContext := new(acl.AuthorizerContext)
|
authzContext := new(acl.AuthorizerContext)
|
||||||
csn.Service.EnterpriseMeta.FillAuthzContext(authzContext)
|
csn.Service.EnterpriseMeta.FillAuthzContext(authzContext)
|
||||||
|
|
||||||
|
if csn.Node.PeerName != "" || csn.Service.PeerName != "" {
|
||||||
|
if authz.ServiceReadAll(authzContext) == acl.Allow ||
|
||||||
|
authz.ServiceWriteAny(authzContext) == acl.Allow {
|
||||||
|
|
||||||
|
return acl.Allow
|
||||||
|
}
|
||||||
|
return acl.Deny
|
||||||
|
}
|
||||||
|
|
||||||
if authz.NodeRead(csn.Node.Node, authzContext) != acl.Allow {
|
if authz.NodeRead(csn.Node.Node, authzContext) != acl.Allow {
|
||||||
return acl.Deny
|
return acl.Deny
|
||||||
}
|
}
|
||||||
|
|
|
@ -1759,6 +1759,33 @@ func TestCheckServiceNode_CanRead(t *testing.T) {
|
||||||
authz: acl.AllowAll(),
|
authz: acl.AllowAll(),
|
||||||
expected: acl.Allow,
|
expected: acl.Allow,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "can read imported csn if can read all",
|
||||||
|
csn: CheckServiceNode{
|
||||||
|
Node: &Node{Node: "name", PeerName: "cluster-2"},
|
||||||
|
Service: &NodeService{Service: "service-name", PeerName: "cluster-2"},
|
||||||
|
},
|
||||||
|
authz: aclAuthorizerCheckServiceNode{allowReadAllServices: true},
|
||||||
|
expected: acl.Allow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "can read imported csn if can write any",
|
||||||
|
csn: CheckServiceNode{
|
||||||
|
Node: &Node{Node: "name", PeerName: "cluster-2"},
|
||||||
|
Service: &NodeService{Service: "service-name", PeerName: "cluster-2"},
|
||||||
|
},
|
||||||
|
authz: aclAuthorizerCheckServiceNode{allowServiceWrite: true},
|
||||||
|
expected: acl.Allow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "can't read imported csn with authz for local services and nodes",
|
||||||
|
csn: CheckServiceNode{
|
||||||
|
Node: &Node{Node: "name", PeerName: "cluster-2"},
|
||||||
|
Service: &NodeService{Service: "service-name", PeerName: "cluster-2"},
|
||||||
|
},
|
||||||
|
authz: aclAuthorizerCheckServiceNode{allowService: true, allowNode: true},
|
||||||
|
expected: acl.Deny,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
@ -1769,8 +1796,10 @@ func TestCheckServiceNode_CanRead(t *testing.T) {
|
||||||
|
|
||||||
type aclAuthorizerCheckServiceNode struct {
|
type aclAuthorizerCheckServiceNode struct {
|
||||||
acl.Authorizer
|
acl.Authorizer
|
||||||
allowNode bool
|
allowNode bool
|
||||||
allowService bool
|
allowService bool
|
||||||
|
allowServiceWrite bool
|
||||||
|
allowReadAllServices bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a aclAuthorizerCheckServiceNode) ServiceRead(string, *acl.AuthorizerContext) acl.EnforcementDecision {
|
func (a aclAuthorizerCheckServiceNode) ServiceRead(string, *acl.AuthorizerContext) acl.EnforcementDecision {
|
||||||
|
@ -1787,6 +1816,20 @@ func (a aclAuthorizerCheckServiceNode) NodeRead(string, *acl.AuthorizerContext)
|
||||||
return acl.Deny
|
return acl.Deny
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a aclAuthorizerCheckServiceNode) ServiceReadAll(*acl.AuthorizerContext) acl.EnforcementDecision {
|
||||||
|
if a.allowReadAllServices {
|
||||||
|
return acl.Allow
|
||||||
|
}
|
||||||
|
return acl.Deny
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a aclAuthorizerCheckServiceNode) ServiceWriteAny(*acl.AuthorizerContext) acl.EnforcementDecision {
|
||||||
|
if a.allowServiceWrite {
|
||||||
|
return acl.Allow
|
||||||
|
}
|
||||||
|
return acl.Deny
|
||||||
|
}
|
||||||
|
|
||||||
func TestStructs_DirEntry_Clone(t *testing.T) {
|
func TestStructs_DirEntry_Clone(t *testing.T) {
|
||||||
e := &DirEntry{
|
e := &DirEntry{
|
||||||
LockIndex: 5,
|
LockIndex: 5,
|
||||||
|
|
Loading…
Reference in New Issue