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;