[#1092] neofs-cli: Add 'acl extended create' command

Follows neofs-cli refactor scheme from #1074

Signed-off-by: Alex Vanin <alexey@nspcc.ru>
This commit is contained in:
Alex Vanin 2022-01-20 18:34:53 +03:00 committed by Alex Vanin
parent e976a55358
commit 08e83a2bc7
8 changed files with 398 additions and 31 deletions

View file

@ -1,31 +0,0 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// aclCmd represents the acl command
var aclCmd = &cobra.Command{
Use: "acl",
Short: "Operations with Access Control Lists",
Long: `Operations with Access Control Lists`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("acl called")
},
}
func init() {
rootCmd.AddCommand(aclCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// aclCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// aclCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View file

@ -0,0 +1,275 @@
package extended
import (
"bytes"
"crypto/ecdsa"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"strings"
"github.com/flynn-archive/go-shlex"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/spf13/cobra"
)
var createCmd = &cobra.Command{
Use: "create",
Short: "Create extended ACL from the text representation",
Long: `Create extended ACL from the text representation.
Rule consist of these blocks: <action> <operation> [<filter1> ...] [<target1> ...]
Action is 'allow' or 'deny'.
Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', 'getrange', or 'getrangehash'.
Filter consists of <typ>:<key><match><value>
Typ is 'obj' for object applied filter or 'req' for request applied filter.
Key is a valid unicode string corresponding to object or request header key.
Match is '==' for matching and '!=' for non-matching filter.
Value is a valid unicode string corresponding to object or request header value.
Target is
'user' for container owner,
'system' for Storage nodes in container and Inner Ring nodes,
'others' for all other request senders,
'pubkey:<key1>,<key2>,...' for exact request sender, where <key> is a hex-encoded 33-byte public key.
When both '--rule' and '--file' arguments are used, '--rule' records will be placed higher in resulting extended ACL table.
`,
Example: `neofs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -f rules.txt --out table.json
neofs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -r 'allow get obj:Key=Value others' -r 'deny put others'`,
Run: createEACL,
}
func init() {
createCmd.Flags().StringArrayP("rule", "r", nil, "extended ACL table record to apply")
createCmd.Flags().StringP("file", "f", "", "read list of extended ACL table records from from text file")
createCmd.Flags().StringP("out", "o", "", "save JSON formatted extended ACL table in file")
createCmd.Flags().StringP("cid", "", "", "container ID")
_ = cobra.MarkFlagFilename(createCmd.Flags(), "file")
_ = cobra.MarkFlagFilename(createCmd.Flags(), "out")
}
func createEACL(cmd *cobra.Command, _ []string) {
rules, _ := cmd.Flags().GetStringArray("rule")
fileArg, _ := cmd.Flags().GetString("file")
outArg, _ := cmd.Flags().GetString("out")
cidArg, _ := cmd.Flags().GetString("cid")
containerID := cid.New()
if err := containerID.Parse(cidArg); err != nil {
cmd.Printf("invalid container ID: %v", err)
return
}
rulesFile, err := getRulesFromFile(fileArg)
if err != nil {
cmd.Printf("can't read rules from file : %v", err)
return
}
rules = append(rules, rulesFile...)
if len(rules) == 0 {
cmd.Println("no extended ACL rules has been provided")
return
}
tb := eacl.NewTable()
for _, ruleStr := range rules {
r, err := shlex.Split(ruleStr)
if err != nil {
cmd.Printf("can't parse rule '%s': %v)", ruleStr, err)
return
}
err = parseTable(tb, r)
if err != nil {
cmd.Printf("can't create extended ACL record from rule '%s': %v", ruleStr, err)
return
}
}
tb.SetCID(containerID)
data, err := tb.MarshalJSON()
if err != nil {
cmd.Println(err)
return
}
buf := new(bytes.Buffer)
err = json.Indent(buf, data, "", " ")
if err != nil {
cmd.Println(err)
return
}
if len(outArg) == 0 {
cmd.Println(buf)
return
}
err = ioutil.WriteFile(outArg, buf.Bytes(), 0644)
if err != nil {
cmd.Println(err)
}
}
func getRulesFromFile(filename string) ([]string, error) {
if len(filename) == 0 {
return nil, nil
}
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return strings.Split(strings.TrimSpace(string(data)), "\n"), nil
}
// parseTable parses eACL table from the following form:
// <action> <operation> [<filter1> ...] [<target1> ...]
//
// Examples:
// allow get req:X-Header=123 obj:Attr=value others:0xkey1,key2 system:key3 user:key4
func parseTable(tb *eacl.Table, args []string) error {
if len(args) < 2 {
return errors.New("at least 2 arguments must be provided")
}
var action eacl.Action
if !action.FromString(strings.ToUpper(args[0])) {
return errors.New("invalid action (expected 'allow' or 'deny')")
}
ops, err := parseOperations(args[1])
if err != nil {
return err
}
r, err := parseRecord(args[2:])
if err != nil {
return err
}
r.SetAction(action)
for _, op := range ops {
r := *r
r.SetOperation(op)
tb.AddRecord(&r)
}
return nil
}
func parseRecord(args []string) (*eacl.Record, error) {
r := new(eacl.Record)
for i := range args {
ss := strings.SplitN(args[i], ":", 2)
switch prefix := strings.ToLower(ss[0]); prefix {
case "req", "obj": // filters
if len(ss) != 2 {
return nil, fmt.Errorf("invalid filter or target: %s", args[i])
}
i := strings.Index(ss[1], "=")
if i < 0 {
return nil, fmt.Errorf("invalid filter key-value pair: %s", ss[1])
}
var key, value string
var op eacl.Match
if 0 < i && ss[1][i-1] == '!' {
key = ss[1][:i-1]
op = eacl.MatchStringNotEqual
} else {
key = ss[1][:i]
op = eacl.MatchStringEqual
}
value = ss[1][i+1:]
typ := eacl.HeaderFromRequest
if ss[0] == "obj" {
typ = eacl.HeaderFromObject
}
r.AddFilter(typ, op, key, value)
case "others", "system", "user", "pubkey": // targets
var err error
var pubs []ecdsa.PublicKey
if len(ss) == 2 {
pubs, err = parseKeyList(ss[1])
if err != nil {
return nil, err
}
}
var role eacl.Role
if prefix != "pubkey" {
role, err = roleFromString(prefix)
if err != nil {
return nil, err
}
}
eacl.AddFormedTarget(r, role, pubs...)
default:
return nil, fmt.Errorf("invalid prefix: %s", ss[0])
}
}
return r, nil
}
func roleFromString(s string) (eacl.Role, error) {
var r eacl.Role
if !r.FromString(strings.ToUpper(s)) {
return r, fmt.Errorf("unexpected role %s", s)
}
return r, nil
}
// parseKeyList parses list of hex-encoded public keys separated by comma.
func parseKeyList(s string) ([]ecdsa.PublicKey, error) {
ss := strings.Split(s, ",")
pubs := make([]ecdsa.PublicKey, len(ss))
for i := range ss {
st := strings.TrimPrefix(ss[i], "0x")
pub, err := keys.NewPublicKeyFromString(st)
if err != nil {
return nil, fmt.Errorf("invalid public key '%s': %w", ss[i], err)
}
pubs[i] = ecdsa.PublicKey(*pub)
}
return pubs, nil
}
func parseOperations(s string) ([]eacl.Operation, error) {
ss := strings.Split(s, ",")
ops := make([]eacl.Operation, len(ss))
for i := range ss {
if !ops[i].FromString(strings.ToUpper(ss[i])) {
return nil, fmt.Errorf("invalid operation: %s", ss[i])
}
}
return ops, nil
}

View file

@ -0,0 +1,91 @@
package extended
import (
"strings"
"testing"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/stretchr/testify/require"
)
func TestParseTable(t *testing.T) {
tests := [...]struct {
name string // test name
rule string // input extended ACL rule
jsonRecord string // produced record after successfull parsing
}{
{
name: "valid rule with multiple filters",
rule: "deny get obj:a=b req:c=d others",
jsonRecord: `{"operation":"GET","action":"DENY","filters":[{"headerType":"OBJECT","matchType":"STRING_EQUAL","key":"a","value":"b"},{"headerType":"REQUEST","matchType":"STRING_EQUAL","key":"c","value":"d"}],"targets":[{"role":"OTHERS","keys":[]}]}`,
},
{
name: "valid rule without filters",
rule: "allow put user",
jsonRecord: `{"operation":"PUT","action":"ALLOW","filters":[],"targets":[{"role":"USER","keys":[]}]}`,
},
{
name: "valid rule with public key",
rule: "deny getrange pubkey:036410abb260bbbda89f61c0cad65a4fa15ac5cb83b3c3abf8aee403856fcf65ed",
jsonRecord: `{"operation":"GETRANGE","action":"DENY","filters":[],"targets":[{"role":"ROLE_UNSPECIFIED","keys":["A2QQq7Jgu72on2HAytZaT6FaxcuDs8Or+K7kA4Vvz2Xt"]}]}`,
},
{
name: "missing action",
rule: "get obj:a=b others",
},
{
name: "invalid action",
rule: "permit get obj:a=b others",
},
{
name: "missing op",
rule: "deny obj:a=b others",
},
{
name: "invalid op action",
rule: "deny look obj:a=b others",
},
{
name: "invalid filter type",
rule: "deny get invalid:a=b others",
},
{
name: "invalid target group",
rule: "deny get obj:a=b helpers",
},
{
name: "invalid public key",
rule: "deny get obj:a=b pubkey:0123",
},
}
eaclTable := eacl.NewTable()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ss := strings.Split(test.rule, " ")
err := parseTable(eaclTable, ss)
ok := len(test.jsonRecord) > 0
require.Equal(t, ok, err == nil, err)
if ok {
expectedRecord := eacl.NewRecord()
err = expectedRecord.UnmarshalJSON([]byte(test.jsonRecord))
require.NoError(t, err)
actualRecord := eaclTable.Records()[len(eaclTable.Records())-1]
equalRecords(t, expectedRecord, actualRecord)
}
})
}
}
func equalRecords(t *testing.T, r1, r2 *eacl.Record) {
d1, err := r1.Marshal()
require.NoError(t, err)
d2, err := r2.Marshal()
require.NoError(t, err)
require.Equal(t, d1, d2)
}

View file

@ -0,0 +1,12 @@
package extended
import "github.com/spf13/cobra"
var Cmd = &cobra.Command{
Use: "extended",
Short: "Operations with Extended Access Control Lists",
}
func init() {
Cmd.AddCommand(createCmd)
}

View file

@ -0,0 +1,15 @@
package acl
import (
"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/acl/extended"
"github.com/spf13/cobra"
)
var Cmd = &cobra.Command{
Use: "acl",
Short: "Operations with Access Control Lists",
}
func init() {
Cmd.AddCommand(extended.Cmd)
}

View file

@ -15,6 +15,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/acl"
"github.com/nspcc-dev/neofs-node/misc"
"github.com/nspcc-dev/neofs-node/pkg/network"
"github.com/nspcc-dev/neofs-sdk-go/client"
@ -120,6 +121,8 @@ func init() {
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().Bool("version", false, "Application version and NeoFS API compatibility")
rootCmd.AddCommand(acl.Cmd)
}
func entryPoint(cmd *cobra.Command, _ []string) {

1
go.mod
View file

@ -3,6 +3,7 @@ module github.com/nspcc-dev/neofs-node
go 1.16
require (
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/google/go-github/v39 v39.2.0
github.com/google/uuid v1.2.0
github.com/hashicorp/golang-lru v0.5.4

1
go.sum
View file

@ -126,6 +126,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=