consul/internal/resource/hooks_test.go
Matt Keeler 5698353652
Resource Hook Pre-Decode Utilities (#18548)
Add some generic type hook wrappers to first decode the data

There seems to be a pattern for Validation, Mutation and Write Authorization hooks where they first need to decode the Any data before doing the domain specific work.

This PR introduces 3 new functions to generate wrappers around the other hooks to pre-decode the data into a DecodedResource and pass that in instead of the original pbresource.Resource.

This PR also updates the various catalog data types to use the new hook generators.
2023-10-26 16:39:06 -04:00

244 lines
7.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package resource_test
import (
"fmt"
"testing"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo"
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
"github.com/hashicorp/consul/proto-public/pbresource"
pbdemo "github.com/hashicorp/consul/proto/private/pbdemo/v2"
"github.com/stretchr/testify/require"
)
func TestDecodeAndValidate(t *testing.T) {
res := rtest.Resource(demo.TypeV2Artist, "babypants").
WithData(t, &pbdemo.Artist{Name: "caspar babypants"}).
Build()
t.Run("ok", func(t *testing.T) {
err := resource.DecodeAndValidate[*pbdemo.Artist](func(dec *resource.DecodedResource[*pbdemo.Artist]) error {
require.NotNil(t, dec.Resource)
require.NotNil(t, dec.Data)
return nil
})(res)
require.NoError(t, err)
})
t.Run("inner-validation-error", func(t *testing.T) {
fakeErr := fmt.Errorf("fake")
err := resource.DecodeAndValidate[*pbdemo.Artist](func(dec *resource.DecodedResource[*pbdemo.Artist]) error {
return fakeErr
})(res)
require.Error(t, err)
require.Equal(t, fakeErr, err)
})
t.Run("decode-error", func(t *testing.T) {
err := resource.DecodeAndValidate[*pbdemo.Album](func(dec *resource.DecodedResource[*pbdemo.Album]) error {
require.Fail(t, "callback should not be called when decoding fails")
return nil
})(res)
require.Error(t, err)
require.ErrorAs(t, err, &resource.ErrDataParse{})
})
}
func TestDecodeAndMutate(t *testing.T) {
res := rtest.Resource(demo.TypeV2Artist, "babypants").
WithData(t, &pbdemo.Artist{Name: "caspar babypants"}).
Build()
t.Run("no-writeback", func(t *testing.T) {
original := res.Data.Value
err := resource.DecodeAndMutate[*pbdemo.Artist](func(dec *resource.DecodedResource[*pbdemo.Artist]) (bool, error) {
require.NotNil(t, dec.Resource)
require.NotNil(t, dec.Data)
// we are going to change the data but not tell the outer hook about it
dec.Data.Name = "changed"
return false, nil
})(res)
require.NoError(t, err)
// Ensure that the outer hook didn't overwrite the resources data because we told it not to
require.Equal(t, original, res.Data.Value)
})
t.Run("writeback", func(t *testing.T) {
original := res.Data.Value
err := resource.DecodeAndMutate[*pbdemo.Artist](func(dec *resource.DecodedResource[*pbdemo.Artist]) (bool, error) {
require.NotNil(t, dec.Resource)
require.NotNil(t, dec.Data)
dec.Data.Name = "changed"
return true, nil
})(res)
require.NoError(t, err)
// Ensure that the outer hook reencoded the Any data because we told it to.
require.NotEqual(t, original, res.Data.Value)
})
t.Run("inner-mutation-error", func(t *testing.T) {
fakeErr := fmt.Errorf("fake")
err := resource.DecodeAndMutate[*pbdemo.Artist](func(dec *resource.DecodedResource[*pbdemo.Artist]) (bool, error) {
return false, fakeErr
})(res)
require.Error(t, err)
require.Equal(t, fakeErr, err)
})
t.Run("decode-error", func(t *testing.T) {
err := resource.DecodeAndMutate[*pbdemo.Album](func(dec *resource.DecodedResource[*pbdemo.Album]) (bool, error) {
require.Fail(t, "callback should not be called when decoding fails")
return false, nil
})(res)
require.Error(t, err)
require.ErrorAs(t, err, &resource.ErrDataParse{})
})
}
func TestDecodeAndAuthorizeWrite(t *testing.T) {
res := rtest.Resource(demo.TypeV2Artist, "babypants").
WithData(t, &pbdemo.Artist{Name: "caspar babypants"}).
Build()
t.Run("allowed", func(t *testing.T) {
err := resource.DecodeAndAuthorizeWrite[*pbdemo.Artist](func(a acl.Authorizer, c *acl.AuthorizerContext, dec *resource.DecodedResource[*pbdemo.Artist]) error {
require.NotNil(t, a)
require.NotNil(t, c)
require.NotNil(t, dec.Resource)
require.NotNil(t, dec.Data)
// access allowed
return nil
})(acl.DenyAll(), &acl.AuthorizerContext{}, res)
require.NoError(t, err)
})
t.Run("denied", func(t *testing.T) {
err := resource.DecodeAndAuthorizeWrite[*pbdemo.Artist](func(a acl.Authorizer, c *acl.AuthorizerContext, dec *resource.DecodedResource[*pbdemo.Artist]) error {
return acl.PermissionDenied("fake")
})(acl.DenyAll(), nil, res)
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
})
t.Run("decode-error", func(t *testing.T) {
err := resource.DecodeAndAuthorizeWrite[*pbdemo.Album](func(a acl.Authorizer, c *acl.AuthorizerContext, dec *resource.DecodedResource[*pbdemo.Album]) error {
require.Fail(t, "callback should not be called when decoding fails")
return nil
})(acl.DenyAll(), &acl.AuthorizerContext{}, res)
require.Error(t, err)
require.ErrorAs(t, err, &resource.ErrDataParse{})
})
}
func TestDecodeAndAuthorizeRead(t *testing.T) {
res := rtest.Resource(demo.TypeV2Artist, "babypants").
WithData(t, &pbdemo.Artist{Name: "caspar babypants"}).
Build()
t.Run("allowed", func(t *testing.T) {
err := resource.DecodeAndAuthorizeRead[*pbdemo.Artist](func(a acl.Authorizer, c *acl.AuthorizerContext, dec *resource.DecodedResource[*pbdemo.Artist]) error {
require.NotNil(t, a)
require.NotNil(t, c)
require.NotNil(t, dec.Resource)
require.NotNil(t, dec.Data)
// access allowed
return nil
})(acl.DenyAll(), &acl.AuthorizerContext{}, nil, res)
require.NoError(t, err)
})
t.Run("denied", func(t *testing.T) {
err := resource.DecodeAndAuthorizeRead[*pbdemo.Artist](func(a acl.Authorizer, c *acl.AuthorizerContext, dec *resource.DecodedResource[*pbdemo.Artist]) error {
return acl.PermissionDenied("fake")
})(acl.DenyAll(), nil, nil, res)
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
})
t.Run("decode-error", func(t *testing.T) {
err := resource.DecodeAndAuthorizeRead[*pbdemo.Album](func(a acl.Authorizer, c *acl.AuthorizerContext, dec *resource.DecodedResource[*pbdemo.Album]) error {
require.Fail(t, "callback should not be called when decoding fails")
return nil
})(acl.DenyAll(), &acl.AuthorizerContext{}, nil, res)
require.Error(t, err)
require.ErrorAs(t, err, &resource.ErrDataParse{})
})
t.Run("err-need-resource", func(t *testing.T) {
err := resource.DecodeAndAuthorizeRead[*pbdemo.Artist](func(a acl.Authorizer, c *acl.AuthorizerContext, dec *resource.DecodedResource[*pbdemo.Artist]) error {
require.Fail(t, "callback should not be called when no resource was provided to be decoded")
return nil
})(acl.DenyAll(), &acl.AuthorizerContext{}, nil, nil)
require.Error(t, err)
require.ErrorIs(t, err, resource.ErrNeedResource)
})
}
func TestAuthorizeReadWithResource(t *testing.T) {
res := rtest.Resource(demo.TypeV2Artist, "babypants").
WithData(t, &pbdemo.Artist{Name: "caspar babypants"}).
Build()
t.Run("allowed", func(t *testing.T) {
err := resource.AuthorizeReadWithResource(func(a acl.Authorizer, c *acl.AuthorizerContext, res *pbresource.Resource) error {
require.NotNil(t, a)
require.NotNil(t, c)
require.NotNil(t, res)
// access allowed
return nil
})(acl.DenyAll(), &acl.AuthorizerContext{}, nil, res)
require.NoError(t, err)
})
t.Run("denied", func(t *testing.T) {
err := resource.AuthorizeReadWithResource(func(a acl.Authorizer, c *acl.AuthorizerContext, res *pbresource.Resource) error {
return acl.PermissionDenied("fake")
})(acl.DenyAll(), nil, nil, res)
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err))
})
t.Run("err-need-resource", func(t *testing.T) {
err := resource.AuthorizeReadWithResource(func(a acl.Authorizer, c *acl.AuthorizerContext, res *pbresource.Resource) error {
require.Fail(t, "callback should not be called when no resource was provided to be decoded")
return nil
})(acl.DenyAll(), &acl.AuthorizerContext{}, nil, nil)
require.Error(t, err)
require.ErrorIs(t, err, resource.ErrNeedResource)
})
}