diff --git a/Makefile b/Makefile index debad31..7efc56b 100755 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ TMP_DIR := .cache OUTPUT_LINT_DIR ?= $(shell pwd)/bin LINT_VERSION ?= 1.55.1 LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION) +EASYJSON_VERSION ?= $(shell go list -f '{{.Version}}' -m github.com/mailru/easyjson) +EASYJSON_DIR ?= $(shell pwd)/bin/easyjson-$(EASYJSON_VERSION) # Run all code formatters fmts: fmt imports @@ -60,3 +62,15 @@ staticcheck-install: # Run staticcheck staticcheck-run: @staticcheck ./... + +easyjson-install: + @rm -rf $(EASYJSON_DIR) + @mkdir -p $(EASYJSON_DIR) + @GOBIN=$(EASYJSON_DIR) go install github.com/mailru/easyjson/...@$(EASYJSON_VERSION) + +generate: + @if [ ! -d "$(EASYJSON_DIR)" ]; then \ + make easyjson-install; \ + fi + find ./ -name "_easyjson.go" -exec rm -rf {} \; + $(EASYJSON_DIR)/easyjson ./pkg/chain/chain.go \ No newline at end of file diff --git a/go.mod b/go.mod index cac5089..2ef97fd 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231129062201-a1b61d394958 github.com/google/uuid v1.3.0 + github.com/mailru/easyjson v0.7.7 github.com/nspcc-dev/neo-go v0.103.0 github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 @@ -14,6 +15,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231020160724-c3955f87d1b5 // indirect diff --git a/go.sum b/go.sum index 96722c1..d29d172 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,10 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 h1:n4ZaFCKt1pQJd7PXoMJabZWK9ejjbLOVrkl/lOUmshg= diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go index d72611f..2da29ff 100644 --- a/pkg/chain/chain.go +++ b/pkg/chain/chain.go @@ -1,7 +1,6 @@ package chain import ( - "encoding/json" "fmt" "strings" @@ -23,6 +22,7 @@ const ( MatchTypeFirstMatch MatchType = 1 ) +//easyjson:json type Chain struct { ID ID @@ -31,19 +31,6 @@ type Chain struct { MatchType MatchType } -func (id ID) MarshalJSON() ([]byte, error) { - return json.Marshal([]byte(id)) -} - -func (id *ID) UnmarshalJSON(data []byte) error { - var idRaw []byte - if err := json.Unmarshal(data, &idRaw); err != nil { - return err - } - *id = ID(idRaw) - return nil -} - func (c *Chain) Bytes() []byte { data, err := c.MarshalBinary() if err != nil { @@ -123,45 +110,36 @@ const ( CondSliceContains ) +var condToStr = []struct { + ct ConditionType + str string +}{ + {CondStringEquals, "StringEquals"}, + {CondStringNotEquals, "StringNotEquals"}, + {CondStringEqualsIgnoreCase, "StringEqualsIgnoreCase"}, + {CondStringNotEqualsIgnoreCase, "StringNotEqualsIgnoreCase"}, + {CondStringLike, "StringLike"}, + {CondStringNotLike, "StringNotLike"}, + {CondStringLessThan, "StringLessThan"}, + {CondStringLessThanEquals, "StringLessThanEquals"}, + {CondStringGreaterThan, "StringGreaterThan"}, + {CondStringGreaterThanEquals, "StringGreaterThanEquals"}, + {CondNumericEquals, "NumericEquals"}, + {CondNumericNotEquals, "NumericNotEquals"}, + {CondNumericLessThan, "NumericLessThan"}, + {CondNumericLessThanEquals, "NumericLessThanEquals"}, + {CondNumericGreaterThan, "NumericGreaterThan"}, + {CondNumericGreaterThanEquals, "NumericGreaterThanEquals"}, + {CondSliceContains, "SliceContains"}, +} + func (c ConditionType) String() string { - switch c { - case CondStringEquals: - return "StringEquals" - case CondStringNotEquals: - return "StringNotEquals" - case CondStringEqualsIgnoreCase: - return "StringEqualsIgnoreCase" - case CondStringNotEqualsIgnoreCase: - return "StringNotEqualsIgnoreCase" - case CondStringLike: - return "StringLike" - case CondStringNotLike: - return "StringNotLike" - case CondStringLessThan: - return "StringLessThan" - case CondStringLessThanEquals: - return "StringLessThanEquals" - case CondStringGreaterThan: - return "StringGreaterThan" - case CondStringGreaterThanEquals: - return "StringGreaterThanEquals" - case CondNumericEquals: - return "NumericEquals" - case CondNumericNotEquals: - return "NumericNotEquals" - case CondNumericLessThan: - return "NumericLessThan" - case CondNumericLessThanEquals: - return "NumericLessThanEquals" - case CondNumericGreaterThan: - return "NumericGreaterThan" - case CondNumericGreaterThanEquals: - return "NumericGreaterThanEquals" - case CondSliceContains: - return "SliceContains" - default: - return "unknown condition type" + for _, v := range condToStr { + if v.ct == c { + return v.str + } } + return "unknown condition type" } const condSliceContainsDelimiter = "\x00" diff --git a/pkg/chain/chain_easyjson.go b/pkg/chain/chain_easyjson.go new file mode 100644 index 0000000..c744878 Binary files /dev/null and b/pkg/chain/chain_easyjson.go differ diff --git a/pkg/chain/marshal.go b/pkg/chain/marshal_binary.go similarity index 100% rename from pkg/chain/marshal.go rename to pkg/chain/marshal_binary.go diff --git a/pkg/chain/marshal_test.go b/pkg/chain/marshal_binary_test.go similarity index 100% rename from pkg/chain/marshal_test.go rename to pkg/chain/marshal_binary_test.go diff --git a/pkg/chain/marshal_json.go b/pkg/chain/marshal_json.go new file mode 100644 index 0000000..6039081 --- /dev/null +++ b/pkg/chain/marshal_json.go @@ -0,0 +1,153 @@ +package chain + +import ( + "fmt" + "strconv" + + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// Run `make generate`` if types added or changed + +var matchTypeToJSONValue = []struct { + mt MatchType + str string +}{ + {MatchTypeDenyPriority, "DenyPriority"}, + {MatchTypeFirstMatch, "FirstMatch"}, +} + +var statusToJSONValue = []struct { + s Status + str string +}{ + {Allow, "Allow"}, + {NoRuleFound, "NoRuleFound"}, + {AccessDenied, "AccessDenied"}, + {QuotaLimitReached, "QuotaLimitReached"}, +} + +var objectTypeToJSONValue = []struct { + t ObjectType + str string +}{ + {ObjectRequest, "Request"}, + {ObjectResource, "Resource"}, +} + +func (mt MatchType) MarshalEasyJSON(w *jwriter.Writer) { + for _, p := range matchTypeToJSONValue { + if p.mt == mt { + w.String(p.str) + return + } + } + w.String(strconv.FormatUint(uint64(mt), 10)) +} + +func (mt *MatchType) UnmarshalEasyJSON(l *jlexer.Lexer) { + str := l.String() + for _, p := range matchTypeToJSONValue { + if p.str == str { + *mt = p.mt + return + } + } + + v, err := strconv.ParseUint(str, 10, 8) + if err != nil { + l.AddError(fmt.Errorf("failed to parse match type: %w", err)) + return + } + *mt = MatchType(v) +} + +func (st Status) MarshalEasyJSON(w *jwriter.Writer) { + for _, p := range statusToJSONValue { + if p.s == st { + w.String(p.str) + return + } + } + w.String(strconv.FormatUint(uint64(st), 10)) +} + +func (st *Status) UnmarshalEasyJSON(l *jlexer.Lexer) { + str := l.String() + for _, p := range statusToJSONValue { + if p.str == str { + *st = p.s + return + } + } + + v, err := strconv.ParseUint(str, 10, 8) + if err != nil { + l.AddError(fmt.Errorf("failed to parse status: %w", err)) + return + } + *st = Status(v) +} + +func (ot ObjectType) MarshalEasyJSON(w *jwriter.Writer) { + for _, p := range objectTypeToJSONValue { + if p.t == ot { + w.String(p.str) + return + } + } + w.String(strconv.FormatUint(uint64(ot), 10)) +} + +func (ot *ObjectType) UnmarshalEasyJSON(l *jlexer.Lexer) { + str := l.String() + for _, p := range objectTypeToJSONValue { + if p.str == str { + *ot = p.t + return + } + } + + v, err := strconv.ParseUint(str, 10, 8) + if err != nil { + l.AddError(fmt.Errorf("failed to parse object type: %w", err)) + return + } + *ot = ObjectType(v) +} + +func (ct ConditionType) MarshalEasyJSON(w *jwriter.Writer) { + for _, p := range condToStr { + if p.ct == ct { + w.String(p.str) + return + } + } + w.String(strconv.FormatUint(uint64(ct), 10)) +} + +func (ct *ConditionType) UnmarshalEasyJSON(l *jlexer.Lexer) { + str := l.String() + for _, p := range condToStr { + if p.str == str { + *ct = p.ct + return + } + } + + v, err := strconv.ParseUint(str, 10, 8) + if err != nil { + l.AddError(fmt.Errorf("failed to parse condition type: %w", err)) + return + } + *ct = ConditionType(v) +} + +func (id ID) MarshalEasyJSON(w *jwriter.Writer) { + w.Base64Bytes([]byte(id)) +} + +func (id *ID) UnmarshalEasyJSON(l *jlexer.Lexer) { + *id = ID(l.Bytes()) +} diff --git a/pkg/chain/marshal_json_test.go b/pkg/chain/marshal_json_test.go new file mode 100644 index 0000000..75b1bc6 --- /dev/null +++ b/pkg/chain/marshal_json_test.go @@ -0,0 +1,121 @@ +package chain + +import ( + "fmt" + "os" + "testing" + + "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/stretchr/testify/require" +) + +func TestID(t *testing.T) { + key, err := keys.NewPrivateKeyFromWIF("L5eVx6HcHaFpQpvjQ3fy29uKDZ8rQ34bfMVx4XfZMm52EqafpNMg") // s3-gw key + require.NoError(t, err) + + chain1 := &Chain{ID: ID(key.PublicKey().GetScriptHash().BytesBE())} + data := chain1.Bytes() + + var chain2 Chain + require.NoError(t, chain2.DecodeBytes(data)) + + require.Equal(t, chain1.ID, chain2.ID) + + data, err = chain1.MarshalJSON() + require.NoError(t, err) + + require.NoError(t, chain2.UnmarshalJSON(data)) + + require.Equal(t, chain1.ID, chain2.ID) +} + +func TestMatchTypeJson(t *testing.T) { + for _, mt := range []MatchType{MatchTypeDenyPriority, MatchTypeFirstMatch, MatchType(100)} { + var chain Chain + chain.MatchType = mt + + data, err := chain.MarshalJSON() + require.NoError(t, err) + if mt == MatchTypeDenyPriority { + require.Equal(t, []byte("{\"ID\":\"\",\"Rules\":null,\"MatchType\":\"DenyPriority\"}"), data) + } else if mt == MatchTypeFirstMatch { + require.Equal(t, []byte("{\"ID\":\"\",\"Rules\":null,\"MatchType\":\"FirstMatch\"}"), data) + } else { + require.Equal(t, []byte(fmt.Sprintf("{\"ID\":\"\",\"Rules\":null,\"MatchType\":\"%d\"}", mt)), data) + } + + var parsed Chain + require.NoError(t, parsed.UnmarshalJSON(data)) + require.Equal(t, chain, parsed) + + require.Error(t, parsed.UnmarshalJSON([]byte("{\"ID\":\"\",\"Rules\":null,\"MatchType\":\"NotValid\"}"))) + } +} + +func TestJsonEnums(t *testing.T) { + chain := Chain{ + ID: "2cca5ae7-cee8-428d-b45f-567fb1d03f01", // will be encoded to base64 + MatchType: MatchTypeFirstMatch, + Rules: []Rule{ + { + Status: AccessDenied, + Actions: Actions{ + Names: []string{native.MethodDeleteObject, native.MethodGetContainer}, + }, + Resources: Resources{ + Names: []string{native.ResourceFormatAllObjects}, + }, + Condition: []Condition{ + { + Op: CondStringEquals, + Object: ObjectRequest, + Key: native.PropertyKeyActorRole, + Value: native.PropertyValueContainerRoleOthers, + }, + }, + }, + { + Status: QuotaLimitReached, + Actions: Actions{ + Inverted: true, + Names: []string{native.MethodPutObject}, + }, + Resources: Resources{ + Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, "9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J")}, + }, + Any: true, + Condition: []Condition{ + { + Op: CondStringNotLike, + Object: ObjectResource, + Key: native.PropertyKeyObjectType, + Value: "regular", + }, + }, + }, + { + Status: Status(100), + Condition: []Condition{ + { + Op: ConditionType(255), + Object: ObjectType(128), + }, + }, + }, + }, + } + + data, err := chain.MarshalJSON() + require.NoError(t, err) + + var parsed Chain + require.NoError(t, parsed.UnmarshalJSON(data)) + require.Equal(t, chain, parsed) + + expected, err := os.ReadFile("./testdata/test_status_json.json") + require.NoError(t, err) + + require.NoError(t, parsed.UnmarshalJSON(expected)) + require.Equal(t, chain, parsed) +} diff --git a/pkg/chain/testdata/test_status_json.json b/pkg/chain/testdata/test_status_json.json new file mode 100644 index 0000000..34bcc63 --- /dev/null +++ b/pkg/chain/testdata/test_status_json.json @@ -0,0 +1,75 @@ +{ + "ID": "MmNjYTVhZTctY2VlOC00MjhkLWI0NWYtNTY3ZmIxZDAzZjAx", + "Rules": [ + { + "Status": "AccessDenied", + "Actions": { + "Inverted": false, + "Names": [ + "DeleteObject", + "GetContainer" + ] + }, + "Resources": { + "Inverted": false, + "Names": [ + "native:object/*" + ] + }, + "Any": false, + "Condition": [ + { + "Op": "StringEquals", + "Object": "Request", + "Key": "$Actor:role", + "Value": "others" + } + ] + }, + { + "Status": "QuotaLimitReached", + "Actions": { + "Inverted": true, + "Names": [ + "PutObject" + ] + }, + "Resources": { + "Inverted": false, + "Names": [ + "native:object//9LPLUFZpEmfidG4n44vi2cjXKXSqWT492tCvLJiJ8W1J/*" + ] + }, + "Any": true, + "Condition": [ + { + "Op": "StringNotLike", + "Object": "Resource", + "Key": "$Object:objectType", + "Value": "regular" + } + ] + }, + { + "Status": "100", + "Actions": { + "Inverted": false, + "Names": null + }, + "Resources": { + "Inverted": false, + "Names": null + }, + "Any": false, + "Condition": [ + { + "Op": "255", + "Object": "128", + "Key": "", + "Value": "" + } + ] + } + ], + "MatchType": "FirstMatch" +} \ No newline at end of file