Skip to content

Commit

Permalink
kms: support JSON strings as RuleSet representation
Browse files Browse the repository at this point in the history
This commit adds support for parsing JSON strings are
valid `RuleSet` representations.

For example, the following policy is now valid:
```
{
    "allow": {
        "key:create":   "my-key",
        "key:decrypt":  "my-key",
        "key:generate": ["my-key", "my-key2"]
    }
}
```

Signed-off-by: Andreas Auernhammer <[email protected]>
  • Loading branch information
aead committed Feb 16, 2024
1 parent cfd2e4f commit f1a97b0
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 24 deletions.
2 changes: 1 addition & 1 deletion kms/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
86 changes: 63 additions & 23 deletions kms/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package kms

import (
"bytes"
"cmp"
"encoding/json"
"slices"

Expand All @@ -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
Expand All @@ -34,33 +34,40 @@ 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:
//
// 1. RuleSet as JSON object:
//
// {
// "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.
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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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))
}
Expand All @@ -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
Expand All @@ -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
}
98 changes: 98 additions & 0 deletions kms/rule_test.go
Original file line number Diff line number Diff line change
@@ -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*": {}},
},
}

0 comments on commit f1a97b0

Please sign in to comment.