package util import ( "bytes" "crypto/ecdsa" "encoding/hex" "errors" "fmt" "strings" "text/tabwriter" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" "github.com/flynn-archive/go-shlex" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) // PrettyPrintTableBACL print basic ACL in table format. func PrettyPrintTableBACL(cmd *cobra.Command, bacl *acl.Basic) { // Header w := tabwriter.NewWriter(cmd.OutOrStdout(), 1, 4, 4, ' ', 0) fmt.Fprintln(w, "\tRangeHASH\tRange\tSearch\tDelete\tPut\tHead\tGet") // Bits bits := []string{ boolToString(bacl.Sticky()) + " " + boolToString(bacl.Extendable()), getRoleBitsForOperation(bacl, acl.OpObjectHash), getRoleBitsForOperation(bacl, acl.OpObjectRange), getRoleBitsForOperation(bacl, acl.OpObjectSearch), getRoleBitsForOperation(bacl, acl.OpObjectDelete), getRoleBitsForOperation(bacl, acl.OpObjectPut), getRoleBitsForOperation(bacl, acl.OpObjectHead), getRoleBitsForOperation(bacl, acl.OpObjectGet), } fmt.Fprintln(w, strings.Join(bits, "\t")) // Footer footer := []string{"X F"} for i := 0; i < 7; i++ { footer = append(footer, "U S O B") } fmt.Fprintln(w, strings.Join(footer, "\t")) w.Flush() cmd.Println(" X-Sticky F-Final U-User S-System O-Others B-Bearer") } func getRoleBitsForOperation(bacl *acl.Basic, op acl.Op) string { return boolToString(bacl.IsOpAllowed(op, acl.RoleOwner)) + " " + boolToString(bacl.IsOpAllowed(op, acl.RoleContainer)) + " " + boolToString(bacl.IsOpAllowed(op, acl.RoleOthers)) + " " + boolToString(bacl.IsOpAllowed(op, acl.RoleInnerRing)) } func boolToString(b bool) string { if b { return "1" } return "0" } // PrettyPrintTableEACL print extended ACL in table format. func PrettyPrintTableEACL(cmd *cobra.Command, table *eacl.Table) { out := tablewriter.NewWriter(cmd.OutOrStdout()) out.SetHeader([]string{"Operation", "Action", "Filters", "Targets"}) out.SetAlignment(tablewriter.ALIGN_CENTER) out.SetRowLine(true) out.SetAutoWrapText(false) for _, r := range table.Records() { out.Append([]string{ r.Operation().String(), r.Action().String(), eaclFiltersToString(r.Filters()), eaclTargetsToString(r.Targets()), }) } out.Render() } func eaclTargetsToString(ts []eacl.Target) string { b := bytes.NewBuffer(nil) for _, t := range ts { keysExists := len(t.BinaryKeys()) > 0 switch t.Role() { case eacl.RoleUser: b.WriteString("User") if keysExists { b.WriteString(": ") } case eacl.RoleSystem: b.WriteString("System") if keysExists { b.WriteString(": ") } case eacl.RoleOthers: b.WriteString("Others") if keysExists { b.WriteString(": ") } default: b.WriteString("Unknown") if keysExists { b.WriteString(": ") } } for i, pub := range t.BinaryKeys() { if i != 0 { b.WriteString(" ") } b.WriteString(hex.EncodeToString(pub)) b.WriteString("\n") } } return b.String() } func eaclFiltersToString(fs []eacl.Filter) string { b := bytes.NewBuffer(nil) tw := tabwriter.NewWriter(b, 0, 0, 1, ' ', 0) for _, f := range fs { switch f.From() { case eacl.HeaderFromObject: _, _ = tw.Write([]byte("O:\t")) case eacl.HeaderFromRequest: _, _ = tw.Write([]byte("R:\t")) case eacl.HeaderFromService: _, _ = tw.Write([]byte("S:\t")) default: _, _ = tw.Write([]byte(" \t")) } _, _ = tw.Write([]byte(f.Key())) switch f.Matcher() { case eacl.MatchStringEqual: _, _ = tw.Write([]byte("\t==\t")) case eacl.MatchStringNotEqual: _, _ = tw.Write([]byte("\t!=\t")) case eacl.MatchUnknown: } _, _ = tw.Write([]byte(f.Value() + "\t")) _, _ = tw.Write([]byte("\n")) } _ = tw.Flush() // To have nice output with tabwriter, we must append newline // after the last line. Here we strip it to delete empty line // in the final output. s := b.String() if len(s) > 0 { s = s[:len(s)-1] } return s } // ParseEACLRules parses eACL table. // Uses ParseEACLRule. // //nolint:godot func ParseEACLRules(table *eacl.Table, rules []string) error { if len(rules) == 0 { return errors.New("no extended ACL rules has been provided") } for _, ruleStr := range rules { err := ParseEACLRule(table, ruleStr) if err != nil { return fmt.Errorf("can't create extended acl record from rule '%s': %v", ruleStr, err) } } return nil } // ParseEACLRule parses eACL table from the following form: // [ ...] [ ...] // // Examples: // allow get req:X-Header=123 obj:Attr=value others:0xkey1,key2 system:key3 user:key4 // //nolint:godot func ParseEACLRule(table *eacl.Table, rule string) error { r, err := shlex.Split(rule) if err != nil { return fmt.Errorf("can't parse rule '%s': %v", rule, err) } return parseEACLTable(table, r) } func parseEACLTable(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 := eaclOperationsFromString(args[1]) if err != nil { return err } r, err := parseEACLRecord(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 parseEACLRecord(args []string) (*eacl.Record, error) { r := new(eacl.Record) for _, arg := range args { before, after, found := strings.Cut(arg, ":") switch prefix := strings.ToLower(before); prefix { case "req", "obj": // filters if !found { return nil, fmt.Errorf("invalid filter or target: %s", arg) } var key, value string var op eacl.Match var f bool key, value, f = strings.Cut(after, "!=") if f { op = eacl.MatchStringNotEqual } else { key, value, f = strings.Cut(after, "=") if !f { return nil, fmt.Errorf("invalid filter key-value pair: %s", after) } op = eacl.MatchStringEqual } typ := eacl.HeaderFromRequest if before == "obj" { typ = eacl.HeaderFromObject } r.AddFilter(typ, op, key, value) case "others", "system", "user", "pubkey": // targets var err error var pubs []ecdsa.PublicKey if found { pubs, err = parseKeyList(after) if err != nil { return nil, err } } var role eacl.Role if prefix != "pubkey" { role, err = eaclRoleFromString(prefix) if err != nil { return nil, err } } eacl.AddFormedTarget(r, role, pubs...) default: return nil, fmt.Errorf("invalid prefix: %s", before) } } return r, nil } // eaclRoleFromString parses eacl.Role from string. func eaclRoleFromString(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 } // eaclOperationsFromString parses list of eacl.Operation separated by comma. func eaclOperationsFromString(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 }