diff --git a/kms/client.go b/kms/client.go index b6bd0d9..3410b11 100644 --- a/kms/client.go +++ b/kms/client.go @@ -69,7 +69,7 @@ func NewClient(conf *Config) (*Client, error) { if err != nil { return nil, err } - tlsConf.GetClientCertificate = func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { + tlsConf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { return &cert, nil } } diff --git a/kms/rule.go b/kms/rule.go index 80bf724..4ad8e0e 100644 --- a/kms/rule.go +++ b/kms/rule.go @@ -5,7 +5,7 @@ package kms import ( - "bytes" + "cmp" "encoding/json" "slices" @@ -25,7 +25,7 @@ type Rule struct{} // command argument matches the "my-key*" pattern. // // The RuleSet is the building block for KMS policies. Such a -// policy defines which RuleSet is be applied for which KMS +// policy defines which RuleSet is applied for which KMS // commands. For example, the RuleSet from above may be used // for the CreateKey command. In this case, the policy would // allow the creation of keys if and only if the key name @@ -34,7 +34,8 @@ type Rule struct{} // A RuleSet can be represented as protobuf message or JSON // object. Usually, policies are defined in JSON to be human // readable. For ease of use, a RuleSet can not just be -// represented as JSON object but also as JSON array. For +// represented as JSON object but also as JSON array or JSON +// string if the RuleSet contains just a single entry. For // example the following to JSON documents are decoded into // equal RuleSets: // @@ -42,16 +43,19 @@ type Rule struct{} // // { // "my-key*": {}, -// "sys-key": {} // } // // 2. RuleSet as JSON array: // -// ["my-key*", "sys-key"] +// ["my-key*"] // -// The 2nd form is shorter and easier to read than the 1st one. -// However, the 1st one more accurately represenets the RuleSet's -// in memory representation and allows future extensions. +// 3. RuleSet as JSON string: +// +// "my-key*" +// +// The 2nd and 3rd forms are shorter and easier to read than the +// 1st one. However, the 1st one more accurately represenets the +// RuleSet's in memory representation and allows future extensions. type RuleSet map[string]Rule // MarshalPB converts the RuleSet into its protobuf representation. @@ -59,8 +63,11 @@ func (r *RuleSet) MarshalPB(v *pb.RuleSet) error { rs := *r v.Rules = make(map[string]*pb.Rule, len(rs)) - for name := range rs { - v.Rules[name] = &pb.Rule{} + for pattern := range rs { + if pattern == "" { + continue + } + v.Rules[pattern] = &pb.Rule{} } return nil } @@ -70,8 +77,11 @@ func (r *RuleSet) UnmarshalPB(v *pb.RuleSet) error { *r = make(RuleSet, len(v.Rules)) rs := *r - for name := range v.Rules { - rs[name] = Rule{} + for pattern := range v.Rules { + if pattern == "" { + continue + } + rs[pattern] = Rule{} } return nil } @@ -101,12 +111,23 @@ func (r RuleSet) MarshalJSON() ([]byte, error) { } } if !hasRule { - names := make([]string, 0, len(r)) - for name := range r { - names = append(names, name) + if len(r) == 1 { + for pattern := range r { + return json.Marshal(pattern) + } } - slices.SortFunc(names, func(a, b string) int { return len(a) - len(b) }) - return json.Marshal(names) + + patterns := make([]string, 0, len(r)) + for pattern := range r { + patterns = append(patterns, pattern) + } + slices.SortFunc(patterns, func(a, b string) int { + if n := len(a) - len(b); n != 0 { + return n + } + return cmp.Compare(a, b) + }) + return json.Marshal(patterns) } return json.Marshal(map[string]Rule(r)) } @@ -118,15 +139,29 @@ func (r RuleSet) MarshalJSON() ([]byte, error) { // containing the patterns as strings and the Rules as // JSON objects. func (r *RuleSet) UnmarshalJSON(b []byte) error { - if bytes.HasPrefix(b, []byte{'['}) && bytes.HasSuffix(b, []byte{']'}) { - var names []string - if err := json.Unmarshal(b, &names); err != nil { + if n := len(b); n >= 2 && b[0] == '"' && b[n-1] == '"' { + var pattern string + if err := json.Unmarshal(b, &pattern); err != nil { + return err + } + if pattern == "" { + *r = RuleSet{} + } else { + *r = RuleSet{pattern: {}} + } + return nil + } + if n := len(b); n >= 2 && b[0] == '[' && b[n-1] == ']' { + var patterns []string + if err := json.Unmarshal(b, &patterns); err != nil { return err } - m := make(RuleSet, len(names)) - for _, name := range names { - m[name] = Rule{} + m := make(RuleSet, len(patterns)) + for _, pattern := range patterns { + if pattern != "" { + m[pattern] = Rule{} + } } *r = m return nil @@ -136,6 +171,11 @@ func (r *RuleSet) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(b, &m); err != nil { return err } + for pattern := range m { + if pattern == "" { + delete(m, pattern) + } + } *r = RuleSet(m) return nil } diff --git a/kms/rule_test.go b/kms/rule_test.go new file mode 100644 index 0000000..161bf99 --- /dev/null +++ b/kms/rule_test.go @@ -0,0 +1,98 @@ +// Copyright 2024 - MinIO, Inc. All rights reserved. +// Use of this source code is governed by the AGPLv3 +// license that can be found in the LICENSE file. + +package kms + +import ( + "encoding/json" + "maps" + "testing" +) + +func TestRuleSet_Marshal(t *testing.T) { + t.Parallel() + + for i, test := range marshalRuleSetTests { + text, err := json.Marshal(test.Set) + if err != nil { + t.Fatalf("Test %d: failed to marshal RuleSet: %v", i, err) + } + + if s := string(text); s != test.JSON { + t.Fatalf("Test %d: JSON mismatch: got '%s' - want '%s'", i, s, test.JSON) + } + } +} + +func TestRuleSet_Unmarshal(t *testing.T) { + t.Parallel() + + for i, test := range unmarshalRuleSetTests { + for _, JSON := range test.JSON { + var set RuleSet + err := json.Unmarshal([]byte(JSON), &set) + if err == nil && test.ShouldFail { + t.Fatalf("Test %d: should have failed to parse RuleSet JSON '%s'", i, JSON) + } + if err != nil && !test.ShouldFail { + t.Fatalf("Test %d: failed to parse RuleSet JSON '%s': %v", i, JSON, err) + } + if test.ShouldFail { + continue + } + if !maps.Equal(set, test.Set) { + t.Fatalf("Test %d: RuleSet mismatch: got '%v' - want '%v'", i, set, test.Set) + } + } + } +} + +var marshalRuleSetTests = []struct { + Set RuleSet + JSON string +}{ + { + Set: RuleSet{}, + JSON: `[]`, + }, + { + Set: RuleSet{"my-key": {}}, + JSON: `"my-key"`, + }, + { + Set: RuleSet{"my-key": {}, "foo": {}, "bar*": {}}, + JSON: `["foo","bar*","my-key"]`, + }, + { + Set: RuleSet{"my-key": {}, "foo": {}, "bar": {}}, + JSON: `["bar","foo","my-key"]`, + }, +} + +var unmarshalRuleSetTests = []struct { + JSON []string // List of JSON representations equal to Set + Set RuleSet + ShouldFail bool +}{ + { + JSON: []string{`""`, `[]`, `{}`}, + Set: RuleSet{}, + }, + { + JSON: []string{`"my-key"`, `["my-key"]`, `{"my-key":{}}`}, + Set: RuleSet{"my-key": {}}, + }, + { + JSON: []string{`"my-*"`, `["my-*"]`, `{"my-*":{}}`}, + Set: RuleSet{"my-*": {}}, + }, + { + JSON: []string{`["my-key", ""]`, `{"my-key":{}, "": {}}`}, + Set: RuleSet{"my-key": {}}, + }, + { + JSON: []string{`["my-key", "foo", "bar*"]`, `{"my-key":{}, "foo": {}, "bar*": {}}`}, + Set: RuleSet{"my-key": {}, "foo": {}, "bar*": {}}, + }, +}