diff --git a/frostfsid/client/client.go b/frostfsid/client/client.go index e92fa33..570df8a 100644 --- a/frostfsid/client/client.go +++ b/frostfsid/client/client.go @@ -57,21 +57,26 @@ type ( } Group struct { + ID int64 Name string Namespace string + KV map[string]string } GroupExtended struct { + ID int64 Name string Namespace string + KV map[string]string SubjectsCount int64 } ) const ( - SubjectIAMPathKey = "iam-path" - SubjectCreatedTimeKey = "ctime" - SubjectModifiedTimeKey = "mtime" + IAMPathKey = "iam-path" + IAMARNKey = "iam-arn" + IAMCreatedTimeKey = "ctime" + IAMModifiedTimeKey = "mtime" ) const iteratorBatchSize = 100 @@ -107,6 +112,10 @@ const ( createGroupMethod = "createGroup" getGroupMethod = "getGroup" getGroupExtendedMethod = "getGroupExtended" + getGroupIDByNameMethod = "getGroupIDByName" + setGroupNameMethod = "setGroupName" + setGroupKVMethod = "setGroupKV" + deleteGroupKVMethod = "deleteGroupKV" listGroupsMethod = "listGroups" addSubjectToGroupMethod = "addSubjectToGroup" removeSubjectFromGroupMethod = "removeSubjectFromGroup" @@ -240,13 +249,13 @@ func (c Client) RemoveSubjectKeyCall(addr util.Uint160, key *keys.PublicKey) (me // SetSubjectKV updates subject kv map. // Must be invoked by contract owner. -// You can use some predefined key constants: SubjectLoginKey, SubjectIAMPathKey, SubjectCreatedTimeKey, SubjectModifiedTimeKey. +// You can use some predefined key constants: IAMPathKey, IAMARNKey, IAMCreatedTimeKey, IAMModifiedTimeKey. func (c Client) SetSubjectKV(addr util.Uint160, key, val string) (tx util.Uint256, vub uint32, err error) { method, args := c.SetSubjectKVCall(addr, key, val) return c.act.SendCall(c.contract, method, args...) } -// SetSubjectKVCall provides args for SetSubjectLogin to use in commonclient.Transaction. +// SetSubjectKVCall provides args for SetSubjectKV to use in commonclient.Transaction. func (c Client) SetSubjectKVCall(addr util.Uint160, key, val string) (method string, args []any) { return setSubjectKVMethod, []any{addr, key, val} } @@ -258,7 +267,7 @@ func (c Client) SetSubjectName(addr util.Uint160, name string) (tx util.Uint256, return c.act.SendCall(c.contract, method, args...) } -// SetSubjectNameCall provides args for SetSubjectLogin to use in commonclient.Transaction. +// SetSubjectNameCall provides args for SetSubjectName to use in commonclient.Transaction. func (c Client) SetSubjectNameCall(addr util.Uint160, name string) (method string, args []any) { return setSubjectNameMethod, []any{addr, name} } @@ -270,7 +279,7 @@ func (c Client) DeleteSubjectKV(addr util.Uint160, key string) (tx util.Uint256, return c.act.SendCall(c.contract, method, args...) } -// DeleteSubjectKVCall provides args for SetSubjectLogin to use in commonclient.Transaction. +// DeleteSubjectKVCall provides args for DeleteSubjectKV to use in commonclient.Transaction. func (c Client) DeleteSubjectKVCall(addr util.Uint160, key string) (method string, args []any) { return deleteSubjectKVMethod, []any{addr, key} } @@ -386,8 +395,8 @@ func (c Client) CreateGroupCall(namespace, group string) (method string, args [] } // GetGroup gets group. -func (c Client) GetGroup(namespace, group string) (*Group, error) { - items, err := unwrap.Array(c.act.Call(c.contract, getGroupMethod, namespace, group)) +func (c Client) GetGroup(namespace string, groupID int64) (*Group, error) { + items, err := unwrap.Array(c.act.Call(c.contract, getGroupMethod, namespace, groupID)) if err != nil { return nil, err } @@ -396,8 +405,8 @@ func (c Client) GetGroup(namespace, group string) (*Group, error) { } // GetGroupExtended gets extended group. -func (c Client) GetGroupExtended(namespace, group string) (*GroupExtended, error) { - items, err := unwrap.Array(c.act.Call(c.contract, getGroupExtendedMethod, namespace, group)) +func (c Client) GetGroupExtended(namespace string, groupID int64) (*GroupExtended, error) { + items, err := unwrap.Array(c.act.Call(c.contract, getGroupExtendedMethod, namespace, groupID)) if err != nil { return nil, err } @@ -405,6 +414,48 @@ func (c Client) GetGroupExtended(namespace, group string) (*GroupExtended, error return parseGroupExtended(items) } +// SetGroupName updates subject name. +// Must be invoked by contract owner. +func (c Client) SetGroupName(namespace string, groupID int64, name string) (tx util.Uint256, vub uint32, err error) { + method, args := c.SetGroupNameCall(namespace, groupID, name) + return c.act.SendCall(c.contract, method, args...) +} + +// SetGroupNameCall provides args for SetGroupName to use in commonclient.Transaction. +func (c Client) SetGroupNameCall(namespace string, groupID int64, name string) (method string, args []any) { + return setGroupNameMethod, []any{namespace, groupID, name} +} + +// SetGroupKV updates group kv map. +// Must be invoked by contract owner. +// You can use some predefined key constants: IAMPathKey, IAMARNKey, IAMCreatedTimeKey, IAMModifiedTimeKey. +func (c Client) SetGroupKV(namespace string, groupID int64, key, val string) (tx util.Uint256, vub uint32, err error) { + method, args := c.SetGroupKVCall(namespace, groupID, key, val) + return c.act.SendCall(c.contract, method, args...) +} + +// SetGroupKVCall provides args for SetGroupKV to use in commonclient.Transaction. +func (c Client) SetGroupKVCall(namespace string, groupID int64, key, val string) (method string, args []any) { + return setGroupKVMethod, []any{namespace, groupID, key, val} +} + +// DeleteGroupKV removes group kv map. +// Must be invoked by contract owner. +func (c Client) DeleteGroupKV(namespace string, groupID int64, key string) (tx util.Uint256, vub uint32, err error) { + method, args := c.DeleteGroupKVCall(namespace, groupID, key) + return c.act.SendCall(c.contract, method, args...) +} + +// DeleteGroupKVCall provides args for DeleteGroupKV to use in commonclient.Transaction. +func (c Client) DeleteGroupKVCall(namespace string, groupID int64, key string) (method string, args []any) { + return deleteGroupKVMethod, []any{namespace, groupID, key} +} + +// GetGroupIDByName gets group id its name (namespace scope). +func (c Client) GetGroupIDByName(namespace, groupName string) (int64, error) { + return unwrap.Int64(c.act.Call(c.contract, getGroupIDByNameMethod, namespace, groupName)) +} + // ListGroups gets all groups in specific namespace. func (c Client) ListGroups(namespace string) ([]*Group, error) { items, err := commonclient.ReadIteratorItems(c.act, iteratorBatchSize, c.contract, listGroupsMethod, namespace) @@ -417,43 +468,43 @@ func (c Client) ListGroups(namespace string) ([]*Group, error) { // AddSubjectToGroup adds a new subject to group. // Must be invoked by contract owner. -func (c Client) AddSubjectToGroup(addr util.Uint160, group string) (tx util.Uint256, vub uint32, err error) { - method, args := c.AddSubjectToGroupCall(addr, group) +func (c Client) AddSubjectToGroup(addr util.Uint160, groupID int64) (tx util.Uint256, vub uint32, err error) { + method, args := c.AddSubjectToGroupCall(addr, groupID) return c.act.SendCall(c.contract, method, args...) } // AddSubjectToGroupCall provides args for AddSubjectToGroup to use in commonclient.Transaction. -func (c Client) AddSubjectToGroupCall(addr util.Uint160, group string) (method string, args []any) { - return addSubjectToGroupMethod, []any{addr, group} +func (c Client) AddSubjectToGroupCall(addr util.Uint160, groupID int64) (method string, args []any) { + return addSubjectToGroupMethod, []any{addr, groupID} } // RemoveSubjectFromGroup removes subject from group. // Must be invoked by contract owner. -func (c Client) RemoveSubjectFromGroup(addr util.Uint160, namespace, group string) (tx util.Uint256, vub uint32, err error) { - method, args := c.RemoveSubjectFromGroupCall(addr, namespace, group) +func (c Client) RemoveSubjectFromGroup(addr util.Uint160, groupID int64) (tx util.Uint256, vub uint32, err error) { + method, args := c.RemoveSubjectFromGroupCall(addr, groupID) return c.act.SendCall(c.contract, method, args...) } // RemoveSubjectFromGroupCall provides args for RemoveSubjectFromGroup to use in commonclient.Transaction. -func (c Client) RemoveSubjectFromGroupCall(addr util.Uint160, namespace, group string) (method string, args []any) { - return removeSubjectFromGroupMethod, []any{addr, namespace, group} +func (c Client) RemoveSubjectFromGroupCall(addr util.Uint160, groupID int64) (method string, args []any) { + return removeSubjectFromGroupMethod, []any{addr, groupID} } // ListGroupSubjects gets all subjects in specific group. -func (c Client) ListGroupSubjects(namespace, group string) ([]util.Uint160, error) { - return unwrapArrayOfUint160(commonclient.ReadIteratorItems(c.act, iteratorBatchSize, c.contract, listGroupSubjectsMethod, namespace, group)) +func (c Client) ListGroupSubjects(namespace string, groupID int64) ([]util.Uint160, error) { + return unwrapArrayOfUint160(commonclient.ReadIteratorItems(c.act, iteratorBatchSize, c.contract, listGroupSubjectsMethod, namespace, groupID)) } // DeleteGroup deletes group. // Must be invoked by contract owner. -func (c Client) DeleteGroup(namespace, group string) (tx util.Uint256, vub uint32, err error) { - method, args := c.DeleteGroupCall(namespace, group) +func (c Client) DeleteGroup(namespace string, groupID int64) (tx util.Uint256, vub uint32, err error) { + method, args := c.DeleteGroupCall(namespace, groupID) return c.act.SendCall(c.contract, method, args...) } // DeleteGroupCall provides args for DeleteGroup to use in commonclient.Transaction. -func (c Client) DeleteGroupCall(namespace, group string) (method string, args []any) { - return deleteGroupMethod, []any{namespace, group} +func (c Client) DeleteGroupCall(namespace string, groupID int64) (method string, args []any) { + return deleteGroupMethod, []any{namespace, groupID} } // ListNonEmptyNamespaces gets namespaces that contain at least one subject. @@ -484,6 +535,15 @@ func (c Client) Wait(tx util.Uint256, vub uint32, err error) (*state.AppExecResu return c.act.Wait(tx, vub, err) } +// ParseGroupID fetch groupID from stack after creating group method invocation. +func (c Client) ParseGroupID(res *state.AppExecResult, err error) (int64, error) { + if err != nil { + return 0, err + } + + return unwrap.Int64(makeResFromAppExec(res)) +} + // ListNonEmptyGroups gets groups that contain at least one subject. func (c Client) ListNonEmptyGroups(namespace string) ([]string, error) { groups, err := c.ListGroups(namespace) @@ -494,7 +554,7 @@ func (c Client) ListNonEmptyGroups(namespace string) ([]string, error) { var res []string for _, group := range groups { - groupExt, err := c.GetGroupExtended(namespace, group.Name) + groupExt, err := c.GetGroupExtended(namespace, group.ID) if err != nil { return nil, err } @@ -522,6 +582,13 @@ func makeValidRes(item stackitem.Item) (*result.Invoke, error) { }, nil } +func makeResFromAppExec(res *state.AppExecResult) (*result.Invoke, error) { + return &result.Invoke{ + Stack: res.Stack, + State: res.VMState.String(), + }, nil +} + func parseSubject(structArr []stackitem.Item) (*Subject, error) { if len(structArr) < 5 { return nil, errors.New("invalid response subject struct") @@ -710,49 +777,73 @@ func parseNamespaces(items []stackitem.Item) ([]*Namespace, error) { } func parseGroup(structArr []stackitem.Item) (*Group, error) { - if len(structArr) < 2 { + if len(structArr) < 4 { return nil, errors.New("invalid response group struct") } - name, err := structArr[0].TryBytes() + groupID, err := structArr[0].TryInteger() if err != nil { return nil, err } - namespace, err := structArr[1].TryBytes() + name, err := structArr[1].TryBytes() + if err != nil { + return nil, err + } + + namespace, err := structArr[2].TryBytes() + if err != nil { + return nil, err + } + + kvs, err := parseMap(structArr[3]) if err != nil { return nil, err } return &Group{ + ID: groupID.Int64(), Name: string(name), Namespace: string(namespace), + KV: kvs, }, nil } func parseGroupExtended(structArr []stackitem.Item) (*GroupExtended, error) { - if len(structArr) < 3 { + if len(structArr) < 5 { return nil, errors.New("invalid response group extended struct") } - name, err := structArr[0].TryBytes() + groupID, err := structArr[0].TryInteger() if err != nil { return nil, err } - namespace, err := structArr[1].TryBytes() + name, err := structArr[1].TryBytes() if err != nil { return nil, err } - subjectCount, err := structArr[2].TryInteger() + namespace, err := structArr[2].TryBytes() + if err != nil { + return nil, err + } + + kvs, err := parseMap(structArr[3]) + if err != nil { + return nil, err + } + + subjectCount, err := structArr[4].TryInteger() if err != nil { return nil, err } return &GroupExtended{ + ID: groupID.Int64(), Name: string(name), Namespace: string(namespace), + KV: kvs, SubjectsCount: subjectCount.Int64(), }, nil } diff --git a/frostfsid/config.yml b/frostfsid/config.yml index 90b7311..36d304d 100644 --- a/frostfsid/config.yml +++ b/frostfsid/config.yml @@ -65,25 +65,51 @@ events: type: String - name: group type: String + - name: SetGroupName + parameters: + - name: namespace + type: String + - name: groupID + type: Integer + - name: name + type: String + - name: SetGroupKV + parameters: + - name: namespace + type: String + - name: groupID + type: Integer + - name: key + type: String + - name: value + type: String + - name: DeleteGroupKV + parameters: + - name: namespace + type: String + - name: groupID + type: Integer + - name: key + type: String - name: AddSubjectToGroup parameters: - name: subjectAddress type: Hash160 - name: namespace type: String - - name: group - type: String + - name: groupID + type: Integer - name: RemoveSubjectFromGroup parameters: - name: subjectAddress type: Hash160 - name: namespace type: String - - name: group - type: String + - name: groupID + type: Integer - name: DeleteGroup parameters: - name: namespace type: String - - name: group - type: String + - name: groupID + type: Integer diff --git a/frostfsid/frostfsid_contract.go b/frostfsid/frostfsid_contract.go index 6d60b9a..f53bfb2 100644 --- a/frostfsid/frostfsid_contract.go +++ b/frostfsid/frostfsid_contract.go @@ -4,6 +4,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-contract/common" "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/convert" "github.com/nspcc-dev/neo-go/pkg/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto" "github.com/nspcc-dev/neo-go/pkg/interop/native/management" @@ -41,13 +42,17 @@ type ( } Group struct { + ID int Name string Namespace string + KV map[string]string } GroupExtended struct { + ID int Name string Namespace string + KV map[string]string SubjectsCount int } ) @@ -61,6 +66,8 @@ const ( namespaceSubjectsNamesPrefix = 'l' groupKeysPrefix = 'g' groupSubjectsKeysPrefix = 'G' + groupCounterKey = 'c' + namespaceGroupsNamesPrefix = 'm' ) func _deploy(data any, isUpdate bool) { @@ -77,6 +84,8 @@ func _deploy(data any, isUpdate bool) { storage.Put(ctx, ownerKey(owner), []byte{1}) } + storage.Put(ctx, groupCounterKey, 0) + runtime.Log("frostfsid contract initialized") } @@ -560,7 +569,7 @@ func ListNamespaceSubjects(ns string) iterator.Iterator { return storage.Find(ctx, namespaceSubjectPrefix(ns), storage.KeysOnly|storage.RemovePrefix) } -func CreateGroup(ns, group string) { +func CreateGroup(ns, group string) int { ctx := storage.GetContext() checkContractOwner(ctx) @@ -577,23 +586,29 @@ func CreateGroup(ns, group string) { panic("namespace not found") } + groupCountID := storage.Get(ctx, groupCounterKey).(int) + groupCountID++ + storage.Put(ctx, groupCounterKey, groupCountID) + gr := Group{ + ID: groupCountID, Name: group, Namespace: ns, } - gKey := groupKey(ns, group) - data = storage.Get(ctx, gKey).([]byte) - if data != nil { - panic("group already exists") - } + + setNamespaceGroupName(ctx, gr) + + gKey := groupKey(ns, groupCountID) storage.Put(ctx, gKey, std.Serialize(gr)) runtime.Notify("CreateGroup", ns, group) + + return groupCountID } -func GetGroup(ns, group string) Group { +func GetGroup(ns string, groupID int) Group { ctx := storage.GetReadOnlyContext() - gKey := groupKey(ns, group) + gKey := groupKey(ns, groupID) data := storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") @@ -602,9 +617,9 @@ func GetGroup(ns, group string) Group { return std.Deserialize(data).(Group) } -func GetGroupExtended(ns, group string) GroupExtended { +func GetGroupExtended(ns string, groupID int) GroupExtended { ctx := storage.GetReadOnlyContext() - gKey := groupKey(ns, group) + gKey := groupKey(ns, groupID) data := storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") @@ -613,11 +628,13 @@ func GetGroupExtended(ns, group string) GroupExtended { gr := std.Deserialize(data).(Group) grExtended := GroupExtended{ + ID: gr.ID, Name: gr.Name, Namespace: gr.Namespace, + KV: gr.KV, } - it := storage.Find(ctx, groupSubjectPrefix(ns, group), storage.KeysOnly) + it := storage.Find(ctx, groupSubjectPrefix(ns, groupID), storage.KeysOnly) for iterator.Next(it) { grExtended.SubjectsCount += 1 } @@ -625,12 +642,85 @@ func GetGroupExtended(ns, group string) GroupExtended { return grExtended } +func GetGroupIDByName(ns, name string) int { + if ns == "" || name == "" { + panic("invalid namespace or name") + } + + ctx := storage.GetReadOnlyContext() + + nsGroupNameKey := namespaceGroupNameKey(ns, name) + groupIDRaw := storage.Get(ctx, nsGroupNameKey).([]byte) + if groupIDRaw == nil { + panic("group not found") + } + + return std.Deserialize(groupIDRaw).(int) +} + +func SetGroupName(ns string, groupID int, name string) { + ctx := storage.GetContext() + checkContractOwner(ctx) + + gKey := groupKey(ns, groupID) + data := storage.Get(ctx, gKey).([]byte) + if data == nil { + panic("group not found") + } + + gr := std.Deserialize(data).(Group) + oldName := gr.Name + gr.Name = name + storage.Put(ctx, gKey, std.Serialize(gr)) + + updateNamespaceGroupName(ctx, gr, oldName) + + runtime.Notify("SetGroupName", ns, groupID, name) +} + +func SetGroupKV(ns string, groupID int, key, val string) { + ctx := storage.GetContext() + checkContractOwner(ctx) + + gKey := groupKey(ns, groupID) + data := storage.Get(ctx, gKey).([]byte) + if data == nil { + panic("group not found") + } + + gr := std.Deserialize(data).(Group) + if gr.KV == nil { + gr.KV = map[string]string{} + } + gr.KV[key] = val + + storage.Put(ctx, gKey, std.Serialize(gr)) + runtime.Notify("SetGroupKV", ns, groupID, key, val) +} + +func DeleteGroupKV(ns string, groupID int, key string) { + ctx := storage.GetContext() + checkContractOwner(ctx) + + gKey := groupKey(ns, groupID) + data := storage.Get(ctx, gKey).([]byte) + if data == nil { + panic("group not found") + } + + gr := std.Deserialize(data).(Group) + delete(gr.KV, key) + + storage.Put(ctx, gKey, std.Serialize(gr)) + runtime.Notify("DeleteGroupKV", ns, groupID, key) +} + func ListGroups(ns string) iterator.Iterator { ctx := storage.GetReadOnlyContext() return storage.Find(ctx, groupPrefix(ns), storage.ValuesOnly|storage.DeserializeValues) } -func AddSubjectToGroup(addr interop.Hash160, group string) { +func AddSubjectToGroup(addr interop.Hash160, groupID int) { ctx := storage.GetContext() checkContractOwner(ctx) @@ -652,19 +742,19 @@ func AddSubjectToGroup(addr interop.Hash160, group string) { nSubjKey := namespaceSubjectKey(subject.Namespace, addr) storage.Put(ctx, nSubjKey, []byte{1}) - gKey := groupKey(subject.Namespace, group) + gKey := groupKey(subject.Namespace, groupID) data = storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } - gsKey := groupSubjectKey(subject.Namespace, group, addr) + gsKey := groupSubjectKey(subject.Namespace, groupID, addr) storage.Put(ctx, gsKey, []byte{1}) - runtime.Notify("AddSubjectToGroup", addr, subject.Namespace, group) + runtime.Notify("AddSubjectToGroup", addr, subject.Namespace, groupID) } -func RemoveSubjectFromGroup(addr interop.Hash160, ns, group string) { +func RemoveSubjectFromGroup(addr interop.Hash160, groupID int) { ctx := storage.GetContext() checkContractOwner(ctx) @@ -678,41 +768,50 @@ func RemoveSubjectFromGroup(addr interop.Hash160, ns, group string) { panic("subject not found") } - gKey := groupKey(ns, group) + subject := std.Deserialize(data).(Subject) + if subject.Namespace == "" { + panic("subject doesn't belong to any namespace") + } + + gKey := groupKey(subject.Namespace, groupID) data = storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } - gsKey := groupSubjectKey(ns, group, addr) + gsKey := groupSubjectKey(subject.Namespace, groupID, addr) storage.Delete(ctx, gsKey) - runtime.Notify("RemoveSubjectFromGroup", addr, ns, group) + runtime.Notify("RemoveSubjectFromGroup", addr, subject.Namespace, groupID) } -func ListGroupSubjects(ns, group string) iterator.Iterator { +func ListGroupSubjects(ns string, groupID int) iterator.Iterator { ctx := storage.GetReadOnlyContext() - return storage.Find(ctx, groupSubjectPrefix(ns, group), storage.KeysOnly|storage.RemovePrefix) + return storage.Find(ctx, groupSubjectPrefix(ns, groupID), storage.KeysOnly|storage.RemovePrefix) } -func DeleteGroup(ns, group string) { +func DeleteGroup(ns string, groupID int) { ctx := storage.GetContext() checkContractOwner(ctx) - gKey := groupKey(ns, group) + gKey := groupKey(ns, groupID) data := storage.Get(ctx, gKey).([]byte) if data == nil { panic("group not found") } + gr := std.Deserialize(data).(Group) + storage.Delete(ctx, gKey) - it := storage.Find(ctx, groupSubjectPrefix(ns, group), storage.KeysOnly) + it := storage.Find(ctx, groupSubjectPrefix(ns, groupID), storage.KeysOnly) for iterator.Next(it) { gsKey := iterator.Value(it).([]byte) storage.Delete(ctx, gsKey) } - runtime.Notify("DeleteGroup", ns, group) + deleteNamespaceGroupName(ctx, ns, gr.Name) + + runtime.Notify("DeleteGroup", ns, groupID) } func checkContractOwner(ctx storage.Context) { @@ -770,6 +869,42 @@ func updateNamespaceSubjectName(ctx storage.Context, subj Subject, oldName strin setNamespaceSubjectName(ctx, subj) } +func updateNamespaceGroupName(ctx storage.Context, gr Group, oldName string) { + if gr.Name == oldName { + return + } + + deleteNamespaceGroupName(ctx, gr.Namespace, oldName) + setNamespaceGroupName(ctx, gr) +} + +func deleteNamespaceGroupName(ctx storage.Context, ns, grName string) { + if grName == "" { + return + } + + nsGroupNameKey := namespaceGroupNameKey(ns, grName) + storage.Delete(ctx, nsGroupNameKey) +} + +func setNamespaceGroupName(ctx storage.Context, gr Group) { + if gr.Name == "" { + return + } + + nsGroupNameKey := namespaceGroupNameKey(gr.Namespace, gr.Name) + groupIDRaw := storage.Get(ctx, nsGroupNameKey).([]byte) + if groupIDRaw == nil { + storage.Put(ctx, nsGroupNameKey, std.Serialize(gr.ID)) + return + } + + groupID := std.Deserialize(groupIDRaw).(int) + if groupID != gr.ID { + panic("group name is not available in the current namespace") + } +} + func ownerKey(owner interop.Hash160) []byte { return append([]byte{ownerKeysPrefix}, owner...) } @@ -799,14 +934,14 @@ func namespaceKeyFromHash(ns []byte) []byte { return append([]byte{namespaceKeysPrefix}, ns...) } -func groupKey(ns, group string) []byte { +func groupKey(ns string, groupID int) []byte { prefix := groupPrefix(ns) - return append(prefix, ripemd160Hash(group)...) + return append(prefix, idToBytes(groupID)...) } -func groupKeyFromHashes(nsHash, groupHash []byte) []byte { +func groupKeyFromHashes(nsHash []byte, groupIDBytes []byte) []byte { prefix := groupPrefixFromHash(nsHash) - return append(prefix, groupHash...) + return append(prefix, groupIDBytes...) } func groupPrefix(ns string) []byte { @@ -837,8 +972,14 @@ func namespaceSubjectNameKey(ns, subjName string) []byte { return append([]byte{namespaceSubjectsNamesPrefix}, append(nsHash, nameHash...)...) } -func groupSubjectKey(ns, group string, addr interop.Hash160) []byte { - prefix := groupSubjectPrefix(ns, group) +func namespaceGroupNameKey(ns, groupName string) []byte { + nsHash := ripemd160Hash(ns) + nameHash := ripemd160Hash(groupName) + return append([]byte{namespaceGroupsNamesPrefix}, append(nsHash, nameHash...)...) +} + +func groupSubjectKey(ns string, groupID int, addr interop.Hash160) []byte { + prefix := groupSubjectPrefix(ns, groupID) return append(prefix, addr...) } @@ -847,12 +988,22 @@ func groupSubjectKeyFromHashes(nsHash, groupHash []byte, addr interop.Hash160) [ return append(prefix, addr...) } -func groupSubjectPrefix(ns, group string) []byte { +func groupSubjectPrefix(ns string, groupID int) []byte { nsHash := ripemd160Hash(ns) - gHash := ripemd160Hash(group) - return groupSubjectPrefixFromHashes(nsHash, gHash) + return groupSubjectPrefixFromHashes(nsHash, idToBytes(groupID)) } -func groupSubjectPrefixFromHashes(nsHash, groupHash []byte) []byte { - return append([]byte{groupSubjectsKeysPrefix}, append(nsHash, groupHash...)...) +func groupSubjectPrefixFromHashes(nsHash, groupIDBytes []byte) []byte { + return append([]byte{groupSubjectsKeysPrefix}, append(nsHash, groupIDBytes...)...) +} + +// idToBytes converts i64 value to BE bytes. Panics if value is bigger than i64. +func idToBytes(itemID int) []byte { + b := convert.ToBytes(itemID) + ln := len(b) + if ln > 8 { + panic("item ID exceeds 8 byte limit") + } + zeros := make([]byte, 8-ln) + return append(b, zeros...) } diff --git a/rpcclient/frostfsid/client.go b/rpcclient/frostfsid/client.go index 5367e76..99d04a2 100644 --- a/rpcclient/frostfsid/client.go +++ b/rpcclient/frostfsid/client.go @@ -81,24 +81,46 @@ type CreateGroupEvent struct { Group string } +// SetGroupNameEvent represents "SetGroupName" event emitted by the contract. +type SetGroupNameEvent struct { + Namespace string + GroupID *big.Int + Name string +} + +// SetGroupKVEvent represents "SetGroupKV" event emitted by the contract. +type SetGroupKVEvent struct { + Namespace string + GroupID *big.Int + Key string + Value string +} + +// DeleteGroupKVEvent represents "DeleteGroupKV" event emitted by the contract. +type DeleteGroupKVEvent struct { + Namespace string + GroupID *big.Int + Key string +} + // AddSubjectToGroupEvent represents "AddSubjectToGroup" event emitted by the contract. type AddSubjectToGroupEvent struct { SubjectAddress util.Uint160 Namespace string - Group string + GroupID *big.Int } // RemoveSubjectFromGroupEvent represents "RemoveSubjectFromGroup" event emitted by the contract. type RemoveSubjectFromGroupEvent struct { SubjectAddress util.Uint160 Namespace string - Group string + GroupID *big.Int } // DeleteGroupEvent represents "DeleteGroup" event emitted by the contract. type DeleteGroupEvent struct { Namespace string - Group string + GroupID *big.Int } // Invoker is used by ContractReader to call various safe methods. @@ -193,23 +215,23 @@ func (c *Contract) AddSubjectKeyUnsigned(addr util.Uint160, key *keys.PublicKey) // AddSubjectToGroup creates a transaction invoking `addSubjectToGroup` 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) AddSubjectToGroup(addr util.Uint160, group string) (util.Uint256, uint32, error) { - return c.actor.SendCall(c.hash, "addSubjectToGroup", addr, group) +func (c *Contract) AddSubjectToGroup(addr util.Uint160, groupID *big.Int) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "addSubjectToGroup", addr, groupID) } // AddSubjectToGroupTransaction creates a transaction invoking `addSubjectToGroup` method of the contract. // This transaction is signed, but not sent to the network, instead it's // returned to the caller. -func (c *Contract) AddSubjectToGroupTransaction(addr util.Uint160, group string) (*transaction.Transaction, error) { - return c.actor.MakeCall(c.hash, "addSubjectToGroup", addr, group) +func (c *Contract) AddSubjectToGroupTransaction(addr util.Uint160, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "addSubjectToGroup", addr, groupID) } // AddSubjectToGroupUnsigned creates a transaction invoking `addSubjectToGroup` 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) AddSubjectToGroupUnsigned(addr util.Uint160, group string) (*transaction.Transaction, error) { - return c.actor.MakeUnsignedCall(c.hash, "addSubjectToGroup", nil, addr, group) +func (c *Contract) AddSubjectToGroupUnsigned(addr util.Uint160, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "addSubjectToGroup", nil, addr, groupID) } // AddSubjectToNamespace creates a transaction invoking `addSubjectToNamespace` method of the contract. @@ -303,23 +325,45 @@ func (c *Contract) CreateSubjectUnsigned(key *keys.PublicKey) (*transaction.Tran // DeleteGroup creates a transaction invoking `deleteGroup` 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) DeleteGroup(ns string, group string) (util.Uint256, uint32, error) { - return c.actor.SendCall(c.hash, "deleteGroup", ns, group) +func (c *Contract) DeleteGroup(ns string, groupID *big.Int) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "deleteGroup", ns, groupID) } // DeleteGroupTransaction creates a transaction invoking `deleteGroup` method of the contract. // This transaction is signed, but not sent to the network, instead it's // returned to the caller. -func (c *Contract) DeleteGroupTransaction(ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeCall(c.hash, "deleteGroup", ns, group) +func (c *Contract) DeleteGroupTransaction(ns string, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "deleteGroup", ns, groupID) } // DeleteGroupUnsigned creates a transaction invoking `deleteGroup` 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) DeleteGroupUnsigned(ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeUnsignedCall(c.hash, "deleteGroup", nil, ns, group) +func (c *Contract) DeleteGroupUnsigned(ns string, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "deleteGroup", nil, ns, groupID) +} + +// DeleteGroupKV creates a transaction invoking `deleteGroupKV` 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) DeleteGroupKV(ns string, groupID *big.Int, key string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "deleteGroupKV", ns, groupID, key) +} + +// DeleteGroupKVTransaction creates a transaction invoking `deleteGroupKV` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) DeleteGroupKVTransaction(ns string, groupID *big.Int, key string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "deleteGroupKV", ns, groupID, key) +} + +// DeleteGroupKVUnsigned creates a transaction invoking `deleteGroupKV` 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) DeleteGroupKVUnsigned(ns string, groupID *big.Int, key string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "deleteGroupKV", nil, ns, groupID, key) } // DeleteOwner creates a transaction invoking `deleteOwner` method of the contract. @@ -391,45 +435,67 @@ func (c *Contract) DeleteSubjectKVUnsigned(addr util.Uint160, key string) (*tran // GetGroup creates a transaction invoking `getGroup` 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) GetGroup(ns string, group string) (util.Uint256, uint32, error) { - return c.actor.SendCall(c.hash, "getGroup", ns, group) +func (c *Contract) GetGroup(ns string, groupID *big.Int) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "getGroup", ns, groupID) } // GetGroupTransaction creates a transaction invoking `getGroup` method of the contract. // This transaction is signed, but not sent to the network, instead it's // returned to the caller. -func (c *Contract) GetGroupTransaction(ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeCall(c.hash, "getGroup", ns, group) +func (c *Contract) GetGroupTransaction(ns string, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "getGroup", ns, groupID) } // GetGroupUnsigned creates a transaction invoking `getGroup` 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) GetGroupUnsigned(ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeUnsignedCall(c.hash, "getGroup", nil, ns, group) +func (c *Contract) GetGroupUnsigned(ns string, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "getGroup", nil, ns, groupID) } // GetGroupExtended creates a transaction invoking `getGroupExtended` 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) GetGroupExtended(ns string, group string) (util.Uint256, uint32, error) { - return c.actor.SendCall(c.hash, "getGroupExtended", ns, group) +func (c *Contract) GetGroupExtended(ns string, groupID *big.Int) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "getGroupExtended", ns, groupID) } // GetGroupExtendedTransaction creates a transaction invoking `getGroupExtended` method of the contract. // This transaction is signed, but not sent to the network, instead it's // returned to the caller. -func (c *Contract) GetGroupExtendedTransaction(ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeCall(c.hash, "getGroupExtended", ns, group) +func (c *Contract) GetGroupExtendedTransaction(ns string, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "getGroupExtended", ns, groupID) } // GetGroupExtendedUnsigned creates a transaction invoking `getGroupExtended` 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) GetGroupExtendedUnsigned(ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeUnsignedCall(c.hash, "getGroupExtended", nil, ns, group) +func (c *Contract) GetGroupExtendedUnsigned(ns string, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "getGroupExtended", nil, ns, groupID) +} + +// GetGroupIDByName creates a transaction invoking `getGroupIDByName` 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) GetGroupIDByName(ns string, name string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "getGroupIDByName", ns, name) +} + +// GetGroupIDByNameTransaction creates a transaction invoking `getGroupIDByName` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) GetGroupIDByNameTransaction(ns string, name string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "getGroupIDByName", ns, name) +} + +// GetGroupIDByNameUnsigned creates a transaction invoking `getGroupIDByName` 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) GetGroupIDByNameUnsigned(ns string, name string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "getGroupIDByName", nil, ns, name) } // GetNamespace creates a transaction invoking `getNamespace` method of the contract. @@ -567,23 +633,23 @@ func (c *Contract) GetSubjectKeyByNameUnsigned(ns string, name string) (*transac // ListGroupSubjects creates a transaction invoking `listGroupSubjects` 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) ListGroupSubjects(ns string, group string) (util.Uint256, uint32, error) { - return c.actor.SendCall(c.hash, "listGroupSubjects", ns, group) +func (c *Contract) ListGroupSubjects(ns string, groupID *big.Int) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "listGroupSubjects", ns, groupID) } // ListGroupSubjectsTransaction creates a transaction invoking `listGroupSubjects` method of the contract. // This transaction is signed, but not sent to the network, instead it's // returned to the caller. -func (c *Contract) ListGroupSubjectsTransaction(ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeCall(c.hash, "listGroupSubjects", ns, group) +func (c *Contract) ListGroupSubjectsTransaction(ns string, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "listGroupSubjects", ns, groupID) } // ListGroupSubjectsUnsigned creates a transaction invoking `listGroupSubjects` 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) ListGroupSubjectsUnsigned(ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeUnsignedCall(c.hash, "listGroupSubjects", nil, ns, group) +func (c *Contract) ListGroupSubjectsUnsigned(ns string, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "listGroupSubjects", nil, ns, groupID) } // ListGroups creates a transaction invoking `listGroups` method of the contract. @@ -699,23 +765,23 @@ func (c *Contract) ListSubjectsUnsigned() (*transaction.Transaction, error) { // RemoveSubjectFromGroup creates a transaction invoking `removeSubjectFromGroup` 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) RemoveSubjectFromGroup(addr util.Uint160, ns string, group string) (util.Uint256, uint32, error) { - return c.actor.SendCall(c.hash, "removeSubjectFromGroup", addr, ns, group) +func (c *Contract) RemoveSubjectFromGroup(addr util.Uint160, groupID *big.Int) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "removeSubjectFromGroup", addr, groupID) } // RemoveSubjectFromGroupTransaction creates a transaction invoking `removeSubjectFromGroup` method of the contract. // This transaction is signed, but not sent to the network, instead it's // returned to the caller. -func (c *Contract) RemoveSubjectFromGroupTransaction(addr util.Uint160, ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeCall(c.hash, "removeSubjectFromGroup", addr, ns, group) +func (c *Contract) RemoveSubjectFromGroupTransaction(addr util.Uint160, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "removeSubjectFromGroup", addr, groupID) } // RemoveSubjectFromGroupUnsigned creates a transaction invoking `removeSubjectFromGroup` 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) RemoveSubjectFromGroupUnsigned(addr util.Uint160, ns string, group string) (*transaction.Transaction, error) { - return c.actor.MakeUnsignedCall(c.hash, "removeSubjectFromGroup", nil, addr, ns, group) +func (c *Contract) RemoveSubjectFromGroupUnsigned(addr util.Uint160, groupID *big.Int) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "removeSubjectFromGroup", nil, addr, groupID) } // RemoveSubjectFromNamespace creates a transaction invoking `removeSubjectFromNamespace` method of the contract. @@ -762,6 +828,50 @@ func (c *Contract) RemoveSubjectKeyUnsigned(addr util.Uint160, key *keys.PublicK return c.actor.MakeUnsignedCall(c.hash, "removeSubjectKey", nil, addr, key) } +// SetGroupKV creates a transaction invoking `setGroupKV` 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) SetGroupKV(ns string, groupID *big.Int, key string, val string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "setGroupKV", ns, groupID, key, val) +} + +// SetGroupKVTransaction creates a transaction invoking `setGroupKV` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) SetGroupKVTransaction(ns string, groupID *big.Int, key string, val string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "setGroupKV", ns, groupID, key, val) +} + +// SetGroupKVUnsigned creates a transaction invoking `setGroupKV` 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) SetGroupKVUnsigned(ns string, groupID *big.Int, key string, val string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "setGroupKV", nil, ns, groupID, key, val) +} + +// SetGroupName creates a transaction invoking `setGroupName` 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) SetGroupName(ns string, groupID *big.Int, name string) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "setGroupName", ns, groupID, name) +} + +// SetGroupNameTransaction creates a transaction invoking `setGroupName` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) SetGroupNameTransaction(ns string, groupID *big.Int, name string) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "setGroupName", ns, groupID, name) +} + +// SetGroupNameUnsigned creates a transaction invoking `setGroupName` 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) SetGroupNameUnsigned(ns string, groupID *big.Int, name string) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "setGroupName", nil, ns, groupID, name) +} + // SetSubjectKV creates a transaction invoking `setSubjectKV` 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. @@ -1645,6 +1755,267 @@ func (e *CreateGroupEvent) FromStackItem(item *stackitem.Array) error { return nil } +// SetGroupNameEventsFromApplicationLog retrieves a set of all emitted events +// with "SetGroupName" name from the provided [result.ApplicationLog]. +func SetGroupNameEventsFromApplicationLog(log *result.ApplicationLog) ([]*SetGroupNameEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*SetGroupNameEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "SetGroupName" { + continue + } + event := new(SetGroupNameEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize SetGroupNameEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to SetGroupNameEvent or +// returns an error if it's not possible to do to so. +func (e *SetGroupNameEvent) 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) != 3 { + 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) + } + + index++ + e.GroupID, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field GroupID: %w", err) + } + + index++ + e.Name, 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 Name: %w", err) + } + + return nil +} + +// SetGroupKVEventsFromApplicationLog retrieves a set of all emitted events +// with "SetGroupKV" name from the provided [result.ApplicationLog]. +func SetGroupKVEventsFromApplicationLog(log *result.ApplicationLog) ([]*SetGroupKVEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*SetGroupKVEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "SetGroupKV" { + continue + } + event := new(SetGroupKVEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize SetGroupKVEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to SetGroupKVEvent or +// returns an error if it's not possible to do to so. +func (e *SetGroupKVEvent) 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) != 4 { + 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) + } + + index++ + e.GroupID, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field GroupID: %w", err) + } + + index++ + e.Key, 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 Key: %w", err) + } + + index++ + e.Value, 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 Value: %w", err) + } + + return nil +} + +// DeleteGroupKVEventsFromApplicationLog retrieves a set of all emitted events +// with "DeleteGroupKV" name from the provided [result.ApplicationLog]. +func DeleteGroupKVEventsFromApplicationLog(log *result.ApplicationLog) ([]*DeleteGroupKVEvent, error) { + if log == nil { + return nil, errors.New("nil application log") + } + + var res []*DeleteGroupKVEvent + for i, ex := range log.Executions { + for j, e := range ex.Events { + if e.Name != "DeleteGroupKV" { + continue + } + event := new(DeleteGroupKVEvent) + err := event.FromStackItem(e.Item) + if err != nil { + return nil, fmt.Errorf("failed to deserialize DeleteGroupKVEvent from stackitem (execution #%d, event #%d): %w", i, j, err) + } + res = append(res, event) + } + } + + return res, nil +} + +// FromStackItem converts provided [stackitem.Array] to DeleteGroupKVEvent or +// returns an error if it's not possible to do to so. +func (e *DeleteGroupKVEvent) 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) != 3 { + 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) + } + + index++ + e.GroupID, err = arr[index].TryInteger() + if err != nil { + return fmt.Errorf("field GroupID: %w", err) + } + + index++ + e.Key, 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 Key: %w", err) + } + + return nil +} + // AddSubjectToGroupEventsFromApplicationLog retrieves a set of all emitted events // with "AddSubjectToGroup" name from the provided [result.ApplicationLog]. func AddSubjectToGroupEventsFromApplicationLog(log *result.ApplicationLog) ([]*AddSubjectToGroupEvent, error) { @@ -1720,18 +2091,9 @@ func (e *AddSubjectToGroupEvent) FromStackItem(item *stackitem.Array) error { } index++ - e.Group, 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]) + e.GroupID, err = arr[index].TryInteger() if err != nil { - return fmt.Errorf("field Group: %w", err) + return fmt.Errorf("field GroupID: %w", err) } return nil @@ -1812,18 +2174,9 @@ func (e *RemoveSubjectFromGroupEvent) FromStackItem(item *stackitem.Array) error } index++ - e.Group, 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]) + e.GroupID, err = arr[index].TryInteger() if err != nil { - return fmt.Errorf("field Group: %w", err) + return fmt.Errorf("field GroupID: %w", err) } return nil @@ -1888,18 +2241,9 @@ func (e *DeleteGroupEvent) FromStackItem(item *stackitem.Array) error { } index++ - e.Group, 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]) + e.GroupID, err = arr[index].TryInteger() if err != nil { - return fmt.Errorf("field Group: %w", err) + return fmt.Errorf("field GroupID: %w", err) } return nil diff --git a/tests/frostfsid_client_test.go b/tests/frostfsid_client_test.go index 1122545..d6bd9ed 100644 --- a/tests/frostfsid_client_test.go +++ b/tests/frostfsid_client_test.go @@ -114,17 +114,17 @@ func TestFrostFSID_Client_SubjectManagement(t *testing.T) { ffsid.a.await(ffsid.cli.CreateSubject(subjKey.PublicKey())) ffsid.a.await(ffsid.cli.SetSubjectName(subjAddr, subjLogin)) - ffsid.a.await(ffsid.cli.SetSubjectKV(subjAddr, client.SubjectIAMPathKey, iamPathKV)) + ffsid.a.await(ffsid.cli.SetSubjectKV(subjAddr, client.IAMPathKey, iamPathKV)) ffsid.a.await(ffsid.cli.AddSubjectKey(subjAddr, extraKey.PublicKey())) subj, err := ffsid.cli.GetSubject(subjAddr) require.NoError(t, err) require.True(t, subjKey.PublicKey().Equal(subj.PrimaryKey)) require.Equal(t, subj.Name, subjLogin) - require.Equal(t, map[string]string{client.SubjectIAMPathKey: iamPathKV}, subj.KV) + require.Equal(t, map[string]string{client.IAMPathKey: iamPathKV}, subj.KV) require.ElementsMatch(t, []*keys.PublicKey{extraKey.PublicKey()}, subj.AdditionalKeys) - ffsid.a.await(ffsid.cli.DeleteSubjectKV(subjAddr, client.SubjectIAMPathKey)) + ffsid.a.await(ffsid.cli.DeleteSubjectKV(subjAddr, client.IAMPathKey)) subj, err = ffsid.cli.GetSubject(subjAddr) require.NoError(t, err) require.Empty(t, subj.KV) @@ -204,43 +204,59 @@ func TestFrostFSID_Client_GroupManagement(t *testing.T) { ffsid.a.await(ffsid.cli.CreateNamespace(namespace)) groupName := "group" - ffsid.a.await(ffsid.cli.CreateGroup(namespace, groupName)) - _, _, err := ffsid.cli.CreateGroup(namespace, groupName) - require.ErrorContains(t, err, "already exists") + groupID := int64(1) + actGroupID, err := ffsid.cli.ParseGroupID(ffsid.cli.Wait(ffsid.cli.CreateGroup(namespace, groupName))) + require.NoError(t, err) + require.Equal(t, groupID, actGroupID) - group, err := ffsid.cli.GetGroup(namespace, groupName) + _, _, err = ffsid.cli.CreateGroup(namespace, groupName) + require.ErrorContains(t, err, "is not available") + + iamARN := "arn" + ffsid.a.await(ffsid.cli.SetGroupKV(namespace, groupID, client.IAMARNKey, iamARN)) + + group, err := ffsid.cli.GetGroup(namespace, groupID) require.NoError(t, err) require.Equal(t, groupName, group.Name) + require.Equal(t, map[string]string{client.IAMARNKey: iamARN}, group.KV) - groupExt, err := ffsid.cli.GetGroupExtended(namespace, groupName) + ffsid.a.await(ffsid.cli.DeleteGroupKV(namespace, groupID, client.IAMARNKey)) + + groupExt, err := ffsid.cli.GetGroupExtended(namespace, groupID) require.NoError(t, err) require.Zero(t, groupExt.SubjectsCount) ffsid.a.await(ffsid.cli.AddSubjectToNamespace(subjAddr, namespace)) - ffsid.a.await(ffsid.cli.AddSubjectToGroup(subjAddr, groupName)) + ffsid.a.await(ffsid.cli.AddSubjectToGroup(subjAddr, groupID)) subjExt, err := ffsid.cli.GetSubjectExtended(subjAddr) require.NoError(t, err) - require.ElementsMatch(t, []*client.Group{{Name: groupName, Namespace: namespace}}, subjExt.Groups) + require.ElementsMatch(t, []*client.Group{{ID: groupID, Name: groupName, Namespace: namespace, KV: map[string]string{}}}, subjExt.Groups) - groupExt, err = ffsid.cli.GetGroupExtended(namespace, groupName) + groupExt, err = ffsid.cli.GetGroupExtended(namespace, groupID) require.NoError(t, err) require.EqualValues(t, 1, groupExt.SubjectsCount) - subjects, err := ffsid.cli.ListGroupSubjects(namespace, groupName) + subjects, err := ffsid.cli.ListGroupSubjects(namespace, groupID) require.NoError(t, err) require.ElementsMatch(t, []util.Uint160{subjAddr}, subjects) + ffsid.a.await(ffsid.cli.RemoveSubjectFromGroup(subjAddr, groupID)) + + subjects, err = ffsid.cli.ListGroupSubjects(namespace, groupID) + require.NoError(t, err) + require.Empty(t, subjects) + subjects, err = ffsid.cli.ListNamespaceSubjects(namespace) require.NoError(t, err) require.ElementsMatch(t, []util.Uint160{subjAddr}, subjects) groups, err := ffsid.cli.ListGroups(namespace) require.NoError(t, err) - require.ElementsMatch(t, []*client.Group{{Name: groupName, Namespace: namespace}}, groups) + require.ElementsMatch(t, []*client.Group{{ID: groupID, Name: groupName, Namespace: namespace, KV: map[string]string{}}}, groups) - ffsid.a.await(ffsid.cli.DeleteGroup(namespace, groupName)) - _, err = ffsid.cli.GetGroup(namespace, groupName) + ffsid.a.await(ffsid.cli.DeleteGroup(namespace, groupID)) + _, err = ffsid.cli.GetGroup(namespace, groupID) require.ErrorContains(t, err, "not found") groups, err = ffsid.cli.ListGroups(namespace) @@ -277,33 +293,47 @@ func TestFrostFSID_Client_Lists(t *testing.T) { groups := []client.Group{ { + ID: 1, Name: "empty-group0", Namespace: namespaces[0], }, { + ID: 2, Name: "empty-group1", Namespace: namespaces[1], }, { + ID: 3, Name: "group2", Namespace: namespaces[1], }, { + ID: 4, Name: "group3", Namespace: namespaces[2], }, { + ID: 5, Name: "group4", Namespace: namespaces[3], }, { + ID: 6, Name: "group5", Namespace: namespaces[3], }, } tx = ffsid.cli.StartTx() - for _, group := range groups { + for _, group := range groups[:3] { + err := tx.WrapCall(ffsid.cli.CreateGroupCall(group.Namespace, group.Name)) + require.NoError(t, err) + } + ffsid.a.await(ffsid.cli.SendTx(tx)) + + // we split into two tx because of gas limit exceeded error + tx = ffsid.cli.StartTx() + for _, group := range groups[3:] { err := tx.WrapCall(ffsid.cli.CreateGroupCall(group.Namespace, group.Name)) require.NoError(t, err) } @@ -366,12 +396,12 @@ func addSubjectsToGroups(t *testing.T, ffsid *testFrostFSIDClientInvoker, groups cli := ffsid.cli tx := cli.StartTx() - err := tx.WrapCall(cli.AddSubjectToGroupCall(subjects[2].addr, groups[2].Name)) + err := tx.WrapCall(cli.AddSubjectToGroupCall(subjects[2].addr, groups[2].ID)) require.NoError(t, err) - err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[3].addr, groups[3].Name)) + err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[3].addr, groups[3].ID)) require.NoError(t, err) - err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[4].addr, groups[3].Name)) + err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[4].addr, groups[3].ID)) require.NoError(t, err) ffsid.a.await(cli.SendTx(tx)) @@ -379,13 +409,13 @@ func addSubjectsToGroups(t *testing.T, ffsid *testFrostFSIDClientInvoker, groups // we have to start new tx because of insufficient gas / gas limit exceeded error tx = cli.StartTx() - err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[6].addr, groups[4].Name)) + err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[6].addr, groups[4].ID)) require.NoError(t, err) - err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[7].addr, groups[4].Name)) + err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[7].addr, groups[4].ID)) require.NoError(t, err) - err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[8].addr, groups[5].Name)) + err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[8].addr, groups[5].ID)) require.NoError(t, err) - err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[9].addr, groups[5].Name)) + err = tx.WrapCall(cli.AddSubjectToGroupCall(subjects[9].addr, groups[5].ID)) require.NoError(t, err) ffsid.a.await(cli.SendTx(tx)) @@ -398,7 +428,7 @@ func checkNamespaceSubjects(t *testing.T, cli *client.Client, ns string, subject } func checkGroupSubjects(t *testing.T, cli *client.Client, group client.Group, subjects []testSubject, start, end int) { - groupSubjects, err := cli.ListGroupSubjects(group.Namespace, group.Name) + groupSubjects, err := cli.ListGroupSubjects(group.Namespace, group.ID) require.NoError(t, err) require.ElementsMatch(t, subjSlice(subjects, start, end), groupSubjects) } @@ -448,6 +478,7 @@ func TestFrostFSID_Client_UseCaseListNSSubjects(t *testing.T) { namespace := "namespace" group := "group" + groupID := int64(1) tx := ffsid.cli.StartTx() err := tx.WrapCall(ffsid.cli.CreateNamespaceCall(namespace)) @@ -471,7 +502,7 @@ func TestFrostFSID_Client_UseCaseListNSSubjects(t *testing.T) { require.NoError(t, err) if i > len(subjects)/2 { - err = tx.WrapCall(ffsid.cli.AddSubjectToGroupCall(subjects[i].addr, group)) + err = tx.WrapCall(ffsid.cli.AddSubjectToGroupCall(subjects[i].addr, groupID)) require.NoError(t, err) } ffsid.a.await(ffsid.cli.SendTx(tx)) diff --git a/tests/frostfsid_test.go b/tests/frostfsid_test.go index 2ae6099..92bbb4a 100644 --- a/tests/frostfsid_test.go +++ b/tests/frostfsid_test.go @@ -50,6 +50,10 @@ const ( createGroupMethod = "createGroup" getGroupMethod = "getGroup" getGroupExtendedMethod = "getGroupExtended" + getGroupIDByNameMethod = "getGroupIDByName" + setGroupNameMethod = "setGroupName" + setGroupKVMethod = "setGroupKV" + deleteGroupKVMethod = "deleteGroupKV" listGroupsMethod = "listGroups" addSubjectToGroupMethod = "addSubjectToGroup" removeSubjectFromGroupMethod = "removeSubjectFromGroup" @@ -220,16 +224,16 @@ func TestFrostFSID_SubjectManagement(t *testing.T) { t.Run("set subject KVs", func(t *testing.T) { iamPath := "iam/path" - anonInvoker.InvokeFail(t, notWitnessedError, setSubjectKVMethod, subjKeyAddr, client.SubjectIAMPathKey, iamPath) - invoker.Invoke(t, stackitem.Null{}, setSubjectKVMethod, subjKeyAddr, client.SubjectIAMPathKey, iamPath) + anonInvoker.InvokeFail(t, notWitnessedError, setSubjectKVMethod, subjKeyAddr, client.IAMPathKey, iamPath) + invoker.Invoke(t, stackitem.Null{}, setSubjectKVMethod, subjKeyAddr, client.IAMPathKey, iamPath) s, err = anonInvoker.TestInvoke(t, getSubjectMethod, subjKeyAddr) require.NoError(t, err) subj = parseSubject(t, s) - require.Equal(t, iamPath, subj.KV[client.SubjectIAMPathKey]) + require.Equal(t, iamPath, subj.KV[client.IAMPathKey]) - anonInvoker.InvokeFail(t, notWitnessedError, deleteSubjectKVMethod, subjKeyAddr, client.SubjectIAMPathKey) - invoker.Invoke(t, stackitem.Null{}, deleteSubjectKVMethod, subjKeyAddr, client.SubjectIAMPathKey) + anonInvoker.InvokeFail(t, notWitnessedError, deleteSubjectKVMethod, subjKeyAddr, client.IAMPathKey) + invoker.Invoke(t, stackitem.Null{}, deleteSubjectKVMethod, subjKeyAddr, client.IAMPathKey) s, err = anonInvoker.TestInvoke(t, getSubjectMethod, subjKeyAddr) require.NoError(t, err) @@ -324,6 +328,55 @@ func TestFrostFSIS_SubjectNameRelatedInvariants(t *testing.T) { checkPublicKeyResult(t, s, err, nil) } +func TestFrostFSIS_GroupNameRelatedInvariants(t *testing.T) { + f := newFrostFSIDInvoker(t) + + groupName1, groupName2 := "group1", "group2" + groupID1, groupID2 := int64(1), int64(2) + + invoker := f.OwnerInvoker() + + ns1, ns2 := "ns1", "ns2" + + // Create two group + // Create two namespace. + // Add these groups to ns1 + invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, ns1) + invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, ns2) + invoker.Invoke(t, stackitem.Make(groupID1), createGroupMethod, ns1, groupName1) + invoker.Invoke(t, stackitem.Make(groupID2), createGroupMethod, ns1, groupName2) + + // Check that we can find group id by name for group1 in ns1 and not in ns2 + s, err := invoker.TestInvoke(t, getGroupIDByNameMethod, ns1, groupName1) + checkGroupIDResult(t, s, err, groupID1) + s, err = invoker.TestInvoke(t, getGroupIDByNameMethod, ns2, groupName1) + checkGroupIDResult(t, s, err, -1) + + // Check that we cannot set for second group name that the first subject has already taken + invoker.InvokeFail(t, "not available", setGroupNameMethod, ns1, groupID1, groupName2) + + // Check that we cannot create group with the same name in namespace, but can in another + invoker.InvokeFail(t, "not available", createGroupMethod, ns1, groupName2) + invoker.Invoke(t, stackitem.Make(3), createGroupMethod, ns2, groupName2) + + + // Check that we cannot find group id by name for group that was removed + invoker.Invoke(t, stackitem.Null{}, deleteGroupMethod, ns1, groupID2) + s, err = invoker.TestInvoke(t, getGroupIDByNameMethod, ns1, groupName2) + checkGroupIDResult(t, s, err, -1) + + // Check that we can create group with the name that was freed after deleting + invoker.Invoke(t, stackitem.Make(4), createGroupMethod, ns1, groupName2) + + // Check that after group renaming its id cannot be found by old name + newGroupName:= "new" + invoker.Invoke(t, stackitem.Null{}, setGroupNameMethod, ns1, groupID1, newGroupName) + s, err = invoker.TestInvoke(t, getGroupIDByNameMethod, ns1, groupName1) + checkGroupIDResult(t, s, err, -1) + s, err = invoker.TestInvoke(t, getGroupIDByNameMethod, ns1, newGroupName) + checkGroupIDResult(t, s, err, groupID1) +} + func TestFrostFSID_NamespaceManagement(t *testing.T) { f := newFrostFSIDInvoker(t) @@ -428,19 +481,21 @@ func TestFrostFSID_GroupManagement(t *testing.T) { nsName := "namespace" invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, nsName) + groupID := int64(1) groupName := "group" anonInvoker.InvokeFail(t, notWitnessedError, createGroupMethod, nsName, groupName) - invoker.Invoke(t, stackitem.Null{}, createGroupMethod, nsName, groupName) + invoker.Invoke(t, stackitem.Make(groupID), createGroupMethod, nsName, groupName) - s, err := anonInvoker.TestInvoke(t, getGroupMethod, nsName, groupName) + s, err := anonInvoker.TestInvoke(t, getGroupMethod, nsName, groupID) require.NoError(t, err) group := parseGroup(t, s.Pop().Item()) + require.Equal(t, groupID, group.ID) require.Equal(t, groupName, group.Name) s, err = anonInvoker.TestInvoke(t, listGroupsMethod, nsName) require.NoError(t, err) groups := parseGroups(t, readIteratorAll(s)) - require.ElementsMatch(t, groups, []Group{{Name: groupName, Namespace: nsName}}) + require.ElementsMatch(t, groups, []Group{{ID: groupID, Name: groupName, Namespace: nsName}}) t.Run("add subjects to group", func(t *testing.T) { subjKey, err := keys.NewPrivateKey() @@ -449,21 +504,21 @@ func TestFrostFSID_GroupManagement(t *testing.T) { subjAddress := subjKey.PublicKey().GetScriptHash() invoker.Invoke(t, stackitem.Null{}, addSubjectToNamespaceMethod, subjAddress, nsName) - anonInvoker.InvokeFail(t, "not witnessed", addSubjectToGroupMethod, subjAddress, groupName) - invoker.Invoke(t, stackitem.Null{}, addSubjectToGroupMethod, subjAddress, groupName) + anonInvoker.InvokeFail(t, "not witnessed", addSubjectToGroupMethod, subjAddress, groupID) + invoker.Invoke(t, stackitem.Null{}, addSubjectToGroupMethod, subjAddress, groupID) t.Run("list group subjects", func(t *testing.T) { - s, err = anonInvoker.TestInvoke(t, listGroupSubjectsMethod, nsName, groupName) + s, err = anonInvoker.TestInvoke(t, listGroupSubjectsMethod, nsName, groupID) require.NoError(t, err) addresses, err := unwrap.ArrayOfUint160(makeValidRes(stackitem.NewArray(readIteratorAll(s))), nil) require.NoError(t, err) require.ElementsMatch(t, addresses, []util.Uint160{subjAddress}) - anonInvoker.InvokeFail(t, "not witnessed", removeSubjectFromGroupMethod, subjAddress, nsName, groupName) - invoker.Invoke(t, stackitem.Null{}, removeSubjectFromGroupMethod, subjAddress, nsName, groupName) + anonInvoker.InvokeFail(t, "not witnessed", removeSubjectFromGroupMethod, subjAddress, groupID) + invoker.Invoke(t, stackitem.Null{}, removeSubjectFromGroupMethod, subjAddress, groupID) - s, err = anonInvoker.TestInvoke(t, listGroupSubjectsMethod, nsName, groupName) + s, err = anonInvoker.TestInvoke(t, listGroupSubjectsMethod, nsName, groupID) require.NoError(t, err) addresses, err = unwrap.ArrayOfUint160(makeValidRes(stackitem.NewArray(readIteratorAll(s))), nil) @@ -477,23 +532,61 @@ func TestFrostFSID_GroupManagement(t *testing.T) { require.NoError(t, err) invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, subjKey.PublicKey().Bytes()) invoker.Invoke(t, stackitem.Null{}, addSubjectToNamespaceMethod, subjKey.PublicKey().GetScriptHash(), nsName) - invoker.Invoke(t, stackitem.Null{}, addSubjectToGroupMethod, subjKey.PublicKey().GetScriptHash(), groupName) + invoker.Invoke(t, stackitem.Null{}, addSubjectToGroupMethod, subjKey.PublicKey().GetScriptHash(), groupID) } - s, err = anonInvoker.TestInvoke(t, getGroupExtendedMethod, nsName, groupName) + s, err = anonInvoker.TestInvoke(t, getGroupExtendedMethod, nsName, groupID) require.NoError(t, err) groupExt := parseGroupExtended(t, s.Pop().Item()) + require.Equal(t, groupID, groupExt.ID) require.Equal(t, groupName, groupExt.Name) + require.Empty(t, groupExt.KV) require.EqualValues(t, subjectsCount, groupExt.SubjectsCount) }) }) }) - t.Run("delete group", func(t *testing.T) { - anonInvoker.InvokeFail(t, notWitnessedError, deleteGroupMethod, nsName, groupName) - invoker.Invoke(t, stackitem.Null{}, deleteGroupMethod, nsName, groupName) + t.Run("set group name", func(t *testing.T) { + newGroupName := "new-name" - anonInvoker.InvokeFail(t, "group not found", getGroupMethod, nsName, groupName) + anonInvoker.InvokeFail(t, notWitnessedError, setGroupNameMethod, nsName, groupID, newGroupName) + invoker.Invoke(t, stackitem.Null{}, setGroupNameMethod, nsName, groupID, newGroupName) + + s, err = anonInvoker.TestInvoke(t, getGroupIDByNameMethod, nsName, newGroupName) + require.NoError(t, err) + require.Equal(t, groupID, s.Pop().BigInt().Int64()) + + s, err = anonInvoker.TestInvoke(t, getGroupMethod, nsName, groupID) + require.NoError(t, err) + group = parseGroup(t, s.Pop().Item()) + require.Equal(t, newGroupName, group.Name) + }) + + t.Run("set group KVs", func(t *testing.T) { + iamARN := "arn" + + anonInvoker.InvokeFail(t, notWitnessedError, setGroupKVMethod, nsName, groupID, client.IAMARNKey, iamARN) + invoker.Invoke(t, stackitem.Null{}, setGroupKVMethod, nsName, groupID, client.IAMARNKey, iamARN) + + s, err = anonInvoker.TestInvoke(t, getGroupMethod, nsName, groupID) + require.NoError(t, err) + group = parseGroup(t, s.Pop().Item()) + require.Equal(t, iamARN, group.KV[client.IAMARNKey]) + + anonInvoker.InvokeFail(t, notWitnessedError, deleteGroupKVMethod, nsName, groupID, client.IAMARNKey) + invoker.Invoke(t, stackitem.Null{}, deleteGroupKVMethod, nsName, groupID, client.IAMARNKey) + + s, err = anonInvoker.TestInvoke(t, getGroupMethod, nsName, groupID) + require.NoError(t, err) + group = parseGroup(t, s.Pop().Item()) + require.Empty(t, group.KV) + }) + + t.Run("delete group", func(t *testing.T) { + anonInvoker.InvokeFail(t, notWitnessedError, deleteGroupMethod, nsName, groupID) + invoker.Invoke(t, stackitem.Null{}, deleteGroupMethod, nsName, groupID) + + anonInvoker.InvokeFail(t, "group not found", getGroupMethod, nsName, groupID) s, err = anonInvoker.TestInvoke(t, listGroupsMethod, nsName) require.NoError(t, err) @@ -514,6 +607,18 @@ func checkPublicKeyResult(t *testing.T, s *vm.Stack, err error, key *keys.Privat require.Equal(t, key.PublicKey(), foundKey) } +func checkGroupIDResult(t *testing.T, s *vm.Stack, err error, groupID int64) { + if groupID == -1 { + require.ErrorContains(t, err, "not found") + return + } + + require.NoError(t, err) + foundGroupID, err := unwrap.Int64(makeValidRes(s.Pop().Item()), nil) + require.NoError(t, err) + require.Equal(t, groupID, foundGroupID) +} + func readIteratorAll(s *vm.Stack) []stackitem.Item { iter := s.Pop().Value().(*storage.Iterator) @@ -649,13 +754,17 @@ func parseNamespaces(t *testing.T, items []stackitem.Item) []Namespace { } type Group struct { + ID int64 Name string Namespace string + KV map[string]string } type GroupExtended struct { + ID int64 Name string Namespace string + KV map[string]string SubjectsCount int64 } @@ -663,16 +772,23 @@ func parseGroup(t *testing.T, item stackitem.Item) Group { var group Group subjStruct := item.Value().([]stackitem.Item) - require.Len(t, subjStruct, 2) + require.Len(t, subjStruct, 4) - nameBytes, err := subjStruct[0].TryBytes() + groupID, err := subjStruct[0].TryInteger() + require.NoError(t, err) + group.ID = groupID.Int64() + + nameBytes, err := subjStruct[1].TryBytes() require.NoError(t, err) group.Name = string(nameBytes) - namespaceBytes, err := subjStruct[1].TryBytes() + namespaceBytes, err := subjStruct[2].TryBytes() require.NoError(t, err) group.Namespace = string(namespaceBytes) + group.KV, err = parseMap(subjStruct[3]) + require.NoError(t, err) + return group } @@ -680,17 +796,24 @@ func parseGroupExtended(t *testing.T, item stackitem.Item) GroupExtended { var gr GroupExtended subjStruct := item.Value().([]stackitem.Item) - require.Len(t, subjStruct, 3) + require.Len(t, subjStruct, 5) - nameBytes, err := subjStruct[0].TryBytes() + groupID, err := subjStruct[0].TryInteger() + require.NoError(t, err) + gr.ID = groupID.Int64() + + nameBytes, err := subjStruct[1].TryBytes() require.NoError(t, err) gr.Name = string(nameBytes) - namespaceBytes, err := subjStruct[1].TryBytes() + namespaceBytes, err := subjStruct[2].TryBytes() require.NoError(t, err) gr.Namespace = string(namespaceBytes) - subjectsCountInt, err := subjStruct[2].TryInteger() + gr.KV, err = parseMap(subjStruct[3]) + require.NoError(t, err) + + subjectsCountInt, err := subjStruct[4].TryInteger() require.NoError(t, err) gr.SubjectsCount = subjectsCountInt.Int64()