forked from TrueCloudLab/frostfs-node
[#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:
parent
e976a55358
commit
08e83a2bc7
8 changed files with 398 additions and 31 deletions
|
@ -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")
|
||||
}
|
275
cmd/neofs-cli/modules/acl/extended/create.go
Normal file
275
cmd/neofs-cli/modules/acl/extended/create.go
Normal 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
|
||||
}
|
91
cmd/neofs-cli/modules/acl/extended/create_test.go
Normal file
91
cmd/neofs-cli/modules/acl/extended/create_test.go
Normal 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)
|
||||
}
|
12
cmd/neofs-cli/modules/acl/extended/root.go
Normal file
12
cmd/neofs-cli/modules/acl/extended/root.go
Normal 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)
|
||||
}
|
15
cmd/neofs-cli/modules/acl/root.go
Normal file
15
cmd/neofs-cli/modules/acl/root.go
Normal 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)
|
||||
}
|
|
@ -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
1
go.mod
|
@ -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
1
go.sum
|
@ -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=
|
||||
|
|
Loading…
Reference in a new issue