diff --git a/cmd/frostfs-adm/internal/modules/morph/frostfsid.go b/cmd/frostfs-adm/internal/modules/morph/frostfsid.go new file mode 100644 index 000000000..2a6043b7c --- /dev/null +++ b/cmd/frostfs-adm/internal/modules/morph/frostfsid.go @@ -0,0 +1,460 @@ +package morph + +import ( + "errors" + "fmt" + "sort" + + frostfsidclient "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" + commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + namespaceFlag = "namespace" + subjectNameFlag = "subject-name" + subjectKeyFlag = "subject-key" + subjectAddressFlag = "subject-address" + includeNamesFlag = "include-names" + groupNameFlag = "group-name" + groupIDFlag = "group-id" +) + +const rootNamespacePlaceholder = "" + +var ( + frostfsidCmd = &cobra.Command{ + Use: "frostfsid", + Short: "Section for frostfsid interactions commands", + } + + frostfsidCreateNamespaceCmd = &cobra.Command{ + Use: "create-namespace", + Short: "Create new namespace in frostfsid contract", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidCreateNamespace, + } + + frostfsidListNamespacesCmd = &cobra.Command{ + Use: "list-namespaces", + Short: "List all namespaces in frostfsid", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidListNamespaces, + } + + frostfsidCreateSubjectCmd = &cobra.Command{ + Use: "create-subject", + Short: "Create subject in frostfsid contract", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidCreateSubject, + } + + frostfsidDeleteSubjectCmd = &cobra.Command{ + Use: "delete-subject", + Short: "Delete subject from frostfsid contract", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidDeleteSubject, + } + + frostfsidListSubjectsCmd = &cobra.Command{ + Use: "list-subjects", + Short: "List subjects in namespace", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidListSubjects, + } + + frostfsidCreateGroupCmd = &cobra.Command{ + Use: "create-group", + Short: "Create group in frostfsid contract", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidCreateGroup, + } + + frostfsidDeleteGroupCmd = &cobra.Command{ + Use: "delete-group", + Short: "Delete group from frostfsid contract", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidDeleteGroup, + } + + frostfsidListGroupsCmd = &cobra.Command{ + Use: "list-groups", + Short: "List groups in namespace", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidListGroups, + } + + frostfsidAddSubjectToGroupCmd = &cobra.Command{ + Use: "add-subject-to-group", + Short: "Add subject to group", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidAddSubjectToGroup, + } + + frostfsidRemoveSubjectFromGroupCmd = &cobra.Command{ + Use: "remove-subject-from-group", + Short: "Remove subject from group", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidRemoveSubjectFromGroup, + } + + frostfsidListGroupSubjectsCmd = &cobra.Command{ + Use: "list-group-subjects", + Short: "List subjects in group", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + Run: frostfsidListGroupSubjects, + } +) + +func initFrostfsIDCreateNamespaceCmd() { + frostfsidCmd.AddCommand(frostfsidCreateNamespaceCmd) + frostfsidCreateNamespaceCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidCreateNamespaceCmd.Flags().String(namespaceFlag, "", "Namespace name to create") +} + +func initFrostfsIDListNamespacesCmd() { + frostfsidCmd.AddCommand(frostfsidListNamespacesCmd) + frostfsidListNamespacesCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) +} + +func initFrostfsIDCreateSubjectCmd() { + frostfsidCmd.AddCommand(frostfsidCreateSubjectCmd) + frostfsidCreateSubjectCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidCreateSubjectCmd.Flags().String(namespaceFlag, "", "Namespace where create subject") + frostfsidCreateSubjectCmd.Flags().String(subjectNameFlag, "", "Subject name, must be unique in namespace") + frostfsidCreateSubjectCmd.Flags().String(subjectKeyFlag, "", "Subject hex-encoded public key") +} + +func initFrostfsIDDeleteSubjectCmd() { + frostfsidCmd.AddCommand(frostfsidDeleteSubjectCmd) + frostfsidDeleteSubjectCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidDeleteSubjectCmd.Flags().String(subjectAddressFlag, "", "Subject address") +} + +func initFrostfsIDListSubjectsCmd() { + frostfsidCmd.AddCommand(frostfsidListSubjectsCmd) + frostfsidListSubjectsCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidListSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace to list subjects") + frostfsidListSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)") +} + +func initFrostfsIDCreateGroupCmd() { + frostfsidCmd.AddCommand(frostfsidCreateGroupCmd) + frostfsidCreateGroupCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidCreateGroupCmd.Flags().String(namespaceFlag, "", "Namespace where create group") + frostfsidCreateGroupCmd.Flags().String(groupNameFlag, "", "Group name, must be unique in namespace") +} + +func initFrostfsIDDeleteGroupCmd() { + frostfsidCmd.AddCommand(frostfsidDeleteGroupCmd) + frostfsidDeleteGroupCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidDeleteGroupCmd.Flags().String(namespaceFlag, "", "Namespace to delete group") + frostfsidDeleteGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id") +} + +func initFrostfsIDListGroupsCmd() { + frostfsidCmd.AddCommand(frostfsidListGroupsCmd) + frostfsidListGroupsCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidListGroupsCmd.Flags().String(namespaceFlag, "", "Namespace to list groups") +} + +func initFrostfsIDAddSubjectToGroupCmd() { + frostfsidCmd.AddCommand(frostfsidAddSubjectToGroupCmd) + frostfsidAddSubjectToGroupCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidAddSubjectToGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address") + frostfsidAddSubjectToGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id") +} + +func initFrostfsIDRemoveSubjectFromGroupCmd() { + frostfsidCmd.AddCommand(frostfsidRemoveSubjectFromGroupCmd) + frostfsidRemoveSubjectFromGroupCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidRemoveSubjectFromGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address") + frostfsidRemoveSubjectFromGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id") +} + +func initFrostfsIDListGroupSubjectsCmd() { + frostfsidCmd.AddCommand(frostfsidListGroupSubjectsCmd) + frostfsidListGroupSubjectsCmd.Flags().StringP(endpointFlag, endpointFlagShort, "", endpointFlagDesc) + frostfsidListGroupSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace name") + frostfsidListGroupSubjectsCmd.Flags().Int64(groupIDFlag, 0, "Group id") + frostfsidListGroupSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)") +} + +func frostfsidCreateNamespace(cmd *cobra.Command, _ []string) { + ns := getFrostfsIDNamespace(cmd) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract client: %w", err) + + ffsid.addCall(ffsid.roCli.CreateNamespaceCall(ns)) + + err = ffsid.sendWait() + commonCmd.ExitOnErr(cmd, "create namespace error: %w", err) +} + +func frostfsidListNamespaces(cmd *cobra.Command, _ []string) { + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract invoker: %w", err) + + namespaces, err := ffsid.roCli.ListNamespaces() + commonCmd.ExitOnErr(cmd, "list namespaces: %w", err) + + sort.Slice(namespaces, func(i, j int) bool { return namespaces[i].Name < namespaces[j].Name }) + + for _, namespace := range namespaces { + if namespace.Name == "" { + namespace.Name = rootNamespacePlaceholder + } + cmd.Printf("%s\n", namespace.Name) + } +} + +func frostfsidCreateSubject(cmd *cobra.Command, _ []string) { + ns := getFrostfsIDNamespace(cmd) + subjName := getFrostfsIDSubjectName(cmd) + subjKey := getFrostfsIDSubjectKey(cmd) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract client: %w", err) + + ffsid.addCall(ffsid.roCli.CreateSubjectCall(ns, subjKey)) + if subjName != "" { + ffsid.addCall(ffsid.roCli.SetSubjectNameCall(subjKey.GetScriptHash(), subjName)) + } + + err = ffsid.sendWait() + commonCmd.ExitOnErr(cmd, "create subject: %w", err) +} + +func frostfsidDeleteSubject(cmd *cobra.Command, _ []string) { + subjectAddress := getFrostfsIDSubjectAddress(cmd) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract client: %w", err) + + ffsid.addCall(ffsid.roCli.DeleteSubjectCall(subjectAddress)) + + err = ffsid.sendWait() + commonCmd.ExitOnErr(cmd, "delete subject error: %w", err) +} + +func frostfsidListSubjects(cmd *cobra.Command, _ []string) { + ns := getFrostfsIDNamespace(cmd) + includeNames, _ := cmd.Flags().GetBool(includeNamesFlag) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract invoker: %w", err) + + subAddresses, err := ffsid.roCli.ListNamespaceSubjects(ns) + commonCmd.ExitOnErr(cmd, "list subjects: %w", err) + + sort.Slice(subAddresses, func(i, j int) bool { return subAddresses[i].Less(subAddresses[j]) }) + + for _, addr := range subAddresses { + if !includeNames { + cmd.Println(address.Uint160ToString(addr)) + continue + } + + subj, err := ffsid.roCli.GetSubject(addr) + commonCmd.ExitOnErr(cmd, "get subject: %w", err) + + cmd.Printf("%s (%s)\n", address.Uint160ToString(addr), subj.Name) + } +} + +func frostfsidCreateGroup(cmd *cobra.Command, _ []string) { + ns := getFrostfsIDNamespace(cmd) + groupName := getFrostfsIDGroupName(cmd) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract client: %w", err) + + ffsid.addCall(ffsid.roCli.CreateGroupCall(ns, groupName)) + + groupID, err := ffsid.roCli.ParseGroupID(ffsid.sendWaitRes()) + commonCmd.ExitOnErr(cmd, "create group: %w", err) + + cmd.Printf("group '%s' created with id: %d\n", groupName, groupID) +} + +func frostfsidDeleteGroup(cmd *cobra.Command, _ []string) { + ns := getFrostfsIDNamespace(cmd) + groupID := getFrostfsIDGroupID(cmd) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract client: %w", err) + + ffsid.addCall(ffsid.roCli.DeleteGroupCall(ns, groupID)) + + err = ffsid.sendWait() + commonCmd.ExitOnErr(cmd, "delete group error: %w", err) +} + +func frostfsidListGroups(cmd *cobra.Command, _ []string) { + ns := getFrostfsIDNamespace(cmd) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract invoker: %w", err) + + groups, err := ffsid.roCli.ListGroups(ns) + commonCmd.ExitOnErr(cmd, "list groups: %w", err) + + sort.Slice(groups, func(i, j int) bool { return groups[i].Name < groups[j].Name }) + + for _, group := range groups { + cmd.Printf("%s (%d)\n", group.Name, group.ID) + } +} + +func frostfsidAddSubjectToGroup(cmd *cobra.Command, _ []string) { + subjectAddress := getFrostfsIDSubjectAddress(cmd) + groupID := getFrostfsIDGroupID(cmd) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract client: %w", err) + + ffsid.addCall(ffsid.roCli.AddSubjectToGroupCall(subjectAddress, groupID)) + + err = ffsid.sendWait() + commonCmd.ExitOnErr(cmd, "add subject to group error: %w", err) +} + +func frostfsidRemoveSubjectFromGroup(cmd *cobra.Command, _ []string) { + subjectAddress := getFrostfsIDSubjectAddress(cmd) + groupID := getFrostfsIDGroupID(cmd) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract client: %w", err) + + ffsid.addCall(ffsid.roCli.RemoveSubjectFromGroupCall(subjectAddress, groupID)) + + err = ffsid.sendWait() + commonCmd.ExitOnErr(cmd, "remove subject from group error: %w", err) +} + +func frostfsidListGroupSubjects(cmd *cobra.Command, _ []string) { + ns := getFrostfsIDNamespace(cmd) + groupID := getFrostfsIDGroupID(cmd) + includeNames, _ := cmd.Flags().GetBool(includeNamesFlag) + + ffsid, err := newFrostfsIDClient(cmd) + commonCmd.ExitOnErr(cmd, "init contract client: %w", err) + + subjects, err := ffsid.roCli.ListGroupSubjects(ns, groupID) + commonCmd.ExitOnErr(cmd, "list group subjects: %w", err) + + sort.Slice(subjects, func(i, j int) bool { return subjects[i].Less(subjects[j]) }) + + for _, subjAddr := range subjects { + if !includeNames { + cmd.Println(address.Uint160ToString(subjAddr)) + continue + } + + subj, err := ffsid.roCli.GetSubject(subjAddr) + commonCmd.ExitOnErr(cmd, "get subject: %w", err) + + cmd.Printf("%s (%s)\n", address.Uint160ToString(subjAddr), subj.Name) + } +} + +type frostfsidClient struct { + bw *io.BufBinWriter + contractHash util.Uint160 + roCli *frostfsidclient.Client // client can be used only for waiting tx, parsing and forming method params + wCtx *initializeContext +} + +func newFrostfsIDClient(cmd *cobra.Command) (*frostfsidClient, error) { + wCtx, err := newInitializeContext(cmd, viper.GetViper()) + if err != nil { + return nil, fmt.Errorf("can't to initialize context: %w", err) + } + + r := management.NewReader(wCtx.ReadOnlyInvoker) + cs, err := r.GetContractByID(1) + if err != nil { + return nil, fmt.Errorf("can't get NNS contract info: %w", err) + } + + ffsidHash, err := nnsResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, domainOf(frostfsIDContract)) + if err != nil { + return nil, fmt.Errorf("can't get proxy contract hash: %w", err) + } + + return &frostfsidClient{ + bw: io.NewBufBinWriter(), + contractHash: ffsidHash, + roCli: frostfsidclient.NewSimple(wCtx.CommitteeAct, ffsidHash), + wCtx: wCtx, + }, nil +} + +func (f *frostfsidClient) addCall(method string, args []any) { + emit.AppCall(f.bw.BinWriter, f.contractHash, method, callflag.All, args...) +} + +func (f *frostfsidClient) sendWait() error { + if err := f.wCtx.sendConsensusTx(f.bw.Bytes()); err != nil { + return err + } + f.bw.Reset() + + return f.wCtx.awaitTx() +} + +func (f *frostfsidClient) sendWaitRes() (*state.AppExecResult, error) { + if err := f.wCtx.sendConsensusTx(f.bw.Bytes()); err != nil { + return nil, err + } + f.bw.Reset() + + if len(f.wCtx.SentTxs) == 0 { + return nil, errors.New("no transactions to wait") + } + + f.wCtx.Command.Println("Waiting for transactions to persist...") + return f.roCli.Wait(f.wCtx.SentTxs[0].hash, f.wCtx.SentTxs[0].vub, nil) +} diff --git a/cmd/frostfs-adm/internal/modules/morph/frostfsid_util.go b/cmd/frostfs-adm/internal/modules/morph/frostfsid_util.go index cfe3c1ee9..f88e4edfb 100644 --- a/cmd/frostfs-adm/internal/modules/morph/frostfsid_util.go +++ b/cmd/frostfs-adm/internal/modules/morph/frostfsid_util.go @@ -1,14 +1,26 @@ package morph import ( + "errors" "fmt" + "regexp" + commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/spf13/cobra" "github.com/spf13/viper" ) +var ( + frostfsidSubjectNameRegexp = regexp.MustCompile(`^[\w+=,.@-]{1,64}$`) + frostfsidGroupNameRegexp = regexp.MustCompile(`^[\w+=,.@-]{1,128}$`) + + // frostfsidNamespaceNameRegexp similar to https://git.frostfs.info/TrueCloudLab/frostfs-contract/src/commit/f2a82aa635aa57d9b05092d8cf15b170b53cc324/nns/nns_contract.go#L690 + frostfsidNamespaceNameRegexp = regexp.MustCompile(`(^$)|(^[a-z0-9]{1,2}$)|(^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$)`) +) + func getFrostfsIDAdmin(v *viper.Viper) (util.Uint160, bool, error) { admin := v.GetString(frostfsIDAdminConfigKey) if admin == "" { @@ -31,3 +43,67 @@ func getFrostfsIDAdmin(v *viper.Viper) (util.Uint160, bool, error) { } return util.Uint160{}, true, fmt.Errorf("frostfsid: admin is invalid: '%s'", admin) } + +func getFrostfsIDSubjectKey(cmd *cobra.Command) *keys.PublicKey { + subjKeyHex, _ := cmd.Flags().GetString(subjectKeyFlag) + subjKey, err := keys.NewPublicKeyFromString(subjKeyHex) + commonCmd.ExitOnErr(cmd, "invalid subject key: %w", err) + return subjKey +} + +func getFrostfsIDSubjectAddress(cmd *cobra.Command) util.Uint160 { + subjAddress, _ := cmd.Flags().GetString(subjectAddressFlag) + subjAddr, err := address.StringToUint160(subjAddress) + commonCmd.ExitOnErr(cmd, "invalid subject address: %w", err) + return subjAddr +} + +func getFrostfsIDSubjectName(cmd *cobra.Command) string { + subjectName, _ := cmd.Flags().GetString(subjectNameFlag) + + if subjectName == "" { + return "" + } + + if !frostfsidSubjectNameRegexp.MatchString(subjectName) { + commonCmd.ExitOnErr(cmd, "invalid subject name: %w", + fmt.Errorf("name must match regexp: %s", frostfsidSubjectNameRegexp.String())) + } + + return subjectName +} + +func getFrostfsIDGroupName(cmd *cobra.Command) string { + groupName, _ := cmd.Flags().GetString(groupNameFlag) + + if !frostfsidGroupNameRegexp.MatchString(groupName) { + commonCmd.ExitOnErr(cmd, "invalid group name: %w", + fmt.Errorf("name must match regexp: %s", frostfsidGroupNameRegexp.String())) + } + + return groupName +} + +func getFrostfsIDGroupID(cmd *cobra.Command) int64 { + groupID, _ := cmd.Flags().GetInt64(groupIDFlag) + if groupID <= 0 { + commonCmd.ExitOnErr(cmd, "invalid group id: %w", + errors.New("group id must be positive integer")) + } + + return groupID +} + +func getFrostfsIDNamespace(cmd *cobra.Command) string { + ns, _ := cmd.Flags().GetString(namespaceFlag) + if ns == rootNamespacePlaceholder { + ns = "" + } + + if !frostfsidNamespaceNameRegexp.MatchString(ns) { + commonCmd.ExitOnErr(cmd, "invalid namespace: %w", + fmt.Errorf("name must match regexp: %s", frostfsidNamespaceNameRegexp.String())) + } + + return ns +} diff --git a/cmd/frostfs-adm/internal/modules/morph/frostfsid_util_test.go b/cmd/frostfs-adm/internal/modules/morph/frostfsid_util_test.go index 192dc9f12..f73ebad08 100644 --- a/cmd/frostfs-adm/internal/modules/morph/frostfsid_util_test.go +++ b/cmd/frostfs-adm/internal/modules/morph/frostfsid_util_test.go @@ -51,3 +51,122 @@ func TestFrostfsIDConfig(t *testing.T) { require.False(t, found) }) } + +func TestNamespaceRegexp(t *testing.T) { + for _, tc := range []struct { + name string + namespace string + matched bool + }{ + { + name: "root empty ns", + namespace: "", + matched: true, + }, + { + name: "simple valid ns", + namespace: "my-namespace-123", + matched: true, + }, + { + name: "root placeholder", + namespace: "", + matched: false, + }, + { + name: "too long", + namespace: "abcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyz", + matched: false, + }, + { + name: "start with hyphen", + namespace: "-ns", + matched: false, + }, + { + name: "end with hyphen", + namespace: "ns-", + matched: false, + }, + { + name: "with spaces", + namespace: "ns ns", + matched: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.matched, frostfsidNamespaceNameRegexp.MatchString(tc.namespace)) + }) + } +} + +func TestSubjectNameRegexp(t *testing.T) { + for _, tc := range []struct { + name string + subject string + matched bool + }{ + { + name: "empty", + subject: "", + matched: false, + }, + { + name: "invalid", + subject: "invalid{name}", + matched: false, + }, + { + name: "too long", + subject: "abcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyz", + matched: false, + }, + { + name: "valid", + subject: "valid_name.012345@6789", + matched: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.matched, frostfsidSubjectNameRegexp.MatchString(tc.subject)) + }) + } +} + +func TestSubjectGroupRegexp(t *testing.T) { + for _, tc := range []struct { + name string + subject string + matched bool + }{ + { + name: "empty", + subject: "", + matched: false, + }, + { + name: "invalid", + subject: "invalid{name}", + matched: false, + }, + { + name: "too long", + subject: "abcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyz", + matched: false, + }, + { + name: "long", + subject: "abcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyz", + matched: true, + }, + { + name: "valid", + subject: "valid_name.012345@6789", + matched: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.matched, frostfsidGroupNameRegexp.MatchString(tc.subject)) + }) + } +} diff --git a/cmd/frostfs-adm/internal/modules/morph/root.go b/cmd/frostfs-adm/internal/modules/morph/root.go index 4176a6470..600fe21c8 100644 --- a/cmd/frostfs-adm/internal/modules/morph/root.go +++ b/cmd/frostfs-adm/internal/modules/morph/root.go @@ -293,6 +293,19 @@ func init() { initProxyAddAccount() initProxyRemoveAccount() + + RootCmd.AddCommand(frostfsidCmd) + initFrostfsIDCreateNamespaceCmd() + initFrostfsIDListNamespacesCmd() + initFrostfsIDCreateSubjectCmd() + initFrostfsIDDeleteSubjectCmd() + initFrostfsIDListSubjectsCmd() + initFrostfsIDCreateGroupCmd() + initFrostfsIDDeleteGroupCmd() + initFrostfsIDListGroupsCmd() + initFrostfsIDAddSubjectToGroupCmd() + initFrostfsIDRemoveSubjectFromGroupCmd() + initFrostfsIDListGroupSubjectsCmd() } func initProxyAddAccount() { diff --git a/go.mod b/go.mod index ddf587ad5..8b48cd788 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20231031104748-498877e378fd - git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958 + git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20240115082915-f2a82aa635aa git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65 git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231122162120-56debcfa569e git.frostfs.info/TrueCloudLab/hrw v1.2.1 diff --git a/go.sum b/go.sum index 9be0e55cd..11b208d32 100644 Binary files a/go.sum and b/go.sum differ