package tests import ( "bytes" "path" "testing" "git.frostfs.info/TrueCloudLab/frostfs-contract/nns" "git.frostfs.info/TrueCloudLab/frostfs-contract/policy" "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) const policyPath = "../policy" type policyContracts struct { policy *neotest.ContractInvoker frostfsid *neotest.ContractInvoker } func newPolicyInvokers(t *testing.T) *policyContracts { e := newExecutor(t) n := deployNNSContract(t, e) ffid := deployFrostfsid(t, e) polic := deployPolicyContract(t, e) n.Invoke(t, true, "register", nns.FrostfsIDNNSName, n.CommitteeHash, "myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL) n.Invoke(t, stackitem.Null{}, "addRecord", nns.FrostfsIDNNSName, int64(nns.TXT), ffid.Hash.StringLE()) n.Invoke(t, stackitem.Null{}, "addRecord", nns.FrostfsIDNNSName, int64(nns.TXT), address.Uint160ToString(ffid.Hash)) return &policyContracts{ policy: polic, frostfsid: ffid, } } func deployNNSContract(t *testing.T, e *neotest.Executor) *neotest.ContractInvoker { ctrNNS := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml")) e.DeployContract(t, ctrNNS, nil) n := e.CommitteeInvoker(ctrNNS.Hash) return n } func deployFrostfsid(t *testing.T, e *neotest.Executor) *neotest.ContractInvoker { acc, err := wallet.NewAccount() require.NoError(t, err) args := make([]any, 5) args[0] = acc.ScriptHash() frostfsID := neotest.CompileFile(t, e.CommitteeHash, frostfsidPath, path.Join(frostfsidPath, "config.yml")) e.DeployContract(t, frostfsID, args) return e.CommitteeInvoker(frostfsID.Hash) } func deployPolicyContract(t *testing.T, e *neotest.Executor) *neotest.ContractInvoker { cfgPath := path.Join(policyPath, "config.yml") c := neotest.CompileFile(t, e.CommitteeHash, policyPath, cfgPath) e.DeployContract(t, c, []any{nil}) return e.CommitteeInvoker(c.Hash) } func TestPolicy(t *testing.T) { c := newPolicyInvokers(t) checkChainsIteratorByPrefix(t, c.policy, policy.Namespace, "mynamespace", "ingress", [][]byte{}) checkChainsIteratorByPrefix(t, c.policy, policy.Container, "cnr1", "ingress", [][]byte{}) // Policies are opaque to the contract and are just raw bytes to store. p1 := []byte("chain1") p2 := []byte("chain2") p3 := []byte("chain3") p33 := []byte("chain33") c.frostfsid.Invoke(t, stackitem.Null{}, "createNamespace", "mynamespace") c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1) checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) checkChains(t, c.policy, "mynamespace", "", "ingress", [][]byte{p1}) checkChains(t, c.policy, "mynamespace", "", "all", nil) c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule2", p2) checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")}) checkChains(t, c.policy, "mynamespace", "", "ingress", [][]byte{p1}) // Only namespace chains. checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2}) checkChains(t, c.policy, "mynamespace", "cnr1", "all", nil) // No chains attached to 'all'. checkChains(t, c.policy, "mynamespace", "cnr2", "ingress", [][]byte{p1}) // Only namespace, no chains for the container. c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p3) checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")}) checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p3}) c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33) checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")}) checkChain(t, c.policy, policy.Container, "cnr1", "ingress:myrule3", p33) checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p33}) // Override chain. checkChainsByPrefix(t, c.policy, policy.Container, "cnr1", "", [][]byte{p2, p33}) checkChainsByPrefix(t, c.policy, policy.IAM, "", "", nil) checkChainKeys(t, c.policy, policy.Container, "cnr1", []string{"ingress:myrule2", "ingress:myrule3"}) checkChainsIteratorByPrefix(t, c.policy, policy.Container, "cnr1", "ingress:myrule3", [][]byte{p33}) checkChainsIteratorByPrefix(t, c.policy, policy.Container, "cnr1", "ingress", [][]byte{p2, p33}) t.Run("removal", func(t *testing.T) { t.Run("wrong name", func(t *testing.T) { c.policy.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress") checkChains(t, c.policy, "mynamespace", "", "ingress", [][]byte{p1}) }) c.policy.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress:123") checkChains(t, c.policy, "mynamespace", "", "ingress", nil) checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p2, p33}) // Container chains still exist. // Remove by prefix. c.policy.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress") checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", nil) // Remove by prefix. c.policy.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress") checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", nil) checkTargets(t, c.policy, policy.Namespace, [][]byte{}) checkTargets(t, c.policy, policy.Container, [][]byte{}) }) t.Run("add again after removal", func(t *testing.T) { c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1) c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33) checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")}) checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")}) }) t.Run("add chain for non-active namespace", func(t *testing.T) { c.frostfsid.Invoke(t, stackitem.Null{}, "createNamespace", "nsdisabled") c.frostfsid.Invoke(t, stackitem.Null{}, "updateNamespace", "nsdisabled", "frozen") c.policy.InvokeFail(t, "namespace is non-active", "addChain", policy.Namespace, "nsdisabled", "ingress:3", p1) c.frostfsid.Invoke(t, stackitem.Null{}, "updateNamespace", "nsdisabled", "purge") c.policy.InvokeFail(t, "namespace is non-active", "addChain", policy.Namespace, "nsdisabled", "ingress:3", p1) c.frostfsid.Invoke(t, stackitem.Null{}, "updateNamespace", "nsdisabled", "active") c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "nsdisabled", "ingress:3", p1) }) } func TestAutorization(t *testing.T) { c := newPolicyInvokers(t) c.policy.Invoke(t, stackitem.Null{}, "getAdmin") s := c.policy.NewAccount(t, 1_0000_0000) cs := c.policy.WithSigners(s) args := []any{policy.Container, "cnr1", "ingress:myrule3", []byte("opaque")} cs.InvokeFail(t, policy.ErrNotAuthorized, "addChain", args...) c.policy.Invoke(t, stackitem.Null{}, "setAdmin", s.ScriptHash()) c.policy.Invoke(t, stackitem.NewBuffer(s.ScriptHash().BytesBE()), "getAdmin") cs.Invoke(t, stackitem.Null{}, "addChain", args...) } func checkChains(t *testing.T, e *neotest.ContractInvoker, namespace, container, name string, expected [][]byte) { s, err := e.TestInvoke(t, "listChains", namespace, container, name) require.NoError(t, err) checksChainsOnStack(t, s, expected) } func checkChainsByPrefix(t *testing.T, e *neotest.ContractInvoker, kind byte, entityName, prefix string, expected [][]byte) { s, err := e.TestInvoke(t, "listChainsByPrefix", kind, entityName, prefix) require.NoError(t, err) checksChainsOnStack(t, s, expected) } func checkChainsIteratorByPrefix(t *testing.T, e *neotest.ContractInvoker, kind byte, entityName, prefix string, expected [][]byte) { s, err := e.TestInvoke(t, "iteratorChainsByPrefix", kind, entityName, prefix) require.NoError(t, err) if s.Len() == 0 { t.Fatal("Stack is empty") } iteratorItem := s.Pop().Value().(*storage.Iterator) policys := iteratorToArray(iteratorItem) require.Equal(t, len(expected), len(policys)) for i := range expected { bytesPolicy, err := policys[i].TryBytes() require.NoError(t, err) require.Equal(t, expected[i], bytesPolicy) } } func checkChainKeys(t *testing.T, e *neotest.ContractInvoker, kind byte, entityName string, expected []string) { s, err := e.TestInvoke(t, "listChainNames", kind, entityName) require.NoError(t, err) if s.Len() == 0 { t.Fatal("Stack is empty") } iteratorItem := s.Pop().Value().(*storage.Iterator) policys := iteratorToArray(iteratorItem) require.Equal(t, len(expected), len(policys)) for i := range expected { bytesPolicy, err := policys[i].TryBytes() require.NoError(t, err) require.Equal(t, expected[i], string(bytesPolicy)) } } func checksChainsOnStack(t *testing.T, s *vm.Stack, expected [][]byte) { require.Equal(t, 1, s.Len()) var actual [][]byte arr := s.Pop().Array() for i := range arr { bs, err := arr[i].TryBytes() require.NoError(t, err) actual = append(actual, bs) } require.ElementsMatch(t, expected, actual) } func checkChain(t *testing.T, e *neotest.ContractInvoker, kind byte, entityName, name string, expected []byte) { s, err := e.TestInvoke(t, "getChain", kind, entityName, name) require.NoError(t, err) require.Equal(t, 1, s.Len()) require.True(t, bytes.Equal(expected, s.Pop().Bytes())) } func checkTargets(t *testing.T, e *neotest.ContractInvoker, kind byte, expected [][]byte) { s, err := e.TestInvoke(t, "listTargets", kind) require.NoError(t, err) require.NotEqual(t, 0, s.Len(), "stack is empty") iteratorItem := s.Pop().Value().(*storage.Iterator) targets := iteratorToArray(iteratorItem) require.Equal(t, len(expected), len(targets)) for i := range expected { bytesTargets, err := targets[i].TryBytes() require.NoError(t, err) require.Equal(t, expected[i], bytesTargets) } }