package ape import ( "context" "crypto/ecdsa" "encoding/hex" "errors" "fmt" "testing" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" frostfsidcore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/frostfsid" apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum" containerSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" "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" commonschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/common" 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, _ bool) (*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" groupID = "1" 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 } var apeCheckTestCases = []struct { name string container string object *string methods []string header testHeader xHeaders []session.XHeader containerRules []chain.Rule groupidRules []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, Kind: chain.KindResource, 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, Kind: chain.KindRequest, Key: nativeschema.PropertyKeyActorPublicKey, Value: senderKey, }, }, }, }, expectAPEErr: true, }, { name: "oid required requests are denied by xheader", container: containerID, object: stringPtr(objectID), methods: methodsRequiredOID, header: testHeader{ headerObjSDK: &headerObjectSDKParams{ attributes: []struct { key string val string }{ { key: "attr1", val: "attribute_value", }, }, }, fromHeaderProvider: true, }, xHeaders: []session.XHeader{ func() (xhead session.XHeader) { xhead.SetKey("X-Test-ID") xhead.SetValue("aezakmi") return }(), }, 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, Kind: chain.KindRequest, Key: fmt.Sprintf(commonschema.PropertyKeyFrostFSXHeader, "X-Test-ID"), Value: "aezakmi", }, }, }, }, 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, Kind: chain.KindResource, Key: nativeschema.PropertyKeyObjectPayloadLength, Value: "1000", }, }, }, }, expectAPEErr: true, }, { name: "optional oid requests reached quota limit by group-id", container: containerID, methods: methodsOptionalOID, header: testHeader{ headerObjSDK: &headerObjectSDKParams{ payloadSize: 1000, }, fromRequestResponseHeader: true, }, groupidRules: []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, Kind: chain.KindRequest, Key: commonschema.PropertyKeyFrostFSIDGroupID, Value: groupID, }, }, }, }, expectAPEErr: true, }, } type stMock struct{} func (m *stMock) CurrentEpoch() uint64 { return 8 } func TestAPECheck_BearerTokenOverrides(t *testing.T) { for _, test := range apeCheckTestCases { t.Run(test.name, func(t *testing.T) { chain := chain.Chain{ Rules: test.containerRules, MatchType: chain.MatchTypeFirstMatch, } chainSDK := apeSDK.Chain{ Raw: chain.Bytes(), } bt := new(bearer.Token) bt.SetIat(1) bt.SetExp(10) bt.SetAPEOverride(bearer.APEOverride{ Target: apeSDK.ChainTarget{ TargetType: apeSDK.TargetTypeContainer, Name: test.container, }, Chains: []apeSDK.Chain{chainSDK}, }) bt.Sign(senderPrivateKey.PrivateKey) var cnrOwner user.ID user.IDFromKey(&cnrOwner, (ecdsa.PublicKey)(*senderPrivateKey.PublicKey())) 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() checker := NewChecker(ls, ms, headerProvider, frostfsidProvider, nil, &stMock{}, nil, nil) prm := Prm{ Method: method, Container: cnr, Object: obj, Role: role, ContainerOwner: cnrOwner, SenderKey: senderKey, BearerToken: bt, } 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) } }) } }) } } func TestAPECheck(t *testing.T) { for _, test := range apeCheckTestCases { 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() if len(test.containerRules) > 0 { ls.AddOverride(chain.Ingress, policyengine.ContainerTarget(test.container), &chain.Chain{ Rules: test.containerRules, MatchType: chain.MatchTypeFirstMatch, }) } if len(test.groupidRules) > 0 { ls.AddOverride(chain.Ingress, policyengine.GroupTarget(":"+groupID), &chain.Chain{ Rules: test.groupidRules, MatchType: chain.MatchTypeFirstMatch, }) } checker := NewChecker(ls, ms, headerProvider, frostfsidProvider, nil, &stMock{}, nil, nil) 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) } }) } }) } } type netmapStub struct { netmaps map[uint64]*netmapSDK.NetMap currentEpoch uint64 } func (s *netmapStub) GetNetMap(diff uint64) (*netmapSDK.NetMap, error) { if diff >= s.currentEpoch { return nil, errors.New("invalid diff") } return s.GetNetMapByEpoch(s.currentEpoch - diff) } func (s *netmapStub) GetNetMapByEpoch(epoch uint64) (*netmapSDK.NetMap, error) { if nm, found := s.netmaps[epoch]; found { return nm, nil } return nil, errors.New("netmap not found") } func (s *netmapStub) Epoch() (uint64, error) { return s.currentEpoch, nil } type testContainerSource struct { containers map[cid.ID]*container.Container } func (s *testContainerSource) Get(cnrID cid.ID) (*container.Container, error) { if cnr, found := s.containers[cnrID]; found { return cnr, nil } return nil, fmt.Errorf("container not found") } func (s *testContainerSource) DeletionInfo(cid.ID) (*container.DelInfo, error) { return nil, nil } func TestGetECChunk(t *testing.T) { headerProvider := newHeaderProviderMock() frostfsidProvider := newFrostfsIDProviderMock(t) cnr := newContainerIDSDK(t, containerID) obj := newObjectIDSDK(t, &objectID) ls := inmemory.NewInmemoryLocalStorage() ms := inmemory.NewInmemoryMorphRuleChainStorage() ls.AddOverride(chain.Ingress, policyengine.ContainerTarget(containerID), &chain.Chain{ Rules: []chain.Rule{ { Status: chain.AccessDenied, Actions: chain.Actions{Names: methodsRequiredOID}, Resources: chain.Resources{ Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, containerID)}, }, Condition: []chain.Condition{ { Op: chain.CondStringEquals, Kind: chain.KindResource, Key: "attr1", Value: "value", }, }, }, { Status: chain.Allow, Actions: chain.Actions{Names: methodsRequiredOID}, Resources: chain.Resources{ Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, containerID)}, }, }, }, }) node1Key, err := keys.NewPrivateKey() require.NoError(t, err) node1 := netmapSDK.NodeInfo{} node1.SetPublicKey(node1Key.PublicKey().Bytes()) node2Key, err := keys.NewPrivateKey() require.NoError(t, err) node2 := netmapSDK.NodeInfo{} node2.SetPublicKey(node1Key.PublicKey().Bytes()) netmap := &netmapSDK.NetMap{} netmap.SetEpoch(100) netmap.SetNodes([]netmapSDK.NodeInfo{node1, node2}) nm := &netmapStub{ currentEpoch: 100, netmaps: map[uint64]*netmapSDK.NetMap{ 99: netmap, 100: netmap, }, } cont := containerSDK.Container{} cont.Init() pp := netmapSDK.PlacementPolicy{} require.NoError(t, pp.DecodeString("EC 1.1")) cont.SetPlacementPolicy(pp) cs := &testContainerSource{ containers: map[cid.ID]*container.Container{ cnr: { Value: cont, }, }, } checker := NewChecker(ls, ms, headerProvider, frostfsidProvider, nm, &stMock{}, cs, node1Key.PublicKey().Bytes()) ecParentID := oidtest.ID() chunkHeader := newHeaderObjectSDK(cnr, obj, nil).ToV2().GetHeader() ecHeader := object.ECHeader{ Index: 1, Total: 2, Parent: &refs.ObjectID{}, } chunkHeader.SetEC(&ecHeader) ecParentID.WriteToV2(ecHeader.Parent) parentHeader := newHeaderObjectSDK(cnr, &ecParentID, &headerObjectSDKParams{ attributes: []struct { key string val string }{ { key: "attr1", val: "value", }, }, }) headerProvider.addHeader(cnr, ecParentID, parentHeader) // container node requests EC parent headers, so container node denies access by matching attribute key/value t.Run("access denied on container node", func(t *testing.T) { prm := Prm{ Method: nativeschema.MethodGetObject, Container: cnr, Object: obj, Role: role, SenderKey: hex.EncodeToString(node2Key.PublicKey().Bytes()), Header: chunkHeader, } err = checker.CheckAPE(context.Background(), prm) require.Error(t, err) }) // non container node has no access rights to collect EC parent header, so it uses EC chunk headers t.Run("access allowed on non container node", func(t *testing.T) { otherKey, err := keys.NewPrivateKey() require.NoError(t, err) checker = NewChecker(ls, ms, headerProvider, frostfsidProvider, nm, &stMock{}, cs, otherKey.PublicKey().Bytes()) prm := Prm{ Method: nativeschema.MethodGetObject, Container: cnr, Object: obj, Role: nativeschema.PropertyValueContainerRoleOthers, SenderKey: senderKey, Header: chunkHeader, } err = checker.CheckAPE(context.Background(), prm) require.NoError(t, err) }) }