diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md index 853e47ee2d..0cd1d9c52a 100644 --- a/docs/content/policy-reference.md +++ b/docs/content/policy-reference.md @@ -341,7 +341,7 @@ complex types. | Built-in | Description | Wasm Support | | ------- |-------------|---------------| -| `value := object.get(object, key, default)` | `value` is the value stored by the `object` at `key`. If no value is found, `default` is returned. | ✅ | +| `value := object.get(object, key, default)` | `value` is the value stored by the `object` at `key`. If no value is found, `default` is returned. If the supplied `key` is an `array`, then `object.get` will search through a nested object or array using each key in turn. For example: `object.get({"a": [{ "b": true }]}, ["a", 0, "b"], false)` results in `true` | ✅ | | `output := object.remove(object, keys)` | `output` is a new object which is the result of removing the specified `keys` from `object`. `keys` must be either an array, object, or set of keys. | ✅ | | `output := object.union(objectA, objectB)` | `output` is a new object which is the result of an asymmetric recursive union of two objects where conflicts are resolved by choosing the key from the right-hand object (`objectB`). For example: `object.union({"a": 1, "b": 2, "c": {"d": 3}}, {"a": 7, "c": {"d": 4, "e": 5}})` will result in `{"a": 7, "b": 2, "c": {"d": 4, "e": 5}}` | ✅ | | `output := object.union_n(array)` | `output` is a new object which is the result of an asymmetric recursive union of all objects in `array`, merged from left to right, where conflicts are resolved by choosing the key from the right-hand object. For example: `object.union_n([{"a": 1}, {"b": 2}, {"a": 3}])` will result in `{"b": 2, "a": 3}` | ``SDK-dependent`` | diff --git a/internal/ref/ref.go b/internal/ref/ref.go index 9bdde5c81f..6e84df4b08 100644 --- a/internal/ref/ref.go +++ b/internal/ref/ref.go @@ -26,3 +26,14 @@ func ParseDataPath(s string) (ast.Ref, error) { return path.Ref(ast.DefaultRootDocument), nil } + +// ArrayPath will take an ast.Array and build an ast.Ref using the ast.Terms in the Array +func ArrayPath(a *ast.Array) ast.Ref { + var ref ast.Ref + + a.Foreach(func(term *ast.Term) { + ref = append(ref, term) + }) + + return ref +} diff --git a/test/cases/testdata/objectget/test-objectget-path.yaml b/test/cases/testdata/objectget/test-objectget-path.yaml new file mode 100644 index 0000000000..9a0a08a80d --- /dev/null +++ b/test/cases/testdata/objectget/test-objectget-path.yaml @@ -0,0 +1,124 @@ +cases: +- note: objectget/empty_path_returns_object + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({"a": 1}, [], 2) + } + want_result: [{x: {a: 1}}] + +- note: objectget/path_with_single_element + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({ "a": 1 }, ["a"], 2) + } + want_result: [{x: 1}] + +- note: objectget/path_with_two_elements + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({ "a": { "b": 1 } }, ["a", "b"], 2) + } + want_result: [{x: 1}] + +- note: objectget/path_with_three_elements + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({ "a": { "b": { "c": 1 } } }, ["a", "b", "c"], 2) + } + want_result: [{x: 1}] + +- note: objectget/path_with_single_element_no_result + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({ "a": 1 }, ["b"], 2) + } + want_result: [{x: 2}] + +- note: objectget/path_with_two_elements_no_result + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({ "a": { "b": 1 } }, ["b", "a"], 2) + } + want_result: [{x: 2}] + +- note: objectget/path_with_three_elements_no_result + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({ "a": { "b": { "c": 1 } } }, ["a", "b", "a"], 2) + } + want_result: [{x: 2}] + +- note: objectget/path_with_non_string_keys + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({ 1: { "b": { [1,2,3]: 1 } } }, [1, "b", [1,2,3]], 2) + } + want_result: [{x: 1}] + +- note: objectget/get_intermediate_non_object + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({ "a": { "b": [1,2,3] } }, ["a", "b", "a"], 2) + } + want_result: [{x: 2}] + +- note: objectget/get_intermediate_array + query: data.test.p = x + modules: + - | + package test + + p = x { + x := object.get({ "a": { "b": [{"c": 1}] } }, ["a", "b", 0, "c"], 2) + } + want_result: [{x: 1}] + +- note: objectget/get_for_non_object + query: data.test.p = x + input_term: '{"obj":"object"}' + modules: + - | + package test + + p = x { + x := object.get(input.obj, ["a"], 2) + } + want_error: 'object.get: operand 1 must be object but got string' + want_error_code: eval_type_error + strict_error: true diff --git a/topdown/object.go b/topdown/object.go index 4dfd9d6ae4..1eb28073de 100644 --- a/topdown/object.go +++ b/topdown/object.go @@ -6,6 +6,7 @@ package topdown import ( "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/internal/ref" "github.com/open-policy-agent/opa/topdown/builtins" "github.com/open-policy-agent/opa/types" ) @@ -100,11 +101,29 @@ func builtinObjectGet(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter return err } - if ret := object.Get(operands[1]); ret != nil { - return iter(ret) + // if the get key is not an array, attempt to get the top level key for the operand value in the object + path, err := builtins.ArrayOperand(operands[1].Value, 2) + if err != nil { + if ret := object.Get(operands[1]); ret != nil { + return iter(ret) + } + + return iter(operands[2]) } - return iter(operands[2]) + // if the path is empty, then we skip selecting nested keys and return the whole object + if path.Len() == 0 { + return iter(operands[0]) + } + + // build an ast.Ref from the array and see if it matches within the object + pathRef := ref.ArrayPath(path) + value, err := object.Find(pathRef) + if err != nil { + return iter(operands[2]) + } + + return iter(ast.NewTerm(value)) } // getObjectKeysParam returns a set of key values diff --git a/wasm/src/object.c b/wasm/src/object.c index fcc2cc4ffd..74f87345d1 100644 --- a/wasm/src/object.c +++ b/wasm/src/object.c @@ -435,10 +435,42 @@ opa_value *builtin_object_get(opa_value *obj, opa_value *key, opa_value *value) return NULL; } - opa_object_elem_t *elem = opa_object_get(opa_cast_object(obj), key); - if (elem != NULL) + opa_value *elem; + + // if the key is not an array, then we get that top level key from the object/array or return the default value + if (opa_value_type(key) != OPA_ARRAY) { + elem = opa_value_get(obj, key); + if (elem != NULL) + { + return elem; + } + + return value; + } + + size_t path_len = opa_cast_array(key)->len; + // if the path is empty, then we skip selecting nested keys and return the default + if (path_len == 0) { + return obj; + } + + for (int i = 0; i < path_len; i++) { - return elem->v; + opa_value *path_component = opa_cast_array(key)->elems[i].v; + + elem = opa_value_get(obj, path_component); + + if (elem == NULL) + { + return value; + } + + if (i == path_len-1) + { + return elem; + } + + obj = elem; } return value;