Compare commits

..

11 commits

Author SHA1 Message Date
ae07280ae8
[#146] frostfsid: Use named variable instead of []byte{1}
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2025-05-05 16:39:07 +03:00
883a39011d
[#146] frostfsid: Bump version and add upgrade
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2025-05-05 16:39:07 +03:00
fb9c8e97c2
[#146] frostfsid: Store additional keys out of subject
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2025-05-05 16:38:11 +03:00
819104db74
[#172] common: Update version to v0.21.3
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2025-05-05 16:10:15 +03:00
9f65415fac [#168] Idempotent namespace deletion method
Signed-off-by: A.Mitropolskiy <a.mitropolskiy@yadro.com>
2025-05-05 15:25:59 +03:00
afe4eb3d76 [#168] Idempotent namespace deletion method
Signed-off-by: A.Mitropolskiy <a.mitropolskiy@yadro.com>
2025-05-05 14:02:58 +03:00
a005dc1161 [#155] Restrict creating policies for non-active namespace
Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
2025-04-24 10:38:34 +03:00
6674526a5b [#159] frostfsid: Restrict creating entities for non-active namespace
Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
2025-04-14 06:50:13 +00:00
c350b7372f
[#167] nns: Fix addRecord() for domains without SOA record
It works for all but second-level domains.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2025-04-05 17:38:17 +03:00
b38d42baf3
[#165] nns: Ignore domain expirations
Domain expirations undeniably complicate reasoning about contract behaviour:
1. SOA record expire field has a bit of a different semantics
2. For our coredns backend we would like to receive everything we put, sudden domain expirations can make life harder.
3. This expiration depends on block time, which in turn may differ from the real timestamp.

Close #165.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2025-04-05 09:38:38 +03:00
9283641cb4
[#164] nns: Add test for deleteRecords() from subdomain
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2025-04-04 14:54:28 +03:00
18 changed files with 734 additions and 226 deletions

View file

@ -7,6 +7,7 @@ Changelog for FrostFS Contract
### Added ### Added
- Field `state` to a namespace to indicate its' lifecycle stage (#154). - Field `state` to a namespace to indicate its' lifecycle stage (#154).
- Method `UpdateNamespace` to adjust namespace state (#154). - Method `UpdateNamespace` to adjust namespace state (#154).
- Method `DeleteNamespace` to remove existing namespace (#168).
### Changed ### Changed
### Removed ### Removed

View file

@ -1 +1 @@
v0.21.2 v0.21.4

10
common/address.go Normal file
View file

@ -0,0 +1,10 @@
package common
import "github.com/nspcc-dev/neo-go/pkg/interop"
const (
NEO3PrefixLen = 1
ChecksumLen = 4
AddressLen = NEO3PrefixLen + interop.Hash160Len + ChecksumLen
)

View file

@ -5,7 +5,7 @@ import "github.com/nspcc-dev/neo-go/pkg/interop/native/std"
const ( const (
major = 0 major = 0
minor = 21 minor = 21
patch = 2 patch = 4
// Versions from which an update should be performed. // Versions from which an update should be performed.
// These should be used in a group (so prevMinor can be equal to minor if there are // These should be used in a group (so prevMinor can be equal to minor if there are

View file

@ -114,6 +114,7 @@ const (
createNamespaceMethod = "createNamespace" createNamespaceMethod = "createNamespace"
updateNamespaceMethod = "updateNamespace" updateNamespaceMethod = "updateNamespace"
deleteNamespaceMethod = "deleteNamespace"
getNamespaceMethod = "getNamespace" getNamespaceMethod = "getNamespace"
getNamespaceExtendedMethod = "getNamespaceExtended" getNamespaceExtendedMethod = "getNamespaceExtended"
listNamespacesMethod = "listNamespaces" listNamespacesMethod = "listNamespaces"
@ -463,6 +464,18 @@ func (c Client) UpdateNamespaceCall(namespace string, state string) (method stri
return updateNamespaceMethod, []any{namespace, state} return updateNamespaceMethod, []any{namespace, state}
} }
// DeleteNamespace idempotently removes the namespace.
// Must be invoked by contract owner.
func (c Client) DeleteNamespace(namespace string) (tx util.Uint256, vub uint32, err error) {
method, args := c.DeleteNamespaceCall(namespace)
return c.act.SendCall(c.contract, method, args...)
}
// DeleteNamespaceCall provides args for DeleteNamespace to use in commonclient.Transaction.
func (c Client) DeleteNamespaceCall(namespace string) (method string, args []any) {
return deleteNamespaceMethod, []any{namespace}
}
// ListNamespaces gets all namespaces. // ListNamespaces gets all namespaces.
func (c Client) ListNamespaces() ([]*Namespace, error) { func (c Client) ListNamespaces() ([]*Namespace, error) {
items, err := commonclient.ReadIteratorItems(c.act, iteratorBatchSize, c.contract, listNamespacesMethod) items, err := commonclient.ReadIteratorItems(c.act, iteratorBatchSize, c.contract, listNamespacesMethod)

View file

@ -185,7 +185,6 @@ func ParseNamespaceExtended(structArr []stackitem.Item) (*NamespaceExtended, err
}, nil }, nil
} }
// TODO: [cleanup] (#151) rewrite this method after new release.
func parseNamespace(structArr []stackitem.Item, stateIndex int) (*Namespace, error) { func parseNamespace(structArr []stackitem.Item, stateIndex int) (*Namespace, error) {
name, err := structArr[0].TryBytes() name, err := structArr[0].TryBytes()
if err != nil { if err != nil {
@ -193,7 +192,7 @@ func parseNamespace(structArr []stackitem.Item, stateIndex int) (*Namespace, err
} }
nsState := Active nsState := Active
if len(structArr) == stateIndex+1 { if len(structArr) >= stateIndex+1 {
nsStateBytes, err := structArr[stateIndex].TryBytes() nsStateBytes, err := structArr[stateIndex].TryBytes()
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -72,6 +72,10 @@ events:
type: String type: String
- name: state - name: state
type: String type: String
- name: DeleteNamespace
parameters:
- name: namespace
type: String
- name: AddSubjectToNamespace - name: AddSubjectToNamespace
parameters: parameters:
- name: subjectAddress - name: subjectAddress

View file

@ -21,6 +21,7 @@ FrostFSID contract does not produce notifications to process.
| `c` | Int | group id counter | | `c` | Int | group id counter |
| `m` + [ RIPEMD160 of namespace ] + [ RIPEMD160 of subject name ] | Serialized group id int | group name to group id index | | `m` + [ RIPEMD160 of namespace ] + [ RIPEMD160 of subject name ] | Serialized group id int | group name to group id index |
| `A` + [ subject address ] | bool | means that the wallet has been used | | `A` + [ subject address ] | bool | means that the wallet has been used |
| `d` + [ subject address ] + [ pk address ] | []byte{1} | link subject to extra public keys |
*/ */

View file

@ -21,6 +21,7 @@ type (
// - Name: a string representing the name of the subject. // - Name: a string representing the name of the subject.
// The name must match the following regex pattern: ^[\w+=,.@-]{1,64}$ // The name must match the following regex pattern: ^[\w+=,.@-]{1,64}$
// The Subject is stored in the storage as a hash(namespace) + hash(name). // The Subject is stored in the storage as a hash(namespace) + hash(name).
// AdditionalKeys are stored in records with subject's address prefix.
Subject struct { Subject struct {
PrimaryKey interop.PublicKey PrimaryKey interop.PublicKey
AdditionalKeys []interop.PublicKey AdditionalKeys []interop.PublicKey
@ -99,10 +100,15 @@ const (
groupCounterKey = 'c' groupCounterKey = 'c'
namespaceGroupsNamesPrefix = 'm' namespaceGroupsNamesPrefix = 'm'
addressPrefix = 'A' addressPrefix = 'A'
subjectToAddKeyPrefix = 'd'
nsActiveState = "active" nsActiveState = "active"
nsFrozenState = "frozen"
nsPurgeState = "purge"
) )
var dummyValue = []byte{1}
func _deploy(data any, isUpdate bool) { func _deploy(data any, isUpdate bool) {
ctx := storage.GetContext() ctx := storage.GetContext()
@ -154,7 +160,23 @@ func _deploy(data any, isUpdate bool) {
storage.Put(ctx, groupCounterKey, maxGroupID) storage.Put(ctx, groupCounterKey, maxGroupID)
} }
if args.version < common.GetVersion(0, 21, 3) {
migrateNamespacesState(ctx) migrateNamespacesState(ctx)
}
if args.version < common.GetVersion(0, 21, 4) {
it := storage.Find(ctx, subjectKeysPrefix, storage.ValuesOnly)
for iterator.Next(it) {
subject := std.Deserialize(iterator.Value(it).([]byte)).(Subject)
subjAddr := contract.CreateStandardAccount(subject.PrimaryKey)
for i := 0; i < len(subject.AdditionalKeys); i++ {
storage.Put(ctx, subjectToAdditionalKeyKey(subjAddr, subject.AdditionalKeys[i]), dummyValue)
}
subject.AdditionalKeys = nil
storage.Put(ctx, subjectKeyFromAddr(subjAddr), std.Serialize(subject))
}
}
return return
} }
@ -209,6 +231,7 @@ func Version() int {
// CreateSubject creates a new subject in the specified namespace with the provided public key. // CreateSubject creates a new subject in the specified namespace with the provided public key.
func CreateSubject(ns string, key interop.PublicKey) { func CreateSubject(ns string, key interop.PublicKey) {
ctx := storage.GetContext() ctx := storage.GetContext()
checkNamespaceState(ns)
checkContractOwner(ctx) checkContractOwner(ctx)
if len(key) != interop.PublicKeyCompressedLen { if len(key) != interop.PublicKeyCompressedLen {
@ -222,7 +245,7 @@ func CreateSubject(ns string, key interop.PublicKey) {
panic("subject already exists") panic("subject already exists")
} }
saPrefix := subjectAdditionalPrefix(key) saPrefix := additionalKeyToSubjectPrefix(key)
it := storage.Find(ctx, saPrefix, storage.KeysOnly) it := storage.Find(ctx, saPrefix, storage.KeysOnly)
for iterator.Next(it) { for iterator.Next(it) {
panic("key is occupied") panic("key is occupied")
@ -247,7 +270,7 @@ func CreateSubject(ns string, key interop.PublicKey) {
storage.Put(ctx, sKey, std.Serialize(subj)) storage.Put(ctx, sKey, std.Serialize(subj))
nsSubjKey := namespaceSubjectKey(ns, addr) nsSubjKey := namespaceSubjectKey(ns, addr)
storage.Put(ctx, nsSubjKey, []byte{1}) storage.Put(ctx, nsSubjKey, dummyValue)
storage.Put(ctx, allAddressKey, true) storage.Put(ctx, allAddressKey, true)
runtime.Notify("CreateSubject", interop.Hash160(addr)) runtime.Notify("CreateSubject", interop.Hash160(addr))
@ -270,13 +293,13 @@ func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) {
panic("key is occupied") panic("key is occupied")
} }
saKey := subjectAdditionalKey(key, addr) saKey := additionalKeyToSubjectKey(key, addr)
data := storage.Get(ctx, saKey).([]byte) data := storage.Get(ctx, saKey).([]byte)
if data != nil { if data != nil {
panic("key already added") panic("key already added")
} }
storage.Put(ctx, saKey, []byte{1}) storage.Put(ctx, saKey, dummyValue)
sKey := subjectKeyFromAddr(addr) sKey := subjectKeyFromAddr(addr)
data = storage.Get(ctx, sKey).([]byte) data = storage.Get(ctx, sKey).([]byte)
@ -284,9 +307,9 @@ func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) {
panic("address not found") panic("address not found")
} }
subject := std.Deserialize(data).(Subject) subject := std.Deserialize(data).(Subject)
subject.AdditionalKeys = append(subject.AdditionalKeys, key) checkNamespaceState(subject.Namespace)
storage.Put(ctx, sKey, std.Serialize(subject)) storage.Put(ctx, subjectToAdditionalKeyKey(addr, key), dummyValue)
storage.Put(ctx, addressKey, true) storage.Put(ctx, addressKey, true)
runtime.Notify("AddSubjectKey", addr, key) runtime.Notify("AddSubjectKey", addr, key)
} }
@ -303,7 +326,7 @@ func RemoveSubjectKey(addr interop.Hash160, key interop.PublicKey) {
panic("incorrect public key length") panic("incorrect public key length")
} }
saKey := subjectAdditionalKey(key, addr) saKey := additionalKeyToSubjectKey(key, addr)
data := storage.Get(ctx, saKey).([]byte) data := storage.Get(ctx, saKey).([]byte)
if data == nil { if data == nil {
panic("key already removed") panic("key already removed")
@ -316,17 +339,14 @@ func RemoveSubjectKey(addr interop.Hash160, key interop.PublicKey) {
if data == nil { if data == nil {
panic("address not found") panic("address not found")
} }
subject := std.Deserialize(data).(Subject)
var additionalKeys []interop.PublicKey subjToAddKey := subjectToAdditionalKeyKey(addr, key)
for i := 0; i < len(subject.AdditionalKeys); i++ { data = storage.Get(ctx, subjToAddKey).([]byte)
if !common.BytesEqual(subject.AdditionalKeys[i], key) { if data == nil {
additionalKeys = append(additionalKeys, subject.AdditionalKeys[i]) panic("key already removed")
} }
} storage.Delete(ctx, subjToAddKey)
subject.AdditionalKeys = additionalKeys
storage.Put(ctx, sKey, std.Serialize(subject))
storage.Delete(ctx, addressKey(contract.CreateStandardAccount(key))) storage.Delete(ctx, addressKey(contract.CreateStandardAccount(key)))
runtime.Notify("RemoveSubjectKey", addr, key) runtime.Notify("RemoveSubjectKey", addr, key)
} }
@ -347,6 +367,7 @@ func SetSubjectName(addr interop.Hash160, name string) {
} }
subject := std.Deserialize(data).(Subject) subject := std.Deserialize(data).(Subject)
checkNamespaceState(subject.Namespace)
oldName := subject.Name oldName := subject.Name
subject.Name = name subject.Name = name
storage.Put(ctx, sKey, std.Serialize(subject)) storage.Put(ctx, sKey, std.Serialize(subject))
@ -372,6 +393,7 @@ func SetSubjectKV(addr interop.Hash160, key, val string) {
} }
subject := std.Deserialize(data).(Subject) subject := std.Deserialize(data).(Subject)
checkNamespaceState(subject.Namespace)
if subject.KV == nil { if subject.KV == nil {
subject.KV = map[string]string{} subject.KV = map[string]string{}
} }
@ -419,8 +441,10 @@ func DeleteSubject(addr interop.Hash160) {
} }
subj := std.Deserialize(data).(Subject) subj := std.Deserialize(data).(Subject)
subj.AdditionalKeys = getSubjectAdditionalKeys(ctx, addr)
for i := 0; i < len(subj.AdditionalKeys); i++ { for i := 0; i < len(subj.AdditionalKeys); i++ {
storage.Delete(ctx, subjectAdditionalKey(subj.AdditionalKeys[i], addr)) storage.Delete(ctx, additionalKeyToSubjectKey(subj.AdditionalKeys[i], addr))
storage.Delete(ctx, subjectToAdditionalKeyKey(addr, subj.AdditionalKeys[i]))
storage.Delete(ctx, addressKey(contract.CreateStandardAccount(subj.AdditionalKeys[i]))) storage.Delete(ctx, addressKey(contract.CreateStandardAccount(subj.AdditionalKeys[i])))
} }
storage.Delete(ctx, addressKey(addr)) storage.Delete(ctx, addressKey(addr))
@ -442,15 +466,17 @@ func GetSubject(addr interop.Hash160) Subject {
sKey := subjectKeyFromAddr(addr) sKey := subjectKeyFromAddr(addr)
data := storage.Get(ctx, sKey).([]byte) data := storage.Get(ctx, sKey).([]byte)
if data == nil { if data == nil {
a := getPrimaryAddr(ctx, addr) addr = getPrimaryAddr(ctx, addr)
sKey = subjectKeyFromAddr(a) sKey = subjectKeyFromAddr(addr)
data = storage.Get(ctx, sKey).([]byte) data = storage.Get(ctx, sKey).([]byte)
if data == nil { if data == nil {
panic("subject not found") panic("subject not found")
} }
} }
return std.Deserialize(data).(Subject) subj := std.Deserialize(data).(Subject)
subj.AdditionalKeys = getSubjectAdditionalKeys(ctx, addr)
return subj
} }
// GetSubjectExtended retrieves the extended information of the subject with the specified address. // GetSubjectExtended retrieves the extended information of the subject with the specified address.
@ -496,14 +522,18 @@ func GetSubjectByKey(key interop.PublicKey) Subject {
sKey := subjectKey(key) sKey := subjectKey(key)
data := storage.Get(ctx, sKey).([]byte) data := storage.Get(ctx, sKey).([]byte)
if data != nil { if data != nil {
return std.Deserialize(data).(Subject) subj := std.Deserialize(data).(Subject)
subj.AdditionalKeys = getSubjectAdditionalKeys(ctx, contract.CreateStandardAccount(key))
return subj
} }
addr := getPrimaryAddr(ctx, contract.CreateStandardAccount(key)) addr := getPrimaryAddr(ctx, contract.CreateStandardAccount(key))
sKey = subjectKeyFromAddr(addr) sKey = subjectKeyFromAddr(addr)
data = storage.Get(ctx, sKey).([]byte) data = storage.Get(ctx, sKey).([]byte)
if data != nil { if data != nil {
return std.Deserialize(data).(Subject) subj := std.Deserialize(data).(Subject)
subj.AdditionalKeys = getSubjectAdditionalKeys(ctx, addr)
return subj
} }
panic("subject not found") panic("subject not found")
@ -518,6 +548,20 @@ func getPrimaryAddr(ctx storage.Context, addr interop.Hash160) interop.Hash160 {
panic("subject not found") panic("subject not found")
} }
func getSubjectAdditionalKeys(ctx storage.Context, addr interop.Hash160) []interop.PublicKey {
var result []interop.PublicKey
subjToAddKeyPrefix := subjectToAdditionalKeyPrefix(addr)
it := storage.Find(ctx, subjToAddKeyPrefix, storage.KeysOnly|storage.RemovePrefix)
if iterator.Next(it) {
key := iterator.Value(it).([]byte)
if len(key) < interop.PublicKeyCompressedLen {
panic("invalid subject additional key")
}
result = append(result, interop.PublicKey(key[:interop.PublicKeyCompressedLen]))
}
return result
}
// GetSubjectByName retrieves the subject with the specified name within the given namespace. // GetSubjectByName retrieves the subject with the specified name within the given namespace.
func GetSubjectByName(ns, name string) Subject { func GetSubjectByName(ns, name string) Subject {
key := GetSubjectKeyByName(ns, name) key := GetSubjectKeyByName(ns, name)
@ -595,6 +639,36 @@ func CreateNamespace(ns string) {
runtime.Notify("CreateNamespace", ns) runtime.Notify("CreateNamespace", ns)
} }
// DeleteNamespace idempotently removes a namespace with the specified name.
func DeleteNamespace(ns string) {
ctx := storage.GetContext()
checkContractOwner(ctx)
nsKey := namespaceKey(ns)
data := storage.Get(ctx, nsKey).([]byte)
if data == nil {
return
}
namespace := std.Deserialize(data).(Namespace)
if namespace.State != nsPurgeState {
panic("namespace should be in 'purge' state for deletion")
}
it := storage.Find(ctx, groupPrefix(ns), storage.KeysOnly)
if iterator.Next(it) {
panic("can't delete non-empty namespace: groups still present")
}
it = storage.Find(ctx, namespaceSubjectPrefix(ns), storage.KeysOnly)
if iterator.Next(it) {
panic("can't delete non-empty namespace: users still present")
}
storage.Delete(ctx, nsKey)
runtime.Notify("DeleteNamespace", ns)
}
// UpdateNamespace updates existing namespace. // UpdateNamespace updates existing namespace.
func UpdateNamespace(ns string, state string) { func UpdateNamespace(ns string, state string) {
ctx := storage.GetContext() ctx := storage.GetContext()
@ -668,6 +742,7 @@ func ListNamespaceSubjects(ns string) iterator.Iterator {
// CreateGroup creates a new group within the specified namespace. // CreateGroup creates a new group within the specified namespace.
func CreateGroup(ns, group string) int { func CreateGroup(ns, group string) int {
ctx := storage.GetContext() ctx := storage.GetContext()
checkNamespaceState(ns)
checkContractOwner(ctx) checkContractOwner(ctx)
if group == "" { if group == "" {
@ -781,6 +856,7 @@ func SetGroupName(ns string, groupID int, name string) {
} }
gr := std.Deserialize(data).(Group) gr := std.Deserialize(data).(Group)
checkNamespaceState(gr.Namespace)
oldName := gr.Name oldName := gr.Name
gr.Name = name gr.Name = name
storage.Put(ctx, gKey, std.Serialize(gr)) storage.Put(ctx, gKey, std.Serialize(gr))
@ -802,6 +878,7 @@ func SetGroupKV(ns string, groupID int, key, val string) {
} }
gr := std.Deserialize(data).(Group) gr := std.Deserialize(data).(Group)
checkNamespaceState(gr.Namespace)
if gr.KV == nil { if gr.KV == nil {
gr.KV = map[string]string{} gr.KV = map[string]string{}
} }
@ -849,6 +926,7 @@ func AddSubjectToGroup(addr interop.Hash160, groupID int) {
panic("subject not found") panic("subject not found")
} }
subject := std.Deserialize(data).(Subject) subject := std.Deserialize(data).(Subject)
checkNamespaceState(subject.Namespace)
gKey := groupKey(subject.Namespace, groupID) gKey := groupKey(subject.Namespace, groupID)
data = storage.Get(ctx, gKey).([]byte) data = storage.Get(ctx, gKey).([]byte)
@ -862,7 +940,7 @@ func AddSubjectToGroup(addr interop.Hash160, groupID int) {
} }
gsKey := groupSubjectKey(subject.Namespace, groupID, addr) gsKey := groupSubjectKey(subject.Namespace, groupID, addr)
storage.Put(ctx, gsKey, []byte{1}) storage.Put(ctx, gsKey, dummyValue)
runtime.Notify("AddSubjectToGroup", addr, subject.Namespace, groupID) runtime.Notify("AddSubjectToGroup", addr, subject.Namespace, groupID)
} }
@ -1031,15 +1109,25 @@ func subjectKeyFromAddr(addr interop.Hash160) []byte {
return append([]byte{subjectKeysPrefix}, addr...) return append([]byte{subjectKeysPrefix}, addr...)
} }
func subjectAdditionalKey(additionalKey interop.PublicKey, primeAddr interop.Hash160) []byte { func additionalKeyToSubjectKey(additionalKey interop.PublicKey, primeAddr interop.Hash160) []byte {
return append(subjectAdditionalPrefix(additionalKey), primeAddr...) return append(additionalKeyToSubjectPrefix(additionalKey), primeAddr...)
} }
func subjectAdditionalPrefix(additionalKey interop.PublicKey) []byte { func additionalKeyToSubjectPrefix(additionalKey interop.PublicKey) []byte {
addr := contract.CreateStandardAccount(additionalKey) addr := contract.CreateStandardAccount(additionalKey)
return append([]byte{additionalKeysPrefix}, addr...) return append([]byte{additionalKeysPrefix}, addr...)
} }
// subjectToAdditionalKeyKey returns 'd' + [20]byte subjectAddr + [33]byte additionalKey.
func subjectToAdditionalKeyKey(subjectAddr interop.Hash160, additionalKey interop.PublicKey) []byte {
return append(subjectToAdditionalKeyPrefix(subjectAddr), additionalKey...)
}
// subjectToAdditionalKeyPrefix returns 'd' + [20]byte subjectAddr.
func subjectToAdditionalKeyPrefix(subjectAddr interop.Hash160) []byte {
return append([]byte{subjectToAddKeyPrefix}, subjectAddr...)
}
func namespaceKey(ns string) []byte { func namespaceKey(ns string) []byte {
return namespaceKeyFromHash(ripemd160Hash(ns)) return namespaceKeyFromHash(ripemd160Hash(ns))
} }
@ -1126,7 +1214,6 @@ func addressKey(address []byte) []byte {
return append([]byte{addressPrefix}, address...) return append([]byte{addressPrefix}, address...)
} }
// TODO: [cleanup] (#151) remove this migration after new release.
func migrateNamespacesState(ctx storage.Context) { func migrateNamespacesState(ctx storage.Context) {
it := storage.Find(ctx, []byte{namespaceKeysPrefix}, storage.None) it := storage.Find(ctx, []byte{namespaceKeysPrefix}, storage.None)
@ -1149,3 +1236,10 @@ func migrateNamespacesState(ctx storage.Context) {
storage.Put(ctx, string(kv.Key), namespaceData) storage.Put(ctx, string(kv.Key), namespaceData)
} }
} }
func checkNamespaceState(name string) {
ns := GetNamespace(name)
if ns.State == nsFrozenState || ns.State == nsPurgeState {
panic("namespace is non-active")
}
}

View file

@ -9,17 +9,12 @@ import (
type NameState struct { type NameState struct {
Owner interop.Hash160 Owner interop.Hash160
Name string Name string
// Expiration field used to contain wall-clock time of a domain expiration.
// It is preserved for backwards compatibility, but is unused by the contract and should be ignored.
Expiration int64 Expiration int64
Admin interop.Hash160 Admin interop.Hash160
} }
// ensureNotExpired panics if domain name is expired.
func (n NameState) ensureNotExpired() {
if int64(runtime.GetTime()) >= n.Expiration {
panic("name has expired")
}
}
// checkAdmin panics if script container is not signed by the domain name admin. // checkAdmin panics if script container is not signed by the domain name admin.
func (n NameState) checkAdmin() { func (n NameState) checkAdmin() {
if runtime.CheckWitness(n.Owner) { if runtime.CheckWitness(n.Owner) {

View file

@ -149,7 +149,6 @@ func Properties(tokenID []byte) map[string]any {
ns := getNameState(ctx, tokenID) ns := getNameState(ctx, tokenID)
return map[string]any{ return map[string]any{
"name": ns.Name, "name": ns.Name,
"expiration": ns.Expiration,
} }
} }
@ -308,7 +307,6 @@ func extractCnametgt(ctx storage.Context, name, domain string) string {
// checkParent returns parent domain or empty string if domain not found. // checkParent returns parent domain or empty string if domain not found.
func checkParent(ctx storage.Context, fragments []string) string { func checkParent(ctx storage.Context, fragments []string) string {
now := int64(runtime.GetTime())
last := len(fragments) - 1 last := len(fragments) - 1
name := fragments[last] name := fragments[last]
parent := "" parent := ""
@ -320,10 +318,6 @@ func checkParent(ctx storage.Context, fragments []string) string {
if nsBytes == nil { if nsBytes == nil {
continue continue
} }
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if now >= ns.Expiration {
panic("domain expired: " + name)
}
parent = name parent = name
} }
return parent return parent
@ -390,9 +384,6 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str
nsBytes := storage.Get(ctx, append([]byte{prefixName}, tokenKey...)) nsBytes := storage.Get(ctx, append([]byte{prefixName}, tokenKey...))
if nsBytes != nil { if nsBytes != nil {
ns := std.Deserialize(nsBytes.([]byte)).(NameState) ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if int64(runtime.GetTime()) < ns.Expiration {
return false
}
oldOwner = ns.Owner oldOwner = ns.Owner
updateBalance(ctx, []byte(name), oldOwner, -1) updateBalance(ctx, []byte(name), oldOwner, -1)
} else { } else {
@ -401,8 +392,7 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str
ns := NameState{ ns := NameState{
Owner: owner, Owner: owner,
Name: name, Name: name,
// NNS expiration is in milliseconds Expiration: 0,
Expiration: int64(runtime.GetTime() + expire*1000),
} }
checkAvailableGlobalDomain(ctx, name) checkAvailableGlobalDomain(ctx, name)
@ -415,18 +405,6 @@ func register(ctx storage.Context, name string, owner interop.Hash160, email str
return true return true
} }
// Renew increases domain expiration date.
func Renew(name string) int64 {
checkDomainNameLength(name)
runtime.BurnGas(GetPrice())
ctx := storage.GetContext()
ns := getNameState(ctx, []byte(name))
ns.checkAdmin()
ns.Expiration += millisecondsInYear
putNameState(ctx, ns)
return ns.Expiration
}
// UpdateSOA updates soa record. // UpdateSOA updates soa record.
func UpdateSOA(name, email string, refresh, retry, expire, ttl int) { func UpdateSOA(name, email string, refresh, retry, expire, ttl int) {
checkDomainNameLength(name) checkDomainNameLength(name)
@ -731,9 +709,7 @@ func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState {
if nsBytes == nil { if nsBytes == nil {
panic("token not found") panic("token not found")
} }
ns := std.Deserialize(nsBytes.([]byte)).(NameState) return std.Deserialize(nsBytes.([]byte)).(NameState)
ns.ensureNotExpired()
return ns
} }
// putNameState stores domain name state. // putNameState stores domain name state.
@ -801,7 +777,7 @@ func addRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType,
ns := NameState{ ns := NameState{
Name: globalDomain, Name: globalDomain,
Owner: nsOriginal.Owner, Owner: nsOriginal.Owner,
Expiration: nsOriginal.Expiration, Expiration: 0,
Admin: nsOriginal.Admin, Admin: nsOriginal.Admin,
} }
@ -1119,17 +1095,14 @@ func tokenIDFromName(name string) string {
ctx := storage.GetReadOnlyContext() ctx := storage.GetReadOnlyContext()
sum := 0 sum := 0
l := len(fragments) - 1 l := len(fragments)
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
tokenKey := getTokenKey([]byte(name[sum:])) tokenKey := getTokenKey([]byte(name[sum:]))
nameKey := append([]byte{prefixName}, tokenKey...) nameKey := append([]byte{prefixName}, tokenKey...)
nsBytes := storage.Get(ctx, nameKey) nsBytes := storage.Get(ctx, nameKey)
if nsBytes != nil { if nsBytes != nil {
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
if int64(runtime.GetTime()) < ns.Expiration {
return name[sum:] return name[sum:]
} }
}
sum += len(fragments[i]) + 1 sum += len(fragments[i]) + 1
} }
return name return name

View file

@ -2,9 +2,13 @@ package policy
import ( import (
"git.frostfs.info/TrueCloudLab/frostfs-contract/common" "git.frostfs.info/TrueCloudLab/frostfs-contract/common"
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid"
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
"github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/interop/native/management"
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage" "github.com/nspcc-dev/neo-go/pkg/interop/storage"
) )
@ -36,6 +40,11 @@ const (
ErrNotAuthorized = "none of the signers is authorized to change the contract" ErrNotAuthorized = "none of the signers is authorized to change the contract"
) )
const (
purgeNsState = "purge"
frozenNsState = "frozen"
)
// _deploy function sets up initial list of inner ring public keys. // _deploy function sets up initial list of inner ring public keys.
func _deploy(data any, isUpdate bool) { func _deploy(data any, isUpdate bool) {
if isUpdate { if isUpdate {
@ -50,7 +59,7 @@ func _deploy(data any, isUpdate bool) {
ctx := storage.GetContext() ctx := storage.GetContext()
if args.Admin != nil { if args.Admin != nil {
if len(args.Admin) != 20 { if len(args.Admin) != 20 {
panic("invaliad admin hash length") panic("invalid admin hash length")
} }
storage.Put(ctx, []byte{ownerKeyPrefix}, args.Admin) storage.Put(ctx, []byte{ownerKeyPrefix}, args.Admin)
} }
@ -142,9 +151,51 @@ func mapToNumericCreateIfNotExists(ctx storage.Context, kind Kind, name []byte)
return numericID.(int) return numericID.(int)
} }
func checkChainNamespace(entity Kind, name string) {
if entity != Namespace {
return
}
frostfsidAddr := getContractHash(nns.FrostfsIDNNSName)
if frostfsidAddr == nil || management.GetContract(frostfsidAddr) == nil {
panic("could not get frostfsid contract")
}
ns := contract.Call(frostfsidAddr, "getNamespace", contract.ReadOnly, name).(frostfsid.Namespace)
if ns.State == purgeNsState || ns.State == frozenNsState {
panic("namespace is non-active")
}
}
// getContractHash returns nil when it can't resolve contract name,
// so custom error message can be thrown.
func getContractHash(name string) interop.Hash160 {
nnsContract := management.GetContractByID(1)
records := contract.Call(nnsContract.Hash, "getRecords", contract.ReadOnly, name, nns.TXT).([]string)
for _, record := range records {
contractHash := readContractHashFromNNSRecord(record)
if contractHash != nil {
return contractHash
}
}
return nil
}
func readContractHashFromNNSRecord(nnsResponse string) interop.Hash160 {
// 40 is size of hex encoded contract hash as string
if len(nnsResponse) == 40 {
return nil
}
decoded := std.Base58Decode([]byte(nnsResponse))
if len(decoded) != common.AddressLen || management.GetContract(decoded[1:21]) == nil {
return nil
}
return decoded[1:21]
}
func AddChain(entity Kind, entityName string, name []byte, chain []byte) { func AddChain(entity Kind, entityName string, name []byte, chain []byte) {
ctx := storage.GetContext() ctx := storage.GetContext()
checkAuthorization(ctx) checkAuthorization(ctx)
checkChainNamespace(entity, entityName)
entityNameBytes := mapToNumericCreateIfNotExists(ctx, entity, []byte(entityName)) entityNameBytes := mapToNumericCreateIfNotExists(ctx, entity, []byte(entityName))
key := storageKey(entity, entityNameBytes, name) key := storageKey(entity, entityNameBytes, name)

View file

@ -70,6 +70,11 @@ type UpdateNamespaceEvent struct {
State string State string
} }
// DeleteNamespaceEvent represents "DeleteNamespace" event emitted by the contract.
type DeleteNamespaceEvent struct {
Namespace string
}
// AddSubjectToNamespaceEvent represents "AddSubjectToNamespace" event emitted by the contract. // AddSubjectToNamespaceEvent represents "AddSubjectToNamespace" event emitted by the contract.
type AddSubjectToNamespaceEvent struct { type AddSubjectToNamespaceEvent struct {
SubjectAddress util.Uint160 SubjectAddress util.Uint160
@ -489,6 +494,28 @@ func (c *Contract) DeleteGroupKVUnsigned(ns string, groupID *big.Int, key string
return c.actor.MakeUnsignedCall(c.hash, "deleteGroupKV", nil, ns, groupID, key) return c.actor.MakeUnsignedCall(c.hash, "deleteGroupKV", nil, ns, groupID, key)
} }
// DeleteNamespace creates a transaction invoking `deleteNamespace` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) DeleteNamespace(ns string) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "deleteNamespace", ns)
}
// DeleteNamespaceTransaction creates a transaction invoking `deleteNamespace` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) DeleteNamespaceTransaction(ns string) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "deleteNamespace", ns)
}
// DeleteNamespaceUnsigned creates a transaction invoking `deleteNamespace` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) DeleteNamespaceUnsigned(ns string) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "deleteNamespace", nil, ns)
}
// DeleteSubject creates a transaction invoking `deleteSubject` method of the contract. // DeleteSubject creates a transaction invoking `deleteSubject` method of the contract.
// This transaction is signed and immediately sent to the network. // This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any. // The values returned are its hash, ValidUntilBlock value and error if any.
@ -1394,6 +1421,67 @@ func (e *UpdateNamespaceEvent) FromStackItem(item *stackitem.Array) error {
return nil return nil
} }
// DeleteNamespaceEventsFromApplicationLog retrieves a set of all emitted events
// with "DeleteNamespace" name from the provided [result.ApplicationLog].
func DeleteNamespaceEventsFromApplicationLog(log *result.ApplicationLog) ([]*DeleteNamespaceEvent, error) {
if log == nil {
return nil, errors.New("nil application log")
}
var res []*DeleteNamespaceEvent
for i, ex := range log.Executions {
for j, e := range ex.Events {
if e.Name != "DeleteNamespace" {
continue
}
event := new(DeleteNamespaceEvent)
err := event.FromStackItem(e.Item)
if err != nil {
return nil, fmt.Errorf("failed to deserialize DeleteNamespaceEvent from stackitem (execution #%d, event #%d): %w", i, j, err)
}
res = append(res, event)
}
}
return res, nil
}
// FromStackItem converts provided [stackitem.Array] to DeleteNamespaceEvent or
// returns an error if it's not possible to do to so.
func (e *DeleteNamespaceEvent) FromStackItem(item *stackitem.Array) error {
if item == nil {
return errors.New("nil item")
}
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not an array")
}
if len(arr) != 1 {
return errors.New("wrong number of structure elements")
}
var (
index = -1
err error
)
index++
e.Namespace, err = func(item stackitem.Item) (string, error) {
b, err := item.TryBytes()
if err != nil {
return "", err
}
if !utf8.Valid(b) {
return "", errors.New("not a UTF-8 string")
}
return string(b), nil
}(arr[index])
if err != nil {
return fmt.Errorf("field Namespace: %w", err)
}
return nil
}
// AddSubjectToNamespaceEventsFromApplicationLog retrieves a set of all emitted events // AddSubjectToNamespaceEventsFromApplicationLog retrieves a set of all emitted events
// with "AddSubjectToNamespace" name from the provided [result.ApplicationLog]. // with "AddSubjectToNamespace" name from the provided [result.ApplicationLog].
func AddSubjectToNamespaceEventsFromApplicationLog(log *result.ApplicationLog) ([]*AddSubjectToNamespaceEvent, error) { func AddSubjectToNamespaceEventsFromApplicationLog(log *result.ApplicationLog) ([]*AddSubjectToNamespaceEvent, error) {

View file

@ -286,28 +286,6 @@ func (c *Contract) RegisterUnsigned(name string, owner util.Uint160, email strin
return c.actor.MakeUnsignedRun(script, nil) return c.actor.MakeUnsignedRun(script, nil)
} }
// Renew creates a transaction invoking `renew` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) Renew(name string) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "renew", name)
}
// RenewTransaction creates a transaction invoking `renew` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) RenewTransaction(name string) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "renew", name)
}
// RenewUnsigned creates a transaction invoking `renew` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) RenewUnsigned(name string) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "renew", nil, name)
}
// SetAdmin creates a transaction invoking `setAdmin` method of the contract. // SetAdmin creates a transaction invoking `setAdmin` method of the contract.
// This transaction is signed and immediately sent to the network. // This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any. // The values returned are its hash, ValidUntilBlock value and error if any.

View file

@ -189,6 +189,79 @@ func TestFrostFSID_Client_NamespaceManagement(t *testing.T) {
subjects, err = ffsid.cli.ListNamespaceSubjects(namespace) subjects, err = ffsid.cli.ListNamespaceSubjects(namespace)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, subjects) require.Empty(t, subjects)
namespace2 := "namespace2"
ffsid.a.await(ffsid.cli.CreateNamespace(namespace2))
ns1, err := ffsid.cli.GetNamespace(namespace)
require.NoError(t, err)
require.Equal(t, namespace, ns1.Name)
require.Equal(t, client.Active, ns1.State)
nsExt2, err := ffsid.cli.GetNamespaceExtended(namespace2)
require.NoError(t, err)
require.Equal(t, namespace2, nsExt2.Name)
require.Equal(t, client.Active, nsExt2.State)
ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Frozen))
nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2)
require.NoError(t, err)
require.Equal(t, namespace2, nsExt2.Name)
require.Equal(t, client.Frozen, nsExt2.State)
_, _, err = ffsid.cli.DeleteNamespace(namespace2)
require.ErrorContains(t, err, "namespace should be in 'purge' state for deletion")
ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Active))
nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2)
require.NoError(t, err)
require.Equal(t, client.Active, nsExt2.State)
subjKey2, subjAddr2 := newKey(t)
ffsid.a.await(ffsid.cli.CreateSubject(namespace2, subjKey2.PublicKey()))
subj2, err := ffsid.cli.GetSubject(subjAddr2)
require.NoError(t, err)
require.Equal(t, namespace2, subj2.Namespace)
ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Purge))
nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2)
require.NoError(t, err)
require.Equal(t, client.Purge, nsExt2.State)
_, _, err = ffsid.cli.DeleteNamespace(namespace2)
require.ErrorContains(t, err, "can't delete non-empty namespace: users still present")
ffsid.a.await(ffsid.cli.DeleteSubject(subjAddr2))
ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Active))
nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2)
require.NoError(t, err)
require.Equal(t, client.Active, nsExt2.State)
groupName := "ns_group"
ffsid.a.await(ffsid.cli.CreateGroup(namespace2, groupName))
group, err := ffsid.cli.GetGroupByName(namespace2, groupName)
require.NoError(t, err)
ffsid.a.await(ffsid.cli.UpdateNamespace(namespace2, client.Purge))
nsExt2, err = ffsid.cli.GetNamespaceExtended(namespace2)
require.NoError(t, err)
require.Equal(t, client.Purge, nsExt2.State)
_, _, err = ffsid.cli.DeleteNamespace(namespace2)
require.ErrorContains(t, err, "can't delete non-empty namespace: groups still present")
ffsid.a.await(ffsid.cli.DeleteGroup(namespace2, group.ID))
ffsid.a.await(ffsid.cli.DeleteNamespace(namespace2))
_, err = ffsid.cli.GetNamespace(namespace2)
require.ErrorContains(t, err, "not found")
namespace3 := "namespace3"
_, err = ffsid.cli.GetNamespace(namespace3)
require.ErrorContains(t, err, "not found")
_, _, err = ffsid.cli.DeleteNamespace(namespace3)
require.NoError(t, err)
} }
func TestFrostFSID_Client_DefaultNamespace(t *testing.T) { func TestFrostFSID_Client_DefaultNamespace(t *testing.T) {
@ -580,3 +653,25 @@ func prettyPrintExtendedSubjects(subjects []*client.SubjectExtended) {
fmt.Println(sb.String()) fmt.Println(sb.String())
} }
} }
func TestFrostfsID_ConcurrentAddSubjectKey(t *testing.T) {
f := newFrostFSIDInvoker(t)
newKey := func(t *testing.T) *keys.PrivateKey {
pk, err := keys.NewPrivateKey()
require.NoError(t, err)
return pk
}
subjKey := newKey(t)
subjKeyAddr := subjKey.PublicKey().GetScriptHash()
invoker := f.OwnerInvoker()
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, defaultNamespace, subjKey.PublicKey().Bytes())
additionalKey1 := newKey(t)
additionalKey2 := newKey(t)
tx1 := invoker.PrepareInvoke(t, addSubjectKeyMethod, subjKeyAddr, additionalKey1.PublicKey().Bytes())
tx2 := invoker.PrepareInvoke(t, addSubjectKeyMethod, subjKeyAddr, additionalKey2.PublicKey().Bytes())
invoker.AddBlockCheckHalt(t, tx1, tx2)
}

View file

@ -24,7 +24,10 @@ import (
const frostfsidPath = "../frostfsid" const frostfsidPath = "../frostfsid"
const defaultNamespace = "" const (
defaultNamespace = ""
customNamespace = "custom"
)
const ( const (
setAdminMethod = "setAdmin" setAdminMethod = "setAdmin"
@ -47,6 +50,7 @@ const (
getNamespaceMethod = "getNamespace" getNamespaceMethod = "getNamespace"
getNamespaceExtendedMethod = "getNamespaceExtended" getNamespaceExtendedMethod = "getNamespaceExtended"
updateNamespaceMethod = "updateNamespace" updateNamespaceMethod = "updateNamespace"
deleteNamespaceMethod = "deleteNamespace"
listNamespacesMethod = "listNamespaces" listNamespacesMethod = "listNamespaces"
listNamespaceSubjectsMethod = "listNamespaceSubjects" listNamespaceSubjectsMethod = "listNamespaceSubjects"
@ -66,7 +70,16 @@ const (
nsActiveState = "active" nsActiveState = "active"
) )
const notWitnessedError = "not witnessed" const (
frozenState = "frozen"
purgeState = "purge"
namespaceNonActive = "namespace is non-active"
notWitnessedError = "not witnessed"
notFoundError = "namespace not found"
cantDeleteNonEmptyNamespceGroupsPresent = "can't delete non-empty namespace: groups still present"
cantDeleteNonEmptyNamespceUsersPresent = "can't delete non-empty namespace: users still present"
namespaceShouldBeInPurgeStateError = "namespace should be in 'purge' state for deletion"
)
type testFrostFSIDInvoker struct { type testFrostFSIDInvoker struct {
e *neotest.Executor e *neotest.Executor
@ -306,6 +319,56 @@ func TestFrostFSID_SubjectManagement(t *testing.T) {
require.ElementsMatch(t, addresses, []util.Uint160{subjKeyAddr, newSubjKey.PublicKey().GetScriptHash()}) require.ElementsMatch(t, addresses, []util.Uint160{subjKeyAddr, newSubjKey.PublicKey().GetScriptHash()})
}) })
t.Run("subject operations for non-active namespace", func(t *testing.T) {
invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, customNamespace)
baseKey, err := keys.NewPrivateKey()
require.NoError(t, err)
baseAddr, baseBytes := baseKey.PublicKey().GetScriptHash(), baseKey.PublicKey().Bytes()
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState)
invoker.InvokeFail(t, namespaceNonActive, createSubjectMethod, customNamespace, baseBytes)
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState)
invoker.InvokeFail(t, namespaceNonActive, createSubjectMethod, customNamespace, baseBytes)
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, "active")
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, customNamespace, baseBytes)
t.Run("addSubjectKey", func(t *testing.T) {
newSubjKey, err := keys.NewPrivateKey()
require.NoError(t, err)
keyBytes := newSubjKey.PublicKey().Bytes()
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState)
invoker.InvokeFail(t, namespaceNonActive, addSubjectKeyMethod, baseAddr, keyBytes)
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState)
invoker.InvokeFail(t, namespaceNonActive, addSubjectKeyMethod, baseAddr, keyBytes)
})
t.Run("setSubjectKV", func(t *testing.T) {
const key, val = "key", "val"
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState)
invoker.InvokeFail(t, namespaceNonActive, setSubjectKVMethod, baseAddr, key, val)
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState)
invoker.InvokeFail(t, namespaceNonActive, setSubjectKVMethod, baseAddr, key, val)
})
t.Run("setSubjectName", func(t *testing.T) {
const login = "testlogin"
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState)
invoker.InvokeFail(t, namespaceNonActive, setSubjectNameMethod, baseAddr, login)
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState)
invoker.InvokeFail(t, namespaceNonActive, setSubjectNameMethod, baseAddr, login)
})
})
anonInvoker.InvokeFail(t, notWitnessedError, deleteSubjectMethod, subjKeyAddr) anonInvoker.InvokeFail(t, notWitnessedError, deleteSubjectMethod, subjKeyAddr)
invoker.Invoke(t, stackitem.Null{}, deleteSubjectMethod, subjKeyAddr) invoker.Invoke(t, stackitem.Null{}, deleteSubjectMethod, subjKeyAddr)
@ -514,6 +577,59 @@ func TestFrostFSID_NamespaceManagement(t *testing.T) {
require.Equal(t, namespace, ns.Name) require.Equal(t, namespace, ns.Name)
require.Equal(t, "frozen", ns.State) require.Equal(t, "frozen", ns.State)
}) })
t.Run("delete namespace", func(t *testing.T) {
namespace3 := "some-namespace3"
invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, namespace3)
s, err = invoker.TestInvoke(t, getNamespaceMethod, namespace3)
require.NoError(t, err)
ns := parseNamespace(t, s.Pop().Item())
require.Equal(t, namespace3, ns.Name)
t.Run("delete existing namespace not in a 'purge' state", func(t *testing.T) {
anonInvoker.InvokeFail(t, notWitnessedError, deleteNamespaceMethod, namespace3)
invoker.InvokeFail(t, namespaceShouldBeInPurgeStateError, deleteNamespaceMethod, namespace3)
})
subjKey, err := keys.NewPrivateKey()
subjKeyAddr := subjKey.PublicKey().GetScriptHash()
require.NoError(t, err)
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, ns.Name, subjKey.PublicKey().Bytes())
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, namespace3, "purge")
t.Run("delete namespace with user fails", func(t *testing.T) {
invoker.InvokeFail(t, cantDeleteNonEmptyNamespceUsersPresent, deleteNamespaceMethod, namespace3)
})
invoker.Invoke(t, stackitem.Null{}, deleteSubjectMethod, subjKeyAddr)
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, namespace3, "active")
groupID1 := int64(1)
groupName1 := "group1"
invoker.Invoke(t, stackitem.Make(groupID1), createGroupMethod, ns.Name, groupName1)
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, namespace3, "purge")
t.Run("delete namespace with group fails", func(t *testing.T) {
invoker.InvokeFail(t, cantDeleteNonEmptyNamespceGroupsPresent, deleteNamespaceMethod, namespace3)
})
invoker.Invoke(t, stackitem.Null{}, deleteGroupMethod, ns.Name, groupID1)
t.Run("delete existing namespace", func(t *testing.T) {
anonInvoker.InvokeFail(t, notWitnessedError, deleteNamespaceMethod, namespace3)
invoker.Invoke(t, stackitem.Null{}, deleteNamespaceMethod, namespace3)
invoker.InvokeFail(t, notFoundError, getNamespaceMethod, namespace3)
})
t.Run("delete non-existing namespace", func(t *testing.T) {
nonExistingNamespace := "non-existing-namespace"
invoker.InvokeFail(t, notFoundError, getNamespaceMethod, nonExistingNamespace)
anonInvoker.InvokeFail(t, notWitnessedError, deleteNamespaceMethod, nonExistingNamespace)
invoker.Invoke(t, stackitem.Null{}, deleteNamespaceMethod, nonExistingNamespace)
})
})
} }
func TestFrostFSID_GroupManagement(t *testing.T) { func TestFrostFSID_GroupManagement(t *testing.T) {
@ -635,6 +751,50 @@ func TestFrostFSID_GroupManagement(t *testing.T) {
groups := parseGroups(t, readIteratorAll(s)) groups := parseGroups(t, readIteratorAll(s))
require.Empty(t, groups) require.Empty(t, groups)
}) })
t.Run("operations with non-active namespace", func(t *testing.T) {
invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, customNamespace)
customGroupID := int64(2)
customGroupName := "customGroup"
invoker.Invoke(t, stackitem.Make(customGroupID), createGroupMethod, customNamespace, customGroupName)
subjKey, err := keys.NewPrivateKey()
require.NoError(t, err)
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, customNamespace, subjKey.PublicKey().Bytes())
t.Run("createGroup", func(t *testing.T) {
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState)
invoker.InvokeFail(t, namespaceNonActive, createGroupMethod, customNamespace, customGroupName)
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState)
invoker.InvokeFail(t, namespaceNonActive, createGroupMethod, customNamespace, customGroupName)
})
t.Run("addSubjectToGroup", func(t *testing.T) {
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState)
invoker.InvokeFail(t, namespaceNonActive, addSubjectToGroupMethod, subjKey.PublicKey().GetScriptHash(), customGroupID)
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState)
invoker.InvokeFail(t, namespaceNonActive, addSubjectToGroupMethod, subjKey.PublicKey().GetScriptHash(), customGroupID)
})
t.Run("setGroupKV", func(t *testing.T) {
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState)
invoker.InvokeFail(t, namespaceNonActive, setGroupKVMethod, customNamespace, customGroupID, client.IAMARNKey, "arn")
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState)
invoker.InvokeFail(t, namespaceNonActive, setGroupKVMethod, customNamespace, customGroupID, client.IAMARNKey, "arn")
})
t.Run("setGroupName", func(t *testing.T) {
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, frozenState)
invoker.InvokeFail(t, namespaceNonActive, setGroupNameMethod, customNamespace, customGroupID, "newCustomGroup")
invoker.Invoke(t, stackitem.Null{}, updateNamespaceMethod, customNamespace, purgeState)
invoker.InvokeFail(t, namespaceNonActive, setGroupNameMethod, customNamespace, customGroupID, "newCustomGroup")
})
})
} }
func TestAdditionalKeyFromPrimarySubject(t *testing.T) { func TestAdditionalKeyFromPrimarySubject(t *testing.T) {

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"path" "path"
"strings"
"testing" "testing"
"time" "time"
@ -270,7 +269,58 @@ func TestNNSRegister(t *testing.T) {
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com"))}) expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com"))})
c.CheckTxNotificationEvent(t, tx, 4, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteDomain", Item: expected}) c.CheckTxNotificationEvent(t, tx, 4, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteDomain", Item: expected})
c.InvokeFail(t, "token not found", "getRecords", "testdomain.com", int64(nns.SOA)) c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.SOA))
}
func TestDeleteRecords_SubdomainNoRegister(t *testing.T) {
c := newNNSInvoker(t, true)
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c.Invoke(t, true, "register",
"test.com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
checkRecords := func(t *testing.T, domain string, typ nns.RecordType, expected ...string) {
s, err := c.TestInvoke(t, "getRecords", domain, int64(typ))
require.NoError(t, err)
if len(expected) == 0 {
_, ok := s.Pop().Item().(stackitem.Null)
require.True(t, ok, "expected 0 records")
return
}
arr, ok := s.Pop().Value().([]stackitem.Item)
require.True(t, ok, "expected an array '%s' %d", domain, typ)
actual := make([]string, len(arr))
for i := range actual {
b, err := arr[i].TryBytes()
require.NoError(t, err)
actual[i] = string(b)
}
require.ElementsMatch(t, expected, actual)
}
c.Invoke(t, stackitem.Null{}, "addRecord", "a.test.com", int64(nns.TXT), "recA1")
c.Invoke(t, stackitem.Null{}, "addRecord", "a.test.com", int64(nns.TXT), "recA2")
c.Invoke(t, stackitem.Null{}, "addRecord", "b.test.com", int64(nns.TXT), "recB")
c.Invoke(t, stackitem.Null{}, "addRecord", "test.com", int64(nns.TXT), "recTop")
{ // Delete subdomain records.
c.Invoke(t, stackitem.Null{}, "deleteRecords", "a.test.com", int64(nns.TXT))
checkRecords(t, "test.com", nns.TXT, "recTop")
checkRecords(t, "a.test.com", nns.TXT)
checkRecords(t, "b.test.com", nns.TXT, "recB")
}
{ // Delete domain records.
c.Invoke(t, stackitem.Null{}, "deleteRecords", "test.com", int64(nns.TXT))
checkRecords(t, "test.com", nns.TXT)
checkRecords(t, "a.test.com", nns.TXT)
checkRecords(t, "b.test.com", nns.TXT, "recB")
}
} }
func TestDeleteDomain(t *testing.T) { func TestDeleteDomain(t *testing.T) {
@ -387,6 +437,10 @@ func TestTLDRecord(t *testing.T) {
result := []stackitem.Item{stackitem.NewByteArray([]byte("1.2.3.4"))} result := []stackitem.Item{stackitem.NewByteArray([]byte("1.2.3.4"))}
c.Invoke(t, result, "resolve", "com", int64(nns.A)) c.Invoke(t, result, "resolve", "com", int64(nns.A))
t.Run("subdomain", func(t *testing.T) {
c.Invoke(t, stackitem.Null{}, "addRecord", "a.com", int64(nns.TXT), "test=frostfs")
})
} }
func TestNNSRegisterMulti(t *testing.T) { func TestNNSRegisterMulti(t *testing.T) {
@ -500,45 +554,6 @@ func TestNNSGetAllRecords(t *testing.T) {
require.False(t, iter.Next()) require.False(t, iter.Next())
} }
func TestExpiration(t *testing.T) {
c := newNNSInvoker(t, true)
refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*10), int64(104)
c.Invoke(t, true, "register",
"testdomain.com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
checkProperties := func(t *testing.T, expiration uint64) {
expected := stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")},
{Key: stackitem.Make("expiration"), Value: stackitem.Make(expiration)},
})
s, err := c.TestInvoke(t, "properties", "testdomain.com")
require.NoError(t, err)
require.Equal(t, expected.Value(), s.Top().Item().Value())
}
top := c.TopBlock(t)
expiration := top.Timestamp + uint64(expire*1000)
checkProperties(t, expiration)
b := c.NewUnsignedBlock(t)
b.Timestamp = expiration - 2 // test invoke is done with +1 timestamp
require.NoError(t, c.Chain.AddBlock(c.SignBlock(b)))
checkProperties(t, expiration)
b = c.NewUnsignedBlock(t)
b.Timestamp = expiration - 1
require.NoError(t, c.Chain.AddBlock(c.SignBlock(b)))
_, err := c.TestInvoke(t, "properties", "testdomain.com")
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "name has expired"))
c.InvokeFail(t, "name has expired", "getAllRecords", "testdomain.com")
c.InvokeFail(t, "name has expired", "ownerOf", "testdomain.com")
}
func TestNNSSetAdmin(t *testing.T) { func TestNNSSetAdmin(t *testing.T) {
c := newNNSInvoker(t, true) c := newNNSInvoker(t, true)
@ -640,31 +655,6 @@ func TestNNSIsAvailable(t *testing.T) {
c.InvokeFail(t, "domain name too long", "isAvailable", getTooLongDomainName(255)) c.InvokeFail(t, "domain name too long", "isAvailable", getTooLongDomainName(255))
} }
func TestNNSRenew(t *testing.T) {
c := newNNSInvoker(t, true)
acc := c.NewAccount(t)
c1 := c.WithSigners(c.Committee, acc)
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
c1.Invoke(t, true, "register",
"testdomain.com", c.CommitteeHash,
"myemail@frostfs.info", refresh, retry, expire, ttl)
const msPerYear = 365 * 24 * time.Hour / time.Millisecond
b := c.TopBlock(t)
ts := b.Timestamp + uint64(expire*1000) + uint64(msPerYear)
cAcc := c.WithSigners(acc)
cAcc.InvokeFail(t, "not witnessed by admin", "renew", "testdomain.com")
c1.Invoke(t, ts, "renew", "testdomain.com")
expected := stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")},
{Key: stackitem.Make("expiration"), Value: stackitem.Make(ts)},
})
cAcc.Invoke(t, expected, "properties", "testdomain.com")
c.InvokeFail(t, "domain name too long", "renew", getTooLongDomainName(255))
}
func TestNNSResolve(t *testing.T) { func TestNNSResolve(t *testing.T) {
c := newNNSInvoker(t, true) c := newNNSInvoker(t, true)

View file

@ -5,35 +5,76 @@ import (
"path" "path"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
"git.frostfs.info/TrueCloudLab/frostfs-contract/policy" "git.frostfs.info/TrueCloudLab/frostfs-contract/policy"
"github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "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/neotest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const policyPath = "../policy" const policyPath = "../policy"
func deployPolicyContract(t *testing.T, e *neotest.Executor) util.Uint160 { 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") cfgPath := path.Join(policyPath, "config.yml")
c := neotest.CompileFile(t, e.CommitteeHash, policyPath, cfgPath) c := neotest.CompileFile(t, e.CommitteeHash, policyPath, cfgPath)
e.DeployContract(t, c, []any{nil}) e.DeployContract(t, c, []any{nil})
return c.Hash return e.CommitteeInvoker(c.Hash)
}
func newPolicyInvoker(t *testing.T) *neotest.ContractInvoker {
e := newExecutor(t)
h := deployPolicyContract(t, e)
return e.CommitteeInvoker(h)
} }
func TestPolicy(t *testing.T) { func TestPolicy(t *testing.T) {
e := newPolicyInvoker(t) c := newPolicyInvokers(t)
checkChainsIteratorByPrefix(t, e, policy.Namespace, "mynamespace", "ingress", [][]byte{}) checkChainsIteratorByPrefix(t, c.policy, policy.Namespace, "mynamespace", "ingress", [][]byte{})
checkChainsIteratorByPrefix(t, e, policy.Container, "cnr1", "ingress", [][]byte{}) checkChainsIteratorByPrefix(t, c.policy, policy.Container, "cnr1", "ingress", [][]byte{})
// Policies are opaque to the contract and are just raw bytes to store. // Policies are opaque to the contract and are just raw bytes to store.
p1 := []byte("chain1") p1 := []byte("chain1")
@ -41,82 +82,97 @@ func TestPolicy(t *testing.T) {
p3 := []byte("chain3") p3 := []byte("chain3")
p33 := []byte("chain33") p33 := []byte("chain33")
e.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1) c.frostfsid.Invoke(t, stackitem.Null{}, "createNamespace", "mynamespace")
checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")})
checkChains(t, e, "mynamespace", "", "ingress", [][]byte{p1})
checkChains(t, e, "mynamespace", "", "all", nil)
e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule2", p2) c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1)
checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")}) checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")})
checkTargets(t, e, policy.Container, [][]byte{[]byte("cnr1")}) checkChains(t, c.policy, "mynamespace", "", "ingress", [][]byte{p1})
checkChains(t, e, "mynamespace", "", "ingress", [][]byte{p1}) // Only namespace chains. checkChains(t, c.policy, "mynamespace", "", "all", nil)
checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2})
checkChains(t, e, "mynamespace", "cnr1", "all", nil) // No chains attached to 'all'.
checkChains(t, e, "mynamespace", "cnr2", "ingress", [][]byte{p1}) // Only namespace, no chains for the container.
e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p3) c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule2", p2)
checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")}) checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")})
checkTargets(t, e, policy.Container, [][]byte{[]byte("cnr1")}) checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")})
checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p3}) 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.
e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33) c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p3)
checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")}) checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")})
checkTargets(t, e, policy.Container, [][]byte{[]byte("cnr1")}) checkTargets(t, c.policy, policy.Container, [][]byte{[]byte("cnr1")})
checkChain(t, e, policy.Container, "cnr1", "ingress:myrule3", p33) checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p3})
checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p1, p2, p33}) // Override chain.
checkChainsByPrefix(t, e, policy.Container, "cnr1", "", [][]byte{p2, p33})
checkChainsByPrefix(t, e, policy.IAM, "", "", nil)
checkChainKeys(t, e, policy.Container, "cnr1", []string{"ingress:myrule2", "ingress:myrule3"})
checkChainsIteratorByPrefix(t, e, policy.Container, "cnr1", "ingress:myrule3", [][]byte{p33}) c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33)
checkChainsIteratorByPrefix(t, e, policy.Container, "cnr1", "ingress", [][]byte{p2, 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("removal", func(t *testing.T) {
t.Run("wrong name", func(t *testing.T) { t.Run("wrong name", func(t *testing.T) {
e.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress") c.policy.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress")
checkChains(t, e, "mynamespace", "", "ingress", [][]byte{p1}) checkChains(t, c.policy, "mynamespace", "", "ingress", [][]byte{p1})
}) })
e.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress:123") c.policy.Invoke(t, stackitem.Null{}, "removeChain", policy.Namespace, "mynamespace", "ingress:123")
checkChains(t, e, "mynamespace", "", "ingress", nil) checkChains(t, c.policy, "mynamespace", "", "ingress", nil)
checkChains(t, e, "mynamespace", "cnr1", "ingress", [][]byte{p2, p33}) // Container chains still exist. checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", [][]byte{p2, p33}) // Container chains still exist.
// Remove by prefix. // Remove by prefix.
e.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress") c.policy.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress")
checkChains(t, e, "mynamespace", "cnr1", "ingress", nil) checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", nil)
// Remove by prefix. // Remove by prefix.
e.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress") c.policy.Invoke(t, stackitem.Null{}, "removeChainsByPrefix", policy.Container, "cnr1", "ingress")
checkChains(t, e, "mynamespace", "cnr1", "ingress", nil) checkChains(t, c.policy, "mynamespace", "cnr1", "ingress", nil)
checkTargets(t, e, policy.Namespace, [][]byte{}) checkTargets(t, c.policy, policy.Namespace, [][]byte{})
checkTargets(t, e, policy.Container, [][]byte{}) checkTargets(t, c.policy, policy.Container, [][]byte{})
}) })
t.Run("add again after removal", func(t *testing.T) { t.Run("add again after removal", func(t *testing.T) {
e.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1) c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Namespace, "mynamespace", "ingress:123", p1)
e.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33) c.policy.Invoke(t, stackitem.Null{}, "addChain", policy.Container, "cnr1", "ingress:myrule3", p33)
checkTargets(t, e, policy.Namespace, [][]byte{[]byte("mynamespace")}) checkTargets(t, c.policy, policy.Namespace, [][]byte{[]byte("mynamespace")})
checkTargets(t, e, policy.Container, [][]byte{[]byte("cnr1")}) 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) { func TestAutorization(t *testing.T) {
e := newPolicyInvoker(t) c := newPolicyInvokers(t)
e.Invoke(t, stackitem.Null{}, "getAdmin") c.policy.Invoke(t, stackitem.Null{}, "getAdmin")
s := e.NewAccount(t, 1_0000_0000) s := c.policy.NewAccount(t, 1_0000_0000)
c := e.WithSigners(s) cs := c.policy.WithSigners(s)
args := []any{policy.Container, "cnr1", "ingress:myrule3", []byte("opaque")} args := []any{policy.Container, "cnr1", "ingress:myrule3", []byte("opaque")}
c.InvokeFail(t, policy.ErrNotAuthorized, "addChain", args...) cs.InvokeFail(t, policy.ErrNotAuthorized, "addChain", args...)
e.Invoke(t, stackitem.Null{}, "setAdmin", s.ScriptHash()) c.policy.Invoke(t, stackitem.Null{}, "setAdmin", s.ScriptHash())
e.Invoke(t, stackitem.NewBuffer(s.ScriptHash().BytesBE()), "getAdmin") c.policy.Invoke(t, stackitem.NewBuffer(s.ScriptHash().BytesBE()), "getAdmin")
c.Invoke(t, stackitem.Null{}, "addChain", args...) cs.Invoke(t, stackitem.Null{}, "addChain", args...)
} }
func checkChains(t *testing.T, e *neotest.ContractInvoker, namespace, container, name string, expected [][]byte) { func checkChains(t *testing.T, e *neotest.ContractInvoker, namespace, container, name string, expected [][]byte) {