Skip to content

Commit

Permalink
support for selecting google proto timestamps from the authorization …
Browse files Browse the repository at this point in the history
…json - converted to rfc3339

Signed-off-by: Guilherme Cassolato <[email protected]>
  • Loading branch information
guicassolato committed Nov 27, 2024
1 parent 20d75a5 commit dbe9477
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 12 deletions.
2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
4 changes: 3 additions & 1 deletion pkg/expressions/cel/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
}

Expand Down
22 changes: 21 additions & 1 deletion pkg/expressions/cel/expressions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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"}`)
}
51 changes: 42 additions & 9 deletions pkg/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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+:{`)
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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 := &timestamppb.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 {
Expand Down
18 changes: 18 additions & 0 deletions pkg/json/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}`)
}

0 comments on commit dbe9477

Please sign in to comment.