From 09861a470e4df0b329a7b8996a80dcda3c5cd291 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Wed, 27 Nov 2024 17:23:05 +0100 Subject: [PATCH] support for selecting google proto timestamps from the authorization json - converted to rfc3339 Signed-off-by: Guilherme Cassolato --- docs/features.md | 2 +- go.mod | 1 + go.sum | 2 + pkg/expressions/cel/expressions.go | 4 +- pkg/expressions/cel/expressions_test.go | 22 ++++++++++- pkg/json/json.go | 51 ++++++++++++++++++++----- pkg/json/json_test.go | 18 +++++++++ 7 files changed, 88 insertions(+), 12 deletions(-) diff --git a/docs/features.md b/docs/features.md index 9667e5a4..acb2d723 100644 --- a/docs/features.md +++ b/docs/features.md @@ -80,7 +80,7 @@ Similar to [JSON Paths](#common-feature-json-paths-selector), Authorino supports [String extension functions](https://pkg.go.dev/github.com/google/cel-go/ext#readme-strings), such as `split`, `substring`, `indexOf`, etc, are also supported. -Use the `expression` field for selecting values from the [Authorization JSON](./architecture.md#the-authorization-json). The type of the selected value will be converted to a JSON-compatible equivalent. Complex types without a direct JSON equivalent may be converted to objects (e.g. `google.golang.org/protobuf/types/known/timestamppb.Timestamp` gets converted to `{ "seconds": Number, "nanos": Number }`) +Use the `expression` field for selecting values from the [Authorization JSON](./architecture.md#the-authorization-json). The type of the selected value will be converted to a JSON-compatible equivalent. Complex types without a direct JSON equivalent may be converted to the raw abstract objects used to represent the type. JSON objects that match [`google.golang.org/protobuf/types/known/timestamppb.Timestamp`](https://pkg.go.dev/google.golang.org/protobuf/types/known/timestamppb#Timestamp) type (i.e. `{ "seconds": Number, "nanos": Number }`) are converted to their corresponding timestamp string representation in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) format. The most common applications of `expression` are for building dynamic URLs and request parameters when fetching metadata from external sources, extending properties of identity objects, and dynamic authorization response attributes (e.g. injected HTTP headers, etc). diff --git a/go.mod b/go.mod index 80ee0f7a..4a1a4524 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/open-policy-agent/opa v0.68.0 github.com/prometheus/client_golang v1.20.2 + github.com/samber/lo v1.47.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/tidwall/gjson v1.14.0 diff --git a/go.sum b/go.sum index d9ef5732..982be7ca 100644 --- a/go.sum +++ b/go.sum @@ -483,6 +483,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= diff --git a/pkg/expressions/cel/expressions.go b/pkg/expressions/cel/expressions.go index ca3e78da..f85d14e5 100644 --- a/pkg/expressions/cel/expressions.go +++ b/pkg/expressions/cel/expressions.go @@ -14,6 +14,8 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" + + authorinojson "github.com/kuadrant/authorino/pkg/json" ) const RootMetadataBinding = "metadata" @@ -80,7 +82,7 @@ func (e *Expression) ResolveFor(json string) (interface{}, error) { if jsonLiteral, err := ValueToJSON(result); err != nil { return nil, err } else { - return gjson.Parse(jsonLiteral).Value(), nil + return authorinojson.TryTimestamp(gjson.Parse(jsonLiteral)), nil } } diff --git a/pkg/expressions/cel/expressions_test.go b/pkg/expressions/cel/expressions_test.go index 5ae7ecbc..d5abb875 100644 --- a/pkg/expressions/cel/expressions_test.go +++ b/pkg/expressions/cel/expressions_test.go @@ -3,10 +3,11 @@ package cel import ( "testing" + "github.com/golang/mock/gomock" mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" "gotest.tools/assert" - "github.com/golang/mock/gomock" + authorinojson "github.com/kuadrant/authorino/pkg/json" ) func TestPredicate(t *testing.T) { @@ -48,3 +49,22 @@ func TestPredicate(t *testing.T) { assert.NilError(t, err) assert.Equal(t, response, true) } + +func TestTimestamp(t *testing.T) { + expression, _ := NewExpression(`request.time`) + + val, err := expression.ResolveFor(`{"request":{"time":{"seconds":1732721739,"nanos":123456}}}`) + s, _ := authorinojson.StringifyJSON(val) + assert.NilError(t, err) + assert.Equal(t, s, "2024-11-27T15:35:39.000123456Z") + + val, err = expression.ResolveFor(`{"request":{"time":"2024-11-27T15:35:39Z"}}`) + s, _ = authorinojson.StringifyJSON(val) + assert.NilError(t, err) + assert.Equal(t, s, "2024-11-27T15:35:39Z") + + val, err = expression.ResolveFor(`{"request":{"time":{"custom":"11 Nov 2024 03:35:39pm UTC"}}}`) + s, _ = authorinojson.StringifyJSON(val) + assert.NilError(t, err) + assert.Equal(t, s, `{"custom":"11 Nov 2024 03:35:39pm UTC"}`) +} diff --git a/pkg/json/json.go b/pkg/json/json.go index e483a811..8b7791c0 100644 --- a/pkg/json/json.go +++ b/pkg/json/json.go @@ -9,13 +9,24 @@ import ( "net/http" "regexp" "strings" + "time" "unicode" "github.com/kuadrant/authorino/pkg/expressions" + "github.com/samber/lo" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/tidwall/gjson" ) +type JSONValueType int + +const ( + JSONValueTypeStatic JSONValueType = iota + JSONValueTypePattern + JSONValueTypeTemplate +) + var ( allCurlyBracesRegex = regexp.MustCompile("{") curlyBracesForModifiersRegex = regexp.MustCompile(`[^@]+@\w+:{`) @@ -44,16 +55,23 @@ func (v *JSONValue) ResolveFor(jsonData string) (interface{}, error) { return v.resolveForSafe(jsonData), nil } +func (v *JSONValue) Type() JSONValueType { + if v.Pattern == "" { + return JSONValueTypeStatic + } + if v.IsTemplate() { + return JSONValueTypeTemplate + } + return JSONValueTypePattern +} + func (v *JSONValue) resolveForSafe(jsonData string) interface{} { - if v.Pattern != "" { - // If all curly braces in the pattern are for passing arguments to modifiers, then it's likely NOT a template. - // To be a template, the pattern must contain at least one curly brace delimiting a variable placeholder. - if v.IsTemplate() { - return ReplaceJSONPlaceholders(v.Pattern, jsonData) - } else { - return gjson.Get(jsonData, v.Pattern).Value() - } - } else { + switch v.Type() { + case JSONValueTypeTemplate: + return ReplaceJSONPlaceholders(v.Pattern, jsonData) + case JSONValueTypePattern: + return TryTimestamp(gjson.Get(jsonData, v.Pattern)) + default: return v.Static } } @@ -66,6 +84,21 @@ func (v *JSONValue) IsTemplate() bool { return len(curlyBracesForModifiersRegex.FindAllStringSubmatch(v.Pattern, -1)) != len(allCurlyBracesRegex.FindAllStringSubmatch(v.Pattern, -1)) } +// TryTimestamp tries to parse a gjson result to a RFC3339 timestamp +// If the result is not a valid timestamp, it returns the value as parsed by gjson +func TryTimestamp(v gjson.Result) interface{} { + if m := v.Map(); len(m) > 0 && len(m) <= 2 && len(lo.Without(lo.Keys(m), "seconds", "nanos")) == 0 { + t := ×tamppb.Timestamp{} + if err := json.Unmarshal([]byte(v.Raw), t); err == nil && t.IsValid() { + if m["nanos"].Exists() { + return t.AsTime().Format(time.RFC3339Nano) + } + return t.AsTime().Format(time.RFC3339) + } + } + return v.Value() +} + // UnmashalJSONResponse unmarshalls a generic HTTP response body into a JSON structure // Pass optionally a pointer to a byte array to get the raw body of the response object written back func UnmashalJSONResponse(resp *http.Response, v interface{}, b *[]byte) error { diff --git a/pkg/json/json_test.go b/pkg/json/json_test.go index b5510115..65ff287f 100644 --- a/pkg/json/json_test.go +++ b/pkg/json/json_test.go @@ -346,3 +346,21 @@ func TestStringifyJSON(t *testing.T) { assert.Equal(t, str, `{"prop_str":"str","prop_num":123,"prop_bool":false,"prop_null":null,"prop_arr":["a","b","c"],"prop_obj":{"a_prop":"a_value"}}`) assert.NilError(t, err) } + +func TestTryTimestamp(t *testing.T) { + val := TryTimestamp(gjson.Parse(`{"seconds":1732721739}`)) + s, _ := StringifyJSON(val) + assert.Equal(t, s, "2024-11-27T15:35:39Z") + + val = TryTimestamp(gjson.Parse(`{"seconds":1732721739,"nanos":123456}`)) + s, _ = StringifyJSON(val) + assert.Equal(t, s, "2024-11-27T15:35:39.000123456Z") + + val = TryTimestamp(gjson.Parse(`"2024-11-27T15:35:39Z"`)) + s, _ = StringifyJSON(val) + assert.Equal(t, s, "2024-11-27T15:35:39Z") + + val = TryTimestamp(gjson.Parse(`{"foo":"bar"}`)) + s, _ = StringifyJSON(val) + assert.Equal(t, s, `{"foo":"bar"}`) +}