mirror of https://github.com/status-im/consul.git
mesh: add ACL checks for xRoute resources (#18926)
xRoute resources are not name-aligned with the Services they control. They have a list of "parent ref" services that they alter traffic flow for, and they contain a list of "backend ref" services that they direct that traffic to. The ACLs should be: - list: (default) - read: - ALL service:<parent_ref_service>:read - write: - ALL service:<parent_ref_service>:write - ALL service:<backend_ref_service>:read
This commit is contained in:
parent
43a8dbb188
commit
633c6c9458
|
@ -35,6 +35,7 @@ func RegisterGRPCRoute(r resource.Registry) {
|
|||
Scope: resource.ScopeNamespace,
|
||||
Mutate: MutateGRPCRoute,
|
||||
Validate: ValidateGRPCRoute,
|
||||
ACLs: xRouteACLHooks[*pbmesh.GRPCRoute](),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/hashicorp/consul/internal/catalog"
|
||||
"github.com/hashicorp/consul/internal/resource"
|
||||
"github.com/hashicorp/consul/internal/resource/resourcetest"
|
||||
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
|
||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||
|
@ -614,3 +615,33 @@ func TestValidateGRPCRoute(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGRPCRouteACLs(t *testing.T) {
|
||||
testXRouteACLs[*pbmesh.GRPCRoute](t, func(t *testing.T, parentRefs, backendRefs []*pbresource.Reference) *pbresource.Resource {
|
||||
data := &pbmesh.GRPCRoute{
|
||||
ParentRefs: nil,
|
||||
}
|
||||
for _, ref := range parentRefs {
|
||||
data.ParentRefs = append(data.ParentRefs, &pbmesh.ParentReference{
|
||||
Ref: ref,
|
||||
})
|
||||
}
|
||||
|
||||
var ruleRefs []*pbmesh.GRPCBackendRef
|
||||
for _, ref := range backendRefs {
|
||||
ruleRefs = append(ruleRefs, &pbmesh.GRPCBackendRef{
|
||||
BackendRef: &pbmesh.BackendReference{
|
||||
Ref: ref,
|
||||
},
|
||||
})
|
||||
}
|
||||
data.Rules = []*pbmesh.GRPCRouteRule{
|
||||
{BackendRefs: ruleRefs},
|
||||
}
|
||||
|
||||
return resourcetest.Resource(GRPCRouteType, "api-grpc-route").
|
||||
WithTenancy(resource.DefaultNamespacedTenancy()).
|
||||
WithData(t, data).
|
||||
Build()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ func RegisterHTTPRoute(r resource.Registry) {
|
|||
Scope: resource.ScopeNamespace,
|
||||
Mutate: MutateHTTPRoute,
|
||||
Validate: ValidateHTTPRoute,
|
||||
ACLs: xRouteACLHooks[*pbmesh.HTTPRoute](),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1174,3 +1174,33 @@ func newTestTenancy(s string) *pbresource.Tenancy {
|
|||
return &pbresource.Tenancy{Partition: "BAD", Namespace: "BAD", PeerName: "BAD"}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPRouteACLs(t *testing.T) {
|
||||
testXRouteACLs[*pbmesh.HTTPRoute](t, func(t *testing.T, parentRefs, backendRefs []*pbresource.Reference) *pbresource.Resource {
|
||||
data := &pbmesh.HTTPRoute{
|
||||
ParentRefs: nil,
|
||||
}
|
||||
for _, ref := range parentRefs {
|
||||
data.ParentRefs = append(data.ParentRefs, &pbmesh.ParentReference{
|
||||
Ref: ref,
|
||||
})
|
||||
}
|
||||
|
||||
var ruleRefs []*pbmesh.HTTPBackendRef
|
||||
for _, ref := range backendRefs {
|
||||
ruleRefs = append(ruleRefs, &pbmesh.HTTPBackendRef{
|
||||
BackendRef: &pbmesh.BackendReference{
|
||||
Ref: ref,
|
||||
},
|
||||
})
|
||||
}
|
||||
data.Rules = []*pbmesh.HTTPRouteRule{
|
||||
{BackendRefs: ruleRefs},
|
||||
}
|
||||
|
||||
return resourcetest.Resource(HTTPRouteType, "api-http-route").
|
||||
WithTenancy(resource.DefaultNamespacedTenancy()).
|
||||
WithData(t, data).
|
||||
Build()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ func RegisterTCPRoute(r resource.Registry) {
|
|||
Scope: resource.ScopeNamespace,
|
||||
Mutate: MutateTCPRoute,
|
||||
Validate: ValidateTCPRoute,
|
||||
ACLs: xRouteACLHooks[*pbmesh.TCPRoute](),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -200,3 +200,33 @@ func TestValidateTCPRoute(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPRouteACLs(t *testing.T) {
|
||||
testXRouteACLs[*pbmesh.TCPRoute](t, func(t *testing.T, parentRefs, backendRefs []*pbresource.Reference) *pbresource.Resource {
|
||||
data := &pbmesh.TCPRoute{
|
||||
ParentRefs: nil,
|
||||
}
|
||||
for _, ref := range parentRefs {
|
||||
data.ParentRefs = append(data.ParentRefs, &pbmesh.ParentReference{
|
||||
Ref: ref,
|
||||
})
|
||||
}
|
||||
|
||||
var ruleRefs []*pbmesh.TCPBackendRef
|
||||
for _, ref := range backendRefs {
|
||||
ruleRefs = append(ruleRefs, &pbmesh.TCPBackendRef{
|
||||
BackendRef: &pbmesh.BackendReference{
|
||||
Ref: ref,
|
||||
},
|
||||
})
|
||||
}
|
||||
data.Rules = []*pbmesh.TCPRouteRule{
|
||||
{BackendRefs: ruleRefs},
|
||||
}
|
||||
|
||||
return resourcetest.Resource(TCPRouteType, "api-tcp-route").
|
||||
WithTenancy(resource.DefaultNamespacedTenancy()).
|
||||
WithData(t, data).
|
||||
Build()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/hashicorp/go-multierror"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/internal/catalog"
|
||||
"github.com/hashicorp/consul/internal/resource"
|
||||
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
|
||||
|
@ -282,3 +283,75 @@ func isValidRetryCondition(retryOn string) bool {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func xRouteACLHooks[R XRouteData]() *resource.ACLHooks {
|
||||
hooks := &resource.ACLHooks{
|
||||
Read: aclReadHookXRoute[R],
|
||||
Write: aclWriteHookXRoute[R],
|
||||
List: aclListHookXRoute[R],
|
||||
}
|
||||
|
||||
return hooks
|
||||
}
|
||||
|
||||
func aclReadHookXRoute[R XRouteData](authorizer acl.Authorizer, _ *acl.AuthorizerContext, _ *pbresource.ID, res *pbresource.Resource) error {
|
||||
if res == nil {
|
||||
return resource.ErrNeedData
|
||||
}
|
||||
|
||||
dec, err := resource.Decode[R](res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
route := dec.Data
|
||||
|
||||
// Need service:read on ALL of the services this is controlling traffic for.
|
||||
for _, parentRef := range route.GetParentRefs() {
|
||||
parentAuthzContext := resource.AuthorizerContext(parentRef.Ref.GetTenancy())
|
||||
parentServiceName := parentRef.Ref.GetName()
|
||||
|
||||
if err := authorizer.ToAllowAuthorizer().ServiceReadAllowed(parentServiceName, parentAuthzContext); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func aclWriteHookXRoute[R XRouteData](authorizer acl.Authorizer, _ *acl.AuthorizerContext, res *pbresource.Resource) error {
|
||||
dec, err := resource.Decode[R](res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
route := dec.Data
|
||||
|
||||
// Need service:write on ALL of the services this is controlling traffic for.
|
||||
for _, parentRef := range route.GetParentRefs() {
|
||||
parentAuthzContext := resource.AuthorizerContext(parentRef.Ref.GetTenancy())
|
||||
parentServiceName := parentRef.Ref.GetName()
|
||||
|
||||
if err := authorizer.ToAllowAuthorizer().ServiceWriteAllowed(parentServiceName, parentAuthzContext); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Need service:read on ALL of the services this directs traffic at.
|
||||
for _, backendRef := range route.GetUnderlyingBackendRefs() {
|
||||
backendAuthzContext := resource.AuthorizerContext(backendRef.Ref.GetTenancy())
|
||||
backendServiceName := backendRef.Ref.GetName()
|
||||
|
||||
if err := authorizer.ToAllowAuthorizer().ServiceReadAllowed(backendServiceName, backendAuthzContext); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func aclListHookXRoute[R XRouteData](authorizer acl.Authorizer, authzContext *acl.AuthorizerContext) error {
|
||||
// No-op List permission as we want to default to filtering resources
|
||||
// from the list using the Read enforcement.
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/internal/catalog"
|
||||
"github.com/hashicorp/consul/internal/resource"
|
||||
"github.com/hashicorp/consul/internal/resource/resourcetest"
|
||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||
)
|
||||
|
||||
func testXRouteACLs[R XRouteData](t *testing.T, newRoute func(t *testing.T, parentRefs, backendRefs []*pbresource.Reference) *pbresource.Resource) {
|
||||
// Wire up a registry to generically invoke hooks
|
||||
registry := resource.NewRegistry()
|
||||
Register(registry)
|
||||
|
||||
userNewRoute := newRoute
|
||||
newRoute = func(t *testing.T, parentRefs, backendRefs []*pbresource.Reference) *pbresource.Resource {
|
||||
res := userNewRoute(t, parentRefs, backendRefs)
|
||||
resourcetest.ValidateAndNormalize(t, registry, res)
|
||||
return res
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
res *pbresource.Resource
|
||||
rules string
|
||||
check func(t *testing.T, authz acl.Authorizer, res *pbresource.Resource)
|
||||
readOK string
|
||||
writeOK string
|
||||
}
|
||||
|
||||
const (
|
||||
DENY = "deny"
|
||||
ALLOW = "allow"
|
||||
DEFAULT = "default"
|
||||
)
|
||||
|
||||
checkF := func(t *testing.T, name string, expect string, got error) {
|
||||
switch expect {
|
||||
case ALLOW:
|
||||
if acl.IsErrPermissionDenied(got) {
|
||||
t.Fatal(name + " should be allowed")
|
||||
}
|
||||
case DENY:
|
||||
if !acl.IsErrPermissionDenied(got) {
|
||||
t.Fatal(name + " should be denied")
|
||||
}
|
||||
case DEFAULT:
|
||||
require.Nil(t, got, name+" expected fallthrough decision")
|
||||
default:
|
||||
t.Fatalf(name+" unexpected expectation: %q", expect)
|
||||
}
|
||||
}
|
||||
|
||||
resOneParentOneBackend := newRoute(t,
|
||||
[]*pbresource.Reference{
|
||||
newRef(catalog.ServiceType, "api1"),
|
||||
},
|
||||
[]*pbresource.Reference{
|
||||
newRef(catalog.ServiceType, "backend1"),
|
||||
},
|
||||
)
|
||||
resTwoParentsOneBackend := newRoute(t,
|
||||
[]*pbresource.Reference{
|
||||
newRef(catalog.ServiceType, "api1"),
|
||||
newRef(catalog.ServiceType, "api2"),
|
||||
},
|
||||
[]*pbresource.Reference{
|
||||
newRef(catalog.ServiceType, "backend1"),
|
||||
},
|
||||
)
|
||||
resOneParentTwoBackends := newRoute(t,
|
||||
[]*pbresource.Reference{
|
||||
newRef(catalog.ServiceType, "api1"),
|
||||
},
|
||||
[]*pbresource.Reference{
|
||||
newRef(catalog.ServiceType, "backend1"),
|
||||
newRef(catalog.ServiceType, "backend2"),
|
||||
},
|
||||
)
|
||||
resTwoParentsTwoBackends := newRoute(t,
|
||||
[]*pbresource.Reference{
|
||||
newRef(catalog.ServiceType, "api1"),
|
||||
newRef(catalog.ServiceType, "api2"),
|
||||
},
|
||||
[]*pbresource.Reference{
|
||||
newRef(catalog.ServiceType, "backend1"),
|
||||
newRef(catalog.ServiceType, "backend2"),
|
||||
},
|
||||
)
|
||||
|
||||
run := func(t *testing.T, name string, tc testcase) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
config := acl.Config{
|
||||
WildcardName: structs.WildcardSpecifier,
|
||||
}
|
||||
authz, err := acl.NewAuthorizerFromRules(tc.rules, &config, nil)
|
||||
require.NoError(t, err)
|
||||
authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()})
|
||||
|
||||
reg, ok := registry.Resolve(tc.res.Id.GetType())
|
||||
require.True(t, ok)
|
||||
|
||||
err = reg.ACLs.Read(authz, &acl.AuthorizerContext{}, tc.res.Id, nil)
|
||||
require.ErrorIs(t, err, resource.ErrNeedData, "read hook should require the data payload")
|
||||
|
||||
checkF(t, "read", tc.readOK, reg.ACLs.Read(authz, &acl.AuthorizerContext{}, tc.res.Id, tc.res))
|
||||
checkF(t, "write", tc.writeOK, reg.ACLs.Write(authz, &acl.AuthorizerContext{}, tc.res))
|
||||
checkF(t, "list", DEFAULT, reg.ACLs.List(authz, &acl.AuthorizerContext{}))
|
||||
})
|
||||
}
|
||||
|
||||
serviceRead := func(name string) string {
|
||||
return fmt.Sprintf(` service %q { policy = "read" } `, name)
|
||||
}
|
||||
serviceWrite := func(name string) string {
|
||||
return fmt.Sprintf(` service %q { policy = "write" } `, name)
|
||||
}
|
||||
|
||||
assert := func(t *testing.T, name string, rules string, res *pbresource.Resource, readOK, writeOK string) {
|
||||
tc := testcase{
|
||||
res: res,
|
||||
rules: rules,
|
||||
readOK: readOK,
|
||||
writeOK: writeOK,
|
||||
}
|
||||
run(t, name, tc)
|
||||
}
|
||||
|
||||
t.Run("no rules", func(t *testing.T) {
|
||||
rules := ``
|
||||
assert(t, "1parent 1backend", rules, resOneParentOneBackend, DENY, DENY)
|
||||
assert(t, "1parent 2backends", rules, resOneParentTwoBackends, DENY, DENY)
|
||||
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend, DENY, DENY)
|
||||
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends, DENY, DENY)
|
||||
})
|
||||
t.Run("api1:read", func(t *testing.T) {
|
||||
rules := serviceRead("api1")
|
||||
assert(t, "1parent 1backend", rules, resOneParentOneBackend, ALLOW, DENY)
|
||||
assert(t, "1parent 2backends", rules, resOneParentTwoBackends, ALLOW, DENY)
|
||||
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend, DENY, DENY)
|
||||
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends, DENY, DENY)
|
||||
})
|
||||
t.Run("api1:write", func(t *testing.T) {
|
||||
rules := serviceWrite("api1")
|
||||
assert(t, "1parent 1backend", rules, resOneParentOneBackend, ALLOW, DENY)
|
||||
assert(t, "1parent 2backends", rules, resOneParentTwoBackends, ALLOW, DENY)
|
||||
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend, DENY, DENY)
|
||||
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends, DENY, DENY)
|
||||
})
|
||||
t.Run("api1:write backend1:read", func(t *testing.T) {
|
||||
rules := serviceWrite("api1") + serviceRead("backend1")
|
||||
assert(t, "1parent 1backend", rules, resOneParentOneBackend, ALLOW, ALLOW)
|
||||
assert(t, "1parent 2backends", rules, resOneParentTwoBackends, ALLOW, DENY)
|
||||
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend, DENY, DENY)
|
||||
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends, DENY, DENY)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue