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) } }) } }) } }