This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 3a418da50b27 CAMEL-24737: camel-opa - let allowKey reach a verdict
nested in the decision document (#26437)
3a418da50b27 is described below
commit 3a418da50b27ff33760cc01927ab42017128088f
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Sep 16 10:33:57 2026 +0200
CAMEL-24737: camel-opa - let allowKey reach a verdict nested in the
decision document (#26437)
allowKey was a single top-level map lookup, so a policy returning
{"result": {"allow": true}} - the common shape when querying a package
rather
than a rule head - could not be read and the component denied. Worse, that
failure was indistinguishable from a real deny: the route saw
CamelOpaDecisionAllow=false either way and the only hint sat at DEBUG.
allowKey now accepts a dotted path, with a direct lookup of the whole key
tried
first so a key that itself contains a dot resolves exactly as before. The
unreadable-decision log moved to WARN and names the header carrying the raw
document, fired once per evaluator so a deny[msg] policy without a default
does
not turn it into a per-message log of a whole document.
Co-authored-by: Claude Opus 5 <[email protected]>
---
.../org/apache/camel/catalog/components/opa.json | 4 +-
.../apache/camel/catalog/docs/opa-component.adoc | 6 ++-
.../org/apache/camel/component/opa/opa.json | 4 +-
.../camel-opa/src/main/docs/opa-component.adoc | 6 ++-
.../camel/component/opa/OpaConfiguration.java | 3 ++
.../camel/component/opa/OpaPolicyEvaluator.java | 42 +++++++++++++++++++--
.../camel/component/opa/OpaProducerTest.java | 44 ++++++++++++++++++++++
.../component/dsl/OpaComponentBuilderFactory.java | 5 ++-
.../endpoint/dsl/OpaEndpointBuilderFactory.java | 5 ++-
9 files changed, 106 insertions(+), 13 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
index 02e9044ba20b..182cd145e675 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
@@ -24,7 +24,7 @@
"remote": true
},
"componentProperties": {
- "allowKey": { "index": 0, "kind": "property", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain b [...]
+ "allowKey": { "index": 0, "kind": "property", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain b [...]
"configuration": { "index": 1, "kind": "property", "displayName":
"Configuration", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.opa.OpaConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration." },
"includeBody": { "index": 2, "kind": "property", "displayName": "Include
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to send the message body to OPA as
part of the input document. Disabled by default: bodies can b [...]
"includeHeaders": { "index": 3, "kind": "property", "displayName":
"Include Headers", "group": "producer", "label": "", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "*", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of message header names
to send to OPA in the input document. The defau [...]
@@ -45,7 +45,7 @@
},
"properties": {
"policyPath": { "index": 0, "kind": "path", "displayName": "Policy Path",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Path of the Rego rule head
to evaluate, relative to the OPA data document. For a rule named allow in a
policy declaring package authz.orders, this is authz\/orders\/allow. The path
is taken from the endpoint only: i [...]
- "allowKey": { "index": 1, "kind": "parameter", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain [...]
+ "allowKey": { "index": 1, "kind": "parameter", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain [...]
"includeBody": { "index": 2, "kind": "parameter", "displayName": "Include
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to send the message body to OPA as
part of the input document. Disabled by default: bodies can [...]
"includeHeaders": { "index": 3, "kind": "parameter", "displayName":
"Include Headers", "group": "producer", "label": "", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "*", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of message header names
to send to OPA in the input document. The defa [...]
"includeProperties": { "index": 4, "kind": "parameter", "displayName":
"Include Properties", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of exchange property
names to send to OPA in the input document, or {code } for all o [...]
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
index eaf35efdf89d..b39ac054b340 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
@@ -151,9 +151,11 @@ boolean verdict read out of it:
* a decision that *is* a boolean is the verdict;
* a decision that is an object is searched for the `allowKey` entry (`allow`
by default), which must itself be a
- boolean;
+ boolean. `allowKey` accepts a dotted path, so `allowKey=result.allow` reads
a verdict nested inside the document
+ as `{"result": {"allow": true}}`;
* anything else cannot be read as a verdict and counts as a deny, with the raw
document still available for the
- route to inspect.
+ route to inspect. That case is logged at WARN rather than DEBUG: a document
that arrived but could not be read is
+ a configuration problem, and from `CamelOpaDecisionAllow` alone it is
indistinguishable from a genuine denial.
Both headers are written on every evaluation, so a verdict set by an inbound
message never survives into the
route.
diff --git
a/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
b/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
index 02e9044ba20b..182cd145e675 100644
---
a/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
+++
b/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
@@ -24,7 +24,7 @@
"remote": true
},
"componentProperties": {
- "allowKey": { "index": 0, "kind": "property", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain b [...]
+ "allowKey": { "index": 0, "kind": "property", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain b [...]
"configuration": { "index": 1, "kind": "property", "displayName":
"Configuration", "group": "producer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.opa.OpaConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration." },
"includeBody": { "index": 2, "kind": "property", "displayName": "Include
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to send the message body to OPA as
part of the input document. Disabled by default: bodies can b [...]
"includeHeaders": { "index": 3, "kind": "property", "displayName":
"Include Headers", "group": "producer", "label": "", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "*", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of message header names
to send to OPA in the input document. The defau [...]
@@ -45,7 +45,7 @@
},
"properties": {
"policyPath": { "index": 0, "kind": "path", "displayName": "Policy Path",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Path of the Rego rule head
to evaluate, relative to the OPA data document. For a rule named allow in a
policy declaring package authz.orders, this is authz\/orders\/allow. The path
is taken from the endpoint only: i [...]
- "allowKey": { "index": 1, "kind": "parameter", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain [...]
+ "allowKey": { "index": 1, "kind": "parameter", "displayName": "Allow Key",
"group": "producer", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "allow", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "The key to read the allow\/deny verdict from
when the policy returns an object rather than a plain [...]
"includeBody": { "index": 2, "kind": "parameter", "displayName": "Include
Body", "group": "producer", "label": "", "required": false, "type": "boolean",
"javaType": "boolean", "deprecated": false, "autowired": false, "secret":
false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Whether to send the message body to OPA as
part of the input document. Disabled by default: bodies can [...]
"includeHeaders": { "index": 3, "kind": "parameter", "displayName":
"Include Headers", "group": "producer", "label": "", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "*", "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of message header names
to send to OPA in the input document. The defa [...]
"includeProperties": { "index": 4, "kind": "parameter", "displayName":
"Include Properties", "group": "producer", "label": "", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.opa.OpaConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of exchange property
names to send to OPA in the input document, or {code } for all o [...]
diff --git a/components/camel-opa/src/main/docs/opa-component.adoc
b/components/camel-opa/src/main/docs/opa-component.adoc
index eaf35efdf89d..b39ac054b340 100644
--- a/components/camel-opa/src/main/docs/opa-component.adoc
+++ b/components/camel-opa/src/main/docs/opa-component.adoc
@@ -151,9 +151,11 @@ boolean verdict read out of it:
* a decision that *is* a boolean is the verdict;
* a decision that is an object is searched for the `allowKey` entry (`allow`
by default), which must itself be a
- boolean;
+ boolean. `allowKey` accepts a dotted path, so `allowKey=result.allow` reads
a verdict nested inside the document
+ as `{"result": {"allow": true}}`;
* anything else cannot be read as a verdict and counts as a deny, with the raw
document still available for the
- route to inspect.
+ route to inspect. That case is logged at WARN rather than DEBUG: a document
that arrived but could not be read is
+ a configuration problem, and from `CamelOpaDecisionAllow` alone it is
indistinguishable from a genuine denial.
Both headers are written on every evaluation, so a verdict set by an inbound
message never survives into the
route.
diff --git
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
index d1389c4bd7c1..69741c1e84c6 100644
---
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
+++
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConfiguration.java
@@ -66,6 +66,9 @@ public class OpaConfiguration implements Cloneable {
/**
* The key to read the allow/deny verdict from when the policy returns an
object rather than a plain boolean. For a
* policy returning <code>{"allow": true, "reasons": []}</code> the
default value of {@code allow} is what you want.
+ * <p/>
+ * A dotted path reaches a verdict nested inside the document: {@code
allowKey=result.allow} reads
+ * <code>{"result": {"allow": true}}</code>. A key with no dot is looked
up directly at the top level.
*/
public String getAllowKey() {
return allowKey;
diff --git
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
index 9c4523519e33..87c2f6867c91 100644
---
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
+++
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicBoolean;
import com.styra.opa.OPAClient;
import org.apache.camel.Exchange;
@@ -56,6 +57,7 @@ public class OpaPolicyEvaluator {
private final Set<String> includedProperties;
private final boolean includeBody;
private final boolean failOpen;
+ private final AtomicBoolean unreadableVerdictWarned = new AtomicBoolean();
public OpaPolicyEvaluator(OPAClient client, String policyPath, String
allowKey, String includeHeaders,
String includeProperties, boolean includeBody,
boolean failOpen) {
@@ -176,14 +178,48 @@ public class OpaPolicyEvaluator {
if (decision instanceof Boolean b) {
return b;
}
- if (decision instanceof Map<?, ?> map && map.get(allowKey) instanceof
Boolean b) {
+ if (readVerdict(decision) instanceof Boolean b) {
return b;
}
- LOG.debug("Policy {} returned a decision with no boolean '{}' verdict,
denying. Decision: {}",
- policyPath, allowKey, decision);
+ // a decision document we cannot read a verdict from is a
configuration problem, not a routine deny, and the
+ // route cannot tell the two apart from the verdict header alone - so
say so at WARN. Once only: a policy
+ // written as deny[msg] without a `default allow := false` reaches
this on every legitimate deny, which on a
+ // busy route would be a log flood carrying a whole decision document
per message.
+ if (unreadableVerdictWarned.compareAndSet(false, true)) {
+ LOG.warn("Policy {} returned a decision with no boolean '{}'
verdict, denying. Check that allowKey"
+ + " matches the shape the policy returns; the raw
document is on the {} header. Logged once"
+ + " per evaluator - later occurrences are at DEBUG.",
+ policyPath, allowKey, OpaConstants.DECISION);
+ }
+ LOG.debug("Policy {} returned no boolean '{}' verdict, denying.
Decision: {}", policyPath, allowKey, decision);
return false;
}
+ /**
+ * Reads {@code allowKey} out of the decision document, walking a dotted
path so a verdict nested inside the result
+ * - {@code allowKey=result.allow} against <code>{"result": {"allow":
true}}</code> - can be reached. A key with no
+ * dot is looked up directly, exactly as before.
+ */
+ private Object readVerdict(Object decision) {
+ if (!(decision instanceof Map<?, ?> top)) {
+ return null;
+ }
+ // a top-level entry under the whole key wins over walking it as a
path, so an allowKey that itself
+ // contains a dot resolves exactly as it did before dotted paths were
understood
+ Object direct = top.get(allowKey);
+ if (direct != null) {
+ return direct;
+ }
+ Object current = decision;
+ for (String segment : allowKey.split("\\.", -1)) {
+ if (!(current instanceof Map<?, ?> map)) {
+ return null;
+ }
+ current = map.get(segment);
+ }
+ return current;
+ }
+
private static void clearDecisionHeaders(Exchange exchange) {
Message message = exchange.getMessage();
message.removeHeader(OpaConstants.DECISION_ALLOW);
diff --git
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaProducerTest.java
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaProducerTest.java
index 5ca1a6d008e6..6127fe65fbf2 100644
---
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaProducerTest.java
+++
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaProducerTest.java
@@ -90,6 +90,50 @@ class OpaProducerTest extends CamelTestSupport {
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
}
+ @Test
+ void readsAVerdictNestedInsideTheDecisionDocument() throws Exception {
+ givenDecision(Map.of("result", Map.of("allow", true)));
+
+ Exchange out = template.request(ENDPOINT + "&allowKey=result.allow", e
-> {
+ });
+
+
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+ }
+
+ @Test
+ void prefersATopLevelKeyThatItselfContainsADot() throws Exception {
+ // walking "com.acme.allow" as a path would miss a document that has
it as one key. Rego rule names cannot
+ // contain a dot, but a decision document is arbitrary JSON and may
well come from elsewhere.
+ givenDecision(Map.of("com.acme.allow", true, "com", Map.of("acme",
Map.of("allow", false))));
+
+ Exchange out = template.request(ENDPOINT + "&allowKey=com.acme.allow",
e -> {
+ });
+
+
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+ }
+
+ @Test
+ void deniesWhenADottedPathDoesNotResolve() throws Exception {
+ givenDecision(Map.of("result", Map.of("permitted", true)));
+
+ Exchange out = template.request(ENDPOINT + "&allowKey=result.allow", e
-> {
+ });
+
+ // fail closed, and the raw document stays available so the
misconfiguration can be seen
+
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(false);
+
assertThat(out.getMessage().getHeader(OpaConstants.DECISION)).isNotNull();
+ }
+
+ @Test
+ void deniesWhenADottedPathRunsPastANonMap() throws Exception {
+ givenDecision(Map.of("result", "not-a-map"));
+
+ Exchange out = template.request(ENDPOINT + "&allowKey=result.allow", e
-> {
+ });
+
+
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(false);
+ }
+
@Test
void deniesWhenTheDecisionObjectHasNoVerdictButKeepsTheRawDocument()
throws Exception {
Map<String, Object> decision = Map.of("deny", List.of("not an owner"));
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
index df6c08ed7cd4..5a9f8d8e5337 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpaComponentBuilderFactory.java
@@ -56,7 +56,10 @@ public interface OpaComponentBuilderFactory {
/**
* The key to read the allow/deny verdict from when the policy returns
* an object rather than a plain boolean. For a policy returning
{allow:
- * true, reasons: } the default value of allow is what you want.
+ * true, reasons: } the default value of allow is what you want. A
+ * dotted path reaches a verdict nested inside the document: {code
+ * allowKey=result.allow} reads {result: {allow: true}}. A key with no
+ * dot is looked up directly at the top level.
*
* The option is a: <code>java.lang.String</code> type.
*
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
index 65c075956b32..6c99e0c240b0 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
@@ -48,7 +48,10 @@ public interface OpaEndpointBuilderFactory {
/**
* The key to read the allow/deny verdict from when the policy returns
* an object rather than a plain boolean. For a policy returning
{allow:
- * true, reasons: } the default value of allow is what you want.
+ * true, reasons: } the default value of allow is what you want. A
+ * dotted path reaches a verdict nested inside the document: {code
+ * allowKey=result.allow} reads {result: {allow: true}}. A key with no
+ * dot is looked up directly at the top level.
*
* The option is a: <code>java.lang.String</code> type.
*