mirror of https://github.com/hashicorp/consul
aahel
12 months ago
committed by
GitHub
21 changed files with 611 additions and 142 deletions
@ -0,0 +1,90 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package types |
||||
|
||||
import ( |
||||
"github.com/hashicorp/go-multierror" |
||||
|
||||
"github.com/hashicorp/consul/acl" |
||||
"github.com/hashicorp/consul/internal/resource" |
||||
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" |
||||
"github.com/hashicorp/consul/proto-public/pbresource" |
||||
) |
||||
|
||||
type DecodedNodeHealthStatus = resource.DecodedResource[*pbcatalog.NodeHealthStatus] |
||||
|
||||
func RegisterNodeHealthStatus(r resource.Registry) { |
||||
r.Register(resource.Registration{ |
||||
Type: pbcatalog.NodeHealthStatusType, |
||||
Proto: &pbcatalog.NodeHealthStatus{}, |
||||
Scope: resource.ScopePartition, |
||||
Validate: ValidateNodeHealthStatus, |
||||
ACLs: &resource.ACLHooks{ |
||||
Read: resource.AuthorizeReadWithResource(aclReadHookNodeHealthStatus), |
||||
Write: aclWriteHookNodeHealthStatus, |
||||
List: resource.NoOpACLListHook, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
var ValidateNodeHealthStatus = resource.DecodeAndValidate(validateNodeHealthStatus) |
||||
|
||||
func validateNodeHealthStatus(res *DecodedNodeHealthStatus) error { |
||||
var err error |
||||
|
||||
// Should we allow empty types? I think for now it will be safest to require
|
||||
// the type field is set and we can relax this restriction in the future
|
||||
// if we deem it desirable.
|
||||
if res.Data.Type == "" { |
||||
err = multierror.Append(err, resource.ErrInvalidField{ |
||||
Name: "type", |
||||
Wrapped: resource.ErrMissing, |
||||
}) |
||||
} |
||||
|
||||
switch res.Data.Status { |
||||
case pbcatalog.Health_HEALTH_PASSING, |
||||
pbcatalog.Health_HEALTH_WARNING, |
||||
pbcatalog.Health_HEALTH_CRITICAL, |
||||
pbcatalog.Health_HEALTH_MAINTENANCE: |
||||
default: |
||||
err = multierror.Append(err, resource.ErrInvalidField{ |
||||
Name: "status", |
||||
Wrapped: errInvalidHealth, |
||||
}) |
||||
} |
||||
|
||||
// Ensure that the NodeHealthStatus' owner is a type that we want to allow. The
|
||||
// owner is currently the resource that this NodeHealthStatus applies to. If we
|
||||
// change this to be a parent reference within the NodeHealthStatus.Data then
|
||||
// we could allow for other owners.
|
||||
if res.Resource.Owner == nil { |
||||
err = multierror.Append(err, resource.ErrInvalidField{ |
||||
Name: "owner", |
||||
Wrapped: resource.ErrMissing, |
||||
}) |
||||
} else if !resource.EqualType(res.Owner.Type, pbcatalog.NodeType) { |
||||
err = multierror.Append(err, resource.ErrOwnerTypeInvalid{ResourceType: res.Id.Type, OwnerType: res.Owner.Type}) |
||||
} |
||||
|
||||
return err |
||||
} |
||||
|
||||
func aclReadHookNodeHealthStatus(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, res *pbresource.Resource) error { |
||||
// For a health status of a node we need to check node:read perms.
|
||||
if res.GetOwner() != nil && resource.EqualType(res.GetOwner().GetType(), pbcatalog.NodeType) { |
||||
return authorizer.ToAllowAuthorizer().NodeReadAllowed(res.GetOwner().GetName(), authzContext) |
||||
} |
||||
|
||||
return acl.PermissionDenied("cannot read catalog.NodeHealthStatus because there is no owner") |
||||
} |
||||
|
||||
func aclWriteHookNodeHealthStatus(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, res *pbresource.Resource) error { |
||||
// For a health status of a node we need to check node:write perms.
|
||||
if res.GetOwner() != nil && resource.EqualType(res.GetOwner().GetType(), pbcatalog.NodeType) { |
||||
return authorizer.ToAllowAuthorizer().NodeWriteAllowed(res.GetOwner().GetName(), authzContext) |
||||
} |
||||
|
||||
return acl.PermissionDenied("cannot write catalog.NodeHealthStatus because there is no owner") |
||||
} |
@ -0,0 +1,273 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package types |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/hashicorp/consul/internal/resource" |
||||
"github.com/hashicorp/consul/internal/resource/resourcetest" |
||||
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" |
||||
"github.com/hashicorp/consul/proto-public/pbresource" |
||||
"github.com/stretchr/testify/require" |
||||
"google.golang.org/protobuf/reflect/protoreflect" |
||||
"google.golang.org/protobuf/types/known/anypb" |
||||
) |
||||
|
||||
var ( |
||||
defaultNodeHealthStatusOwnerTenancy = &pbresource.Tenancy{ |
||||
Partition: "default", |
||||
PeerName: "local", |
||||
} |
||||
|
||||
defaultNodeHealthStatusOwner = &pbresource.ID{ |
||||
Type: pbcatalog.NodeType, |
||||
Tenancy: defaultNodeHealthStatusOwnerTenancy, |
||||
Name: "foo", |
||||
} |
||||
) |
||||
|
||||
func createNodeHealthStatusResource(t *testing.T, data protoreflect.ProtoMessage, owner *pbresource.ID) *pbresource.Resource { |
||||
res := &pbresource.Resource{ |
||||
Id: &pbresource.ID{ |
||||
Type: pbcatalog.NodeHealthStatusType, |
||||
Tenancy: &pbresource.Tenancy{ |
||||
Partition: "default", |
||||
PeerName: "local", |
||||
}, |
||||
Name: "test-status", |
||||
}, |
||||
Owner: owner, |
||||
} |
||||
|
||||
var err error |
||||
res.Data, err = anypb.New(data) |
||||
require.NoError(t, err) |
||||
return res |
||||
} |
||||
|
||||
func TestValidateNodeHealthStatus_Ok(t *testing.T) { |
||||
data := &pbcatalog.NodeHealthStatus{ |
||||
Type: "tcp", |
||||
Status: pbcatalog.Health_HEALTH_PASSING, |
||||
Description: "Doesn't matter as this is user settable", |
||||
Output: "Health check executors are free to use this field", |
||||
} |
||||
|
||||
type testCase struct { |
||||
owner *pbresource.ID |
||||
} |
||||
|
||||
cases := map[string]testCase{ |
||||
"node-owned": { |
||||
owner: &pbresource.ID{ |
||||
Type: pbcatalog.NodeType, |
||||
Tenancy: defaultNodeHealthStatusOwnerTenancy, |
||||
Name: "bar-node", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for name, tcase := range cases { |
||||
t.Run(name, func(t *testing.T) { |
||||
res := createNodeHealthStatusResource(t, data, tcase.owner) |
||||
err := ValidateNodeHealthStatus(res) |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestValidateNodeHealthStatus_ParseError(t *testing.T) { |
||||
// Any type other than the NodeHealthStatus type would work
|
||||
// to cause the error we are expecting
|
||||
data := &pbcatalog.IP{Address: "198.18.0.1"} |
||||
|
||||
res := createNodeHealthStatusResource(t, data, defaultNodeHealthStatusOwner) |
||||
|
||||
err := ValidateNodeHealthStatus(res) |
||||
require.Error(t, err) |
||||
require.ErrorAs(t, err, &resource.ErrDataParse{}) |
||||
} |
||||
|
||||
func TestValidateNodeHealthStatus_InvalidHealth(t *testing.T) { |
||||
// while this is a valid enum value it is not allowed to be used
|
||||
// as the Status field.
|
||||
data := &pbcatalog.NodeHealthStatus{ |
||||
Type: "tcp", |
||||
Status: pbcatalog.Health_HEALTH_ANY, |
||||
} |
||||
|
||||
res := createNodeHealthStatusResource(t, data, defaultNodeHealthStatusOwner) |
||||
|
||||
err := ValidateNodeHealthStatus(res) |
||||
require.Error(t, err) |
||||
expected := resource.ErrInvalidField{ |
||||
Name: "status", |
||||
Wrapped: errInvalidHealth, |
||||
} |
||||
var actual resource.ErrInvalidField |
||||
require.ErrorAs(t, err, &actual) |
||||
require.Equal(t, expected, actual) |
||||
} |
||||
|
||||
func TestValidateNodeHealthStatus_MissingType(t *testing.T) { |
||||
data := &pbcatalog.NodeHealthStatus{ |
||||
Status: pbcatalog.Health_HEALTH_PASSING, |
||||
} |
||||
|
||||
res := createNodeHealthStatusResource(t, data, defaultNodeHealthStatusOwner) |
||||
|
||||
err := ValidateNodeHealthStatus(res) |
||||
require.Error(t, err) |
||||
expected := resource.ErrInvalidField{ |
||||
Name: "type", |
||||
Wrapped: resource.ErrMissing, |
||||
} |
||||
var actual resource.ErrInvalidField |
||||
require.ErrorAs(t, err, &actual) |
||||
require.Equal(t, expected, actual) |
||||
} |
||||
|
||||
func TestValidateNodeHealthStatus_MissingOwner(t *testing.T) { |
||||
data := &pbcatalog.NodeHealthStatus{ |
||||
Type: "tcp", |
||||
Status: pbcatalog.Health_HEALTH_PASSING, |
||||
} |
||||
|
||||
res := createNodeHealthStatusResource(t, data, nil) |
||||
|
||||
err := ValidateNodeHealthStatus(res) |
||||
require.Error(t, err) |
||||
expected := resource.ErrInvalidField{ |
||||
Name: "owner", |
||||
Wrapped: resource.ErrMissing, |
||||
} |
||||
var actual resource.ErrInvalidField |
||||
require.ErrorAs(t, err, &actual) |
||||
require.Equal(t, expected, actual) |
||||
} |
||||
|
||||
func TestValidateNodeHealthStatus_InvalidOwner(t *testing.T) { |
||||
data := &pbcatalog.NodeHealthStatus{ |
||||
Type: "tcp", |
||||
Status: pbcatalog.Health_HEALTH_PASSING, |
||||
} |
||||
|
||||
type testCase struct { |
||||
owner *pbresource.ID |
||||
} |
||||
|
||||
cases := map[string]testCase{ |
||||
"group-mismatch": { |
||||
owner: &pbresource.ID{ |
||||
Type: &pbresource.Type{ |
||||
Group: "fake", |
||||
GroupVersion: pbcatalog.Version, |
||||
Kind: pbcatalog.NodeKind, |
||||
}, |
||||
Tenancy: defaultNodeHealthStatusOwnerTenancy, |
||||
Name: "baz", |
||||
}, |
||||
}, |
||||
"group-version-mismatch": { |
||||
owner: &pbresource.ID{ |
||||
Type: &pbresource.Type{ |
||||
Group: pbcatalog.GroupName, |
||||
GroupVersion: "v99", |
||||
Kind: pbcatalog.NodeKind, |
||||
}, |
||||
Tenancy: defaultNodeHealthStatusOwnerTenancy, |
||||
Name: "baz", |
||||
}, |
||||
}, |
||||
"kind-mismatch": { |
||||
owner: &pbresource.ID{ |
||||
Type: pbcatalog.ServiceType, |
||||
Tenancy: defaultNodeHealthStatusOwnerTenancy, |
||||
Name: "baz", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for name, tcase := range cases { |
||||
t.Run(name, func(t *testing.T) { |
||||
res := createNodeHealthStatusResource(t, data, tcase.owner) |
||||
err := ValidateNodeHealthStatus(res) |
||||
require.Error(t, err) |
||||
expected := resource.ErrOwnerTypeInvalid{ |
||||
ResourceType: pbcatalog.NodeHealthStatusType, |
||||
OwnerType: tcase.owner.Type, |
||||
} |
||||
var actual resource.ErrOwnerTypeInvalid |
||||
require.ErrorAs(t, err, &actual) |
||||
require.Equal(t, expected, actual) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestNodeHealthStatusACLs(t *testing.T) { |
||||
registry := resource.NewRegistry() |
||||
Register(registry) |
||||
|
||||
node := resourcetest.Resource(pbcatalog.NodeType, "test").ID() |
||||
|
||||
nodehealthStatusData := &pbcatalog.NodeHealthStatus{ |
||||
Type: "tcp", |
||||
Status: pbcatalog.Health_HEALTH_PASSING, |
||||
} |
||||
|
||||
cases := map[string]resourcetest.ACLTestCase{ |
||||
"no rules": { |
||||
Rules: ``, |
||||
Data: nodehealthStatusData, |
||||
Owner: node, |
||||
Typ: pbcatalog.NodeHealthStatusType, |
||||
ReadOK: resourcetest.DENY, |
||||
WriteOK: resourcetest.DENY, |
||||
ListOK: resourcetest.DEFAULT, |
||||
}, |
||||
"service test read with node owner": { |
||||
Rules: `service "test" { policy = "read" }`, |
||||
Data: nodehealthStatusData, |
||||
Owner: node, |
||||
Typ: pbcatalog.NodeHealthStatusType, |
||||
ReadOK: resourcetest.DENY, |
||||
WriteOK: resourcetest.DENY, |
||||
ListOK: resourcetest.DEFAULT, |
||||
}, |
||||
"service test write with node owner": { |
||||
Rules: `service "test" { policy = "write" }`, |
||||
Data: nodehealthStatusData, |
||||
Owner: node, |
||||
Typ: pbcatalog.NodeHealthStatusType, |
||||
ReadOK: resourcetest.DENY, |
||||
WriteOK: resourcetest.DENY, |
||||
ListOK: resourcetest.DEFAULT, |
||||
}, |
||||
"node test read with node owner": { |
||||
Rules: `node "test" { policy = "read" }`, |
||||
Data: nodehealthStatusData, |
||||
Owner: node, |
||||
Typ: pbcatalog.NodeHealthStatusType, |
||||
ReadOK: resourcetest.ALLOW, |
||||
WriteOK: resourcetest.DENY, |
||||
ListOK: resourcetest.DEFAULT, |
||||
}, |
||||
"node test write with node owner": { |
||||
Rules: `node "test" { policy = "write" }`, |
||||
Data: nodehealthStatusData, |
||||
Owner: node, |
||||
Typ: pbcatalog.NodeHealthStatusType, |
||||
ReadOK: resourcetest.ALLOW, |
||||
WriteOK: resourcetest.ALLOW, |
||||
ListOK: resourcetest.DEFAULT, |
||||
}, |
||||
} |
||||
|
||||
for name, tc := range cases { |
||||
t.Run(name, func(t *testing.T) { |
||||
resourcetest.RunACLTestCase(t, tc, registry) |
||||
}) |
||||
} |
||||
} |
Loading…
Reference in new issue