frostfs-node/pkg/services/object/ape/checker_test.go

439 lines
12 KiB
Go
Raw Normal View History

package ape
import (
"context"
"encoding/hex"
"fmt"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
frostfsidcore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/frostfsid"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require"
)
type headerProviderMock struct {
m map[oid.Address]*objectSDK.Object
}
var _ HeaderProvider = (*headerProviderMock)(nil)
func (h *headerProviderMock) addHeader(c cid.ID, o oid.ID, header *objectSDK.Object) {
var addr oid.Address
addr.SetContainer(c)
addr.SetObject(o)
h.m[addr] = header
}
func (h *headerProviderMock) GetHeader(_ context.Context, c cid.ID, o oid.ID) (*objectSDK.Object, error) {
var addr oid.Address
addr.SetContainer(c)
addr.SetObject(o)
obj, ok := h.m[addr]
if !ok {
return nil, fmt.Errorf("address not found")
}
return obj, nil
}
func newHeaderProviderMock() *headerProviderMock {
return &headerProviderMock{
m: make(map[oid.Address]*objectSDK.Object),
}
}
func newContainerIDSDK(t *testing.T, encodedCID string) cid.ID {
var cnr cid.ID
require.NoError(t, cnr.DecodeString(encodedCID))
return cnr
}
func newObjectIDSDK(t *testing.T, encodedOID *string) *oid.ID {
if encodedOID == nil {
return nil
}
obj := new(oid.ID)
require.NoError(t, obj.DecodeString(*encodedOID))
return obj
}
type headerObjectSDKParams struct {
majorVersion, minorVersion uint32
owner user.ID
epoch uint64
payloadSize uint64
typ objectSDK.Type
payloadChecksum checksum.Checksum
payloadHomomorphicHash checksum.Checksum
attributes []struct {
key string
val string
}
}
func stringPtr(s string) *string {
return &s
}
func newHeaderObjectSDK(cnr cid.ID, oid *oid.ID, headerObjSDK *headerObjectSDKParams) *objectSDK.Object {
objSDK := objectSDK.New()
objSDK.SetContainerID(cnr)
if oid != nil {
objSDK.SetID(*oid)
}
if headerObjSDK == nil {
return objSDK
}
ver := new(version.Version)
ver.SetMajor(headerObjSDK.majorVersion)
ver.SetMinor(headerObjSDK.minorVersion)
objSDK.SetVersion(ver)
objSDK.SetCreationEpoch(headerObjSDK.epoch)
objSDK.SetOwnerID(headerObjSDK.owner)
objSDK.SetPayloadSize(headerObjSDK.payloadSize)
objSDK.SetType(headerObjSDK.typ)
objSDK.SetPayloadChecksum(headerObjSDK.payloadChecksum)
objSDK.SetPayloadHomomorphicHash(headerObjSDK.payloadHomomorphicHash)
var attrs []objectSDK.Attribute
for _, attr := range headerObjSDK.attributes {
attrSDK := objectSDK.NewAttribute()
attrSDK.SetKey(attr.key)
attrSDK.SetValue(attr.val)
attrs = append(attrs, *attrSDK)
}
objSDK.SetAttributes(attrs...)
return objSDK
}
type testHeader struct {
headerObjSDK *headerObjectSDKParams
// If fromHeaderProvider is set, then running test should
// consider that a header is recieved from a header provider.
fromHeaderProvider bool
// If fromHeaderProvider is set, then running test should
// consider that a header is recieved from a message header.
fromRequestResponseHeader bool
}
var (
methodsRequiredOID = []string{
nativeschema.MethodGetObject,
nativeschema.MethodHeadObject,
nativeschema.MethodRangeObject,
nativeschema.MethodHashObject,
nativeschema.MethodDeleteObject,
}
methodsOptionalOID = []string{
nativeschema.MethodSearchObject, nativeschema.MethodPutObject,
}
namespace = "test_namespace"
containerID = "73tQMTYyUkTgmvPR1HWib6pndbhSoBovbnMF7Pws8Rcy"
objectID = "BzQw5HH3feoxFDD5tCT87Y1726qzgLfxEE7wgtoRzB3R"
role = "Container"
senderPrivateKey, _ = keys.NewPrivateKey()
senderKey = hex.EncodeToString(senderPrivateKey.PublicKey().Bytes())
)
type frostfsIDProviderMock struct {
subjects map[util.Uint160]*client.Subject
subjectsExtended map[util.Uint160]*client.SubjectExtended
}
var _ frostfsidcore.SubjectProvider = (*frostfsIDProviderMock)(nil)
func newFrostfsIDProviderMock(t *testing.T) *frostfsIDProviderMock {
return &frostfsIDProviderMock{
subjects: map[util.Uint160]*client.Subject{
scriptHashFromSenderKey(t, senderKey): {
Namespace: "testnamespace",
Name: "test",
KV: map[string]string{
"tag-attr1": "value1",
"tag-attr2": "value2",
},
},
},
subjectsExtended: map[util.Uint160]*client.SubjectExtended{
scriptHashFromSenderKey(t, senderKey): {
Namespace: "testnamespace",
Name: "test",
KV: map[string]string{
"tag-attr1": "value1",
"tag-attr2": "value2",
},
Groups: []*client.Group{
{
ID: 1,
Name: "test",
Namespace: "testnamespace",
KV: map[string]string{
"attr1": "value1",
"attr2": "value2",
},
},
},
},
},
}
}
func scriptHashFromSenderKey(t *testing.T, senderKey string) util.Uint160 {
pk, err := keys.NewPublicKeyFromString(senderKey)
require.NoError(t, err)
return pk.GetScriptHash()
}
func (f *frostfsIDProviderMock) GetSubject(key util.Uint160) (*client.Subject, error) {
v, ok := f.subjects[key]
if !ok {
return nil, fmt.Errorf("%s", frostfsidcore.SubjectNotFoundErrorMessage)
}
return v, nil
}
func (f *frostfsIDProviderMock) GetSubjectExtended(key util.Uint160) (*client.SubjectExtended, error) {
v, ok := f.subjectsExtended[key]
if !ok {
return nil, fmt.Errorf("%s", frostfsidcore.SubjectNotFoundErrorMessage)
}
return v, nil
}
func TestAPECheck(t *testing.T) {
for _, test := range []struct {
name string
container string
object *string
methods []string
header testHeader
containerRules []chain.Rule
expectAPEErr bool
}{
{
name: "oid required requests are allowed",
container: containerID,
object: stringPtr(objectID),
methods: methodsRequiredOID,
containerRules: []chain.Rule{
{
Status: chain.Allow,
Actions: chain.Actions{Names: methodsRequiredOID},
Resources: chain.Resources{
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, containerID, objectID)},
},
},
},
},
{
name: "oid optional requests are allowed",
container: containerID,
methods: methodsOptionalOID,
containerRules: []chain.Rule{
{
Status: chain.Allow,
Actions: chain.Actions{Names: methodsOptionalOID},
Resources: chain.Resources{
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, containerID)},
},
},
},
},
{
name: "oid required requests are denied",
container: containerID,
object: stringPtr(objectID),
methods: methodsRequiredOID,
containerRules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{Names: methodsRequiredOID},
Resources: chain.Resources{
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, containerID, objectID)},
},
},
},
expectAPEErr: true,
},
{
name: "oid required requests are denied by an attribute",
container: containerID,
object: stringPtr(objectID),
methods: methodsRequiredOID,
header: testHeader{
headerObjSDK: &headerObjectSDKParams{
attributes: []struct {
key string
val string
}{
{
key: "attr1",
val: "attribute_value",
},
},
},
fromHeaderProvider: true,
},
containerRules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{Names: methodsRequiredOID},
Resources: chain.Resources{
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, containerID, objectID)},
},
Any: true,
Condition: []chain.Condition{
{
Op: chain.CondStringLike,
Object: chain.ObjectResource,
Key: "attr1",
Value: "attribute*",
},
},
},
},
expectAPEErr: true,
},
{
name: "oid required requests are denied by sender",
container: containerID,
object: stringPtr(objectID),
methods: methodsRequiredOID,
header: testHeader{
headerObjSDK: &headerObjectSDKParams{
attributes: []struct {
key string
val string
}{
{
key: "attr1",
val: "attribute_value",
},
},
},
fromHeaderProvider: true,
},
containerRules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{Names: methodsRequiredOID},
Resources: chain.Resources{
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, containerID, objectID)},
},
Any: true,
Condition: []chain.Condition{
{
Op: chain.CondStringLike,
Object: chain.ObjectRequest,
Key: nativeschema.PropertyKeyActorPublicKey,
Value: senderKey,
},
},
},
},
expectAPEErr: true,
},
{
name: "optional oid requests reached quota limit by an attribute",
container: containerID,
methods: methodsOptionalOID,
header: testHeader{
headerObjSDK: &headerObjectSDKParams{
payloadSize: 1000,
},
fromRequestResponseHeader: true,
},
containerRules: []chain.Rule{
{
Status: chain.QuotaLimitReached,
Actions: chain.Actions{Names: methodsOptionalOID},
Resources: chain.Resources{
Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, containerID)},
},
Any: true,
Condition: []chain.Condition{
{
Op: chain.CondStringEquals,
Object: chain.ObjectResource,
Key: nativeschema.PropertyKeyObjectPayloadLength,
Value: "1000",
},
},
},
},
expectAPEErr: true,
},
} {
t.Run(test.name, func(t *testing.T) {
for _, method := range test.methods {
t.Run(method, func(t *testing.T) {
headerProvider := newHeaderProviderMock()
frostfsidProvider := newFrostfsIDProviderMock(t)
cnr := newContainerIDSDK(t, test.container)
obj := newObjectIDSDK(t, test.object)
ls := inmemory.NewInmemoryLocalStorage()
ms := inmemory.NewInmemoryMorphRuleChainStorage()
ls.AddOverride(chain.Ingress, policyengine.ContainerTarget(test.container), &chain.Chain{
Rules: test.containerRules,
MatchType: chain.MatchTypeFirstMatch,
})
router := policyengine.NewDefaultChainRouterWithLocalOverrides(ms, ls)
checker := NewChecker(router, headerProvider, frostfsidProvider)
prm := Prm{
Method: method,
Container: cnr,
Object: obj,
Role: role,
SenderKey: senderKey,
}
var headerObjSDK *objectSDK.Object
if test.header.headerObjSDK != nil {
headerObjSDK = newHeaderObjectSDK(cnr, obj, test.header.headerObjSDK)
if test.header.fromHeaderProvider {
require.NotNil(t, obj, "oid is required if a header is expected to be found in header provider")
headerProvider.addHeader(cnr, *obj, headerObjSDK)
} else if test.header.fromRequestResponseHeader {
prm.Header = headerObjSDK.ToV2().GetHeader()
}
}
err := checker.CheckAPE(context.Background(), prm)
if test.expectAPEErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
})
}
}