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 5220998ad575 CAMEL-24736: camel-opa - add a readiness check for the
OpaSecurityPolicy (#26436)
5220998ad575 is described below
commit 5220998ad5750c2bd25900033106427d403184a8
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Sep 16 12:45:24 2026 +0200
CAMEL-24736: camel-opa - add a readiness check for the OpaSecurityPolicy
(#26436)
CAMEL-24644 gave the opa: producer a health check but left OpaSecurityPolicy
without one, which is the wrong way round: a producer that cannot reach OPA
records a deny verdict the route can inspect, while the policy throws
CamelAuthorizationException and stops the exchange outright. The path that
hard-fails every message had no health signal at all.
A policy is a bean with no producer, so it registers through
HealthCheckRegistry.get(route.getCamelContext()) from beforeWrap, guarded to
fire once however many routes it wraps and only for a client it built itself
from serverUrl. Both probes now share OpaHealthProbe rather than being two
near-identical copies that could drift apart in security-relevant code.
healthCheckEnabled (default true) turns it off for a route that should stay
ready regardless. The shared probe also stops building //health from a
serverUrl ending in a slash, which OPA answers with a redirect the client
does
not follow - reporting a healthy server DOWN.
Co-authored-by: Claude Opus 5 <[email protected]>
---
.../apache/camel/catalog/docs/opa-component.adoc | 12 +-
.../camel-opa/src/main/docs/opa-component.adoc | 12 +-
...roducerHealthCheck.java => OpaHealthProbe.java} | 53 ++++---
.../component/opa/OpaProducerHealthCheck.java | 45 +-----
.../component/opa/security/OpaSecurityPolicy.java | 45 ++++++
.../opa/security/OpaSecurityPolicyHealthCheck.java | 53 +++++++
.../security/OpaSecurityPolicyHealthCheckTest.java | 175 +++++++++++++++++++++
7 files changed, 322 insertions(+), 73 deletions(-)
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 52f995677920..4df6be0ac77e 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
@@ -304,8 +304,16 @@ component with `healthCheckProducerEnabled`. The check
reports DOWN with the und
server and a server answering its health endpoint with an error are reported
differently, so a deny is never
confused with an outage.
-The check is only registered when the endpoint was given a `serverUrl`. An
injected `opaClient` may point anywhere
-and the endpoint has no way to ask it where, so no probe is registered in that
case.
+`OpaSecurityPolicy` registers an equivalent check, under an id starting
`security-policy:opa-`. It is arguably the
+more important of the two: a denied producer merely records a verdict the
route can inspect, while the policy throws
+`CamelAuthorizationException` and stops the exchange, so an unreachable server
there fails every message outright.
+That is why the policy's check is on by default rather than opt-in like the
producer's. Set
+`healthCheckEnabled=false` on the policy for a route that should stay ready
regardless — one running `failOpen`, say
+— in preference to hiding the check with `camel.health.exclude-pattern`.
+
+Neither check is registered when an `opaClient` was injected: that client may
point anywhere and neither the
+endpoint nor the policy has a way to ask it where, so probing the configured
`serverUrl` would report on a server
+they may never talk to. The endpoint check is also skipped when no `serverUrl`
was given.
== Security notes
diff --git a/components/camel-opa/src/main/docs/opa-component.adoc
b/components/camel-opa/src/main/docs/opa-component.adoc
index 52f995677920..4df6be0ac77e 100644
--- a/components/camel-opa/src/main/docs/opa-component.adoc
+++ b/components/camel-opa/src/main/docs/opa-component.adoc
@@ -304,8 +304,16 @@ component with `healthCheckProducerEnabled`. The check
reports DOWN with the und
server and a server answering its health endpoint with an error are reported
differently, so a deny is never
confused with an outage.
-The check is only registered when the endpoint was given a `serverUrl`. An
injected `opaClient` may point anywhere
-and the endpoint has no way to ask it where, so no probe is registered in that
case.
+`OpaSecurityPolicy` registers an equivalent check, under an id starting
`security-policy:opa-`. It is arguably the
+more important of the two: a denied producer merely records a verdict the
route can inspect, while the policy throws
+`CamelAuthorizationException` and stops the exchange, so an unreachable server
there fails every message outright.
+That is why the policy's check is on by default rather than opt-in like the
producer's. Set
+`healthCheckEnabled=false` on the policy for a route that should stay ready
regardless — one running `failOpen`, say
+— in preference to hiding the check with `camel.health.exclude-pattern`.
+
+Neither check is registered when an `opaClient` was injected: that client may
point anywhere and neither the
+endpoint nor the policy has a way to ask it where, so probing the configured
`serverUrl` would report on a server
+they may never talk to. The endpoint check is also skipped when no `serverUrl`
was given.
== Security notes
diff --git
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaHealthProbe.java
similarity index 58%
copy from
components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
copy to
components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaHealthProbe.java
index 31d754769dfd..b354052c0154 100644
---
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
+++
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaHealthProbe.java
@@ -21,50 +21,53 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
-import java.util.Map;
import org.apache.camel.health.HealthCheckResultBuilder;
-import org.apache.camel.impl.health.AbstractHealthCheck;
+import org.apache.camel.util.FileUtil;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.URISupport;
/**
- * Readiness check for the OPA server a producer sends its decisions to.
+ * The OPA readiness probe, shared by the producer health check and the one on
{@code OpaSecurityPolicy}.
* <p/>
- * The component fails closed, so an OPA server that cannot be reached fails
every exchange through the route. This
- * check probes the server's {@code /health} endpoint so that an unavailable
policy decision point is visible before
- * traffic starts failing, rather than only in the error logs afterwards.
+ * Both ask the same question of the same endpoint, so the probe lives in one
place: a fix here - a changed timeout, a
+ * new failure mode to report - applies to both rather than to whichever was
remembered.
*/
-public class OpaProducerHealthCheck extends AbstractHealthCheck {
+public final class OpaHealthProbe {
private static final Duration TIMEOUT = Duration.ofSeconds(5);
- // java.net.http.HttpClient only became AutoCloseable in Java 21 (JEP
480); on Camel's Java 17 baseline there
- // is no way to shut down its internal executor/selector threads, so a
per-instance client would leak a thread
- // pool on every producer start. Share a single client across all checks -
the per-request URL and bearer token
- // are set on the HttpRequest, so nothing endpoint-specific needs to live
on the client.
+ /**
+ * Shared across every OPA health check in the JVM. {@link HttpClient}
only became {@link AutoCloseable} in Java 21,
+ * so on the Java 17 baseline one client per check would leak its selector
thread with no way to shut it down. The
+ * client is immutable and thread-safe, so sharing is free; the
per-request URL and token live on the
+ * {@link HttpRequest}.
+ */
private static final HttpClient HTTP_CLIENT =
HttpClient.newBuilder().connectTimeout(TIMEOUT).build();
- private final String serverUrl;
- private final String bearerToken;
- private final String policyPath;
-
- public OpaProducerHealthCheck(String serverUrl, String bearerToken, String
policyPath, String id) {
- // the id is built from the endpoint URI so that two endpoints sharing
a policy path stay distinct, but that
- // URI carries the bearerToken in the clear and the id is published in
the health output, so sanitize it
- super("camel", "producer:opa-" + URISupport.sanitizeUri(id));
- this.serverUrl = serverUrl;
- this.bearerToken = bearerToken;
- this.policyPath = policyPath;
+ private OpaHealthProbe() {
}
- @Override
- protected void doCall(HealthCheckResultBuilder builder, Map<String,
Object> options) {
+ /**
+ * Probes the OPA server's health endpoint and records the outcome on the
builder.
+ * <p/>
+ * An unreachable server and a server answering with an error are reported
differently, so an outage is never
+ * mistaken for a policy that denied.
+ *
+ * @param builder the result to populate
+ * @param serverUrl base URL of the OPA server, without the /v1/data
suffix
+ * @param bearerToken token for OPA API authentication, or null when OPA
does not require one
+ * @param policyPath the policy this check is reporting for, recorded as
a detail
+ */
+ public static void probe(
+ HealthCheckResultBuilder builder, String serverUrl, String
bearerToken, String policyPath) {
builder.detail("opa.serverUrl", URISupport.sanitizeUri(serverUrl));
builder.detail("opa.policyPath", policyPath);
HttpRequest.Builder request = HttpRequest.newBuilder()
- .uri(URI.create(serverUrl + "/health"))
+ // a serverUrl with a trailing slash would build //health,
which OPA's router answers with a
+ // redirect the client is not configured to follow - reporting
a healthy server DOWN on HTTP 301
+ .uri(URI.create(FileUtil.stripTrailingSeparator(serverUrl) +
"/health"))
.timeout(TIMEOUT)
.GET();
if (ObjectHelper.isNotEmpty(bearerToken)) {
diff --git
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
index 31d754769dfd..d91946850d99 100644
---
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
+++
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaProducerHealthCheck.java
@@ -16,16 +16,10 @@
*/
package org.apache.camel.component.opa;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.time.Duration;
import java.util.Map;
import org.apache.camel.health.HealthCheckResultBuilder;
import org.apache.camel.impl.health.AbstractHealthCheck;
-import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.URISupport;
/**
@@ -37,14 +31,6 @@ import org.apache.camel.util.URISupport;
*/
public class OpaProducerHealthCheck extends AbstractHealthCheck {
- private static final Duration TIMEOUT = Duration.ofSeconds(5);
-
- // java.net.http.HttpClient only became AutoCloseable in Java 21 (JEP
480); on Camel's Java 17 baseline there
- // is no way to shut down its internal executor/selector threads, so a
per-instance client would leak a thread
- // pool on every producer start. Share a single client across all checks -
the per-request URL and bearer token
- // are set on the HttpRequest, so nothing endpoint-specific needs to live
on the client.
- private static final HttpClient HTTP_CLIENT =
HttpClient.newBuilder().connectTimeout(TIMEOUT).build();
-
private final String serverUrl;
private final String bearerToken;
private final String policyPath;
@@ -60,35 +46,6 @@ public class OpaProducerHealthCheck extends
AbstractHealthCheck {
@Override
protected void doCall(HealthCheckResultBuilder builder, Map<String,
Object> options) {
- builder.detail("opa.serverUrl", URISupport.sanitizeUri(serverUrl));
- builder.detail("opa.policyPath", policyPath);
-
- HttpRequest.Builder request = HttpRequest.newBuilder()
- .uri(URI.create(serverUrl + "/health"))
- .timeout(TIMEOUT)
- .GET();
- if (ObjectHelper.isNotEmpty(bearerToken)) {
- request.header("Authorization", "Bearer " + bearerToken);
- }
-
- try {
- HttpResponse<Void> response = HTTP_CLIENT.send(request.build(),
HttpResponse.BodyHandlers.discarding());
- if (response.statusCode() == 200) {
- builder.up();
- } else {
- builder.down();
- builder.message("OPA server answered its health endpoint with
HTTP " + response.statusCode());
- builder.detail("opa.statusCode", response.statusCode());
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- builder.down();
- builder.message("Interrupted while checking the OPA server");
- builder.error(e);
- } catch (Exception e) {
- builder.down();
- builder.message("Cannot reach the OPA server: " + e.getMessage());
- builder.error(e);
- }
+ OpaHealthProbe.probe(builder, serverUrl, bearerToken, policyPath);
}
}
diff --git
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
index d26ef07ab575..3c3a60c907a1 100644
---
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
+++
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicy.java
@@ -22,7 +22,9 @@ import org.apache.camel.Processor;
import org.apache.camel.Route;
import org.apache.camel.component.opa.OpaPolicyEvaluator;
import org.apache.camel.component.opa.OpaRestEvaluator;
+import org.apache.camel.health.HealthCheckRegistry;
import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,7 +58,11 @@ public class OpaSecurityPolicy implements
AuthorizationPolicy {
private boolean failOpen;
private OPAClient opaClient;
+ private boolean healthCheckEnabled = true;
+
private volatile OpaPolicyEvaluator evaluator;
+ private volatile OpaSecurityPolicyHealthCheck healthCheck;
+ private volatile boolean ownsClient;
public OpaSecurityPolicy() {
}
@@ -71,11 +77,36 @@ public class OpaSecurityPolicy implements
AuthorizationPolicy {
if (evaluator == null) {
StringHelper.notEmpty(policyPath, "policyPath", this);
if (opaClient == null) {
+ // createClient moved to OpaRestEvaluator when the evaluator
became an abstract base
opaClient = OpaRestEvaluator.createClient(serverUrl,
bearerToken);
+ ownsClient = true;
}
evaluator = new OpaRestEvaluator(
opaClient, policyPath, allowKey, includeHeaders,
includeProperties, includeBody, failOpen);
}
+ // after validation, so a policy that is missing its policyPath fails
without leaving a ".../null" check
+ // behind in the registry
+ registerHealthCheck(route);
+ }
+
+ /**
+ * Registers a readiness check for the OPA server, once per policy however
many routes it wraps.
+ * <p/>
+ * Only for a client this policy built itself from {@code serverUrl}. An
injected {@code opaClient} can point
+ * anywhere and this policy has no way to ask it where, so probing the
configured URL would report on a server it
+ * may never talk to - hence {@code ownsClient} rather than a null check
on {@code opaClient}, which by the time
+ * this runs is set either way.
+ */
+ private void registerHealthCheck(Route route) {
+ if (!healthCheckEnabled || healthCheck != null || !ownsClient ||
ObjectHelper.isEmpty(serverUrl)) {
+ return;
+ }
+ HealthCheckRegistry registry =
HealthCheckRegistry.get(route.getCamelContext());
+ if (registry == null) {
+ return;
+ }
+ healthCheck = new OpaSecurityPolicyHealthCheck(serverUrl, bearerToken,
policyPath);
+ registry.register(healthCheck);
}
@Override
@@ -180,6 +211,20 @@ public class OpaSecurityPolicy implements
AuthorizationPolicy {
this.failOpen = failOpen;
}
+ public boolean isHealthCheckEnabled() {
+ return healthCheckEnabled;
+ }
+
+ /**
+ * Whether to register a readiness check for the OPA server this policy
queries. Enabled by default: the policy
+ * denies every exchange it guards while the server is unreachable, so a
route that is up but cannot reach OPA is
+ * not ready. Disable it for a policy whose route should stay ready
regardless - for example one wrapped in
+ * {@code failOpen} - rather than excluding the check by pattern.
+ */
+ public void setHealthCheckEnabled(boolean healthCheckEnabled) {
+ this.healthCheckEnabled = healthCheckEnabled;
+ }
+
public OPAClient getOpaClient() {
return opaClient;
}
diff --git
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheck.java
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheck.java
new file mode 100644
index 000000000000..75e4aad3a7cf
--- /dev/null
+++
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheck.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.opa.security;
+
+import java.util.Map;
+
+import org.apache.camel.component.opa.OpaHealthProbe;
+import org.apache.camel.health.HealthCheckResultBuilder;
+import org.apache.camel.impl.health.AbstractHealthCheck;
+import org.apache.camel.util.URISupport;
+
+/**
+ * Readiness check for the OPA server behind an {@link OpaSecurityPolicy}.
+ * <p/>
+ * The policy is the stricter of the component's two paths: a denied producer
merely records a verdict the route can
+ * inspect, while this one throws {@link
org.apache.camel.CamelAuthorizationException} and stops the exchange. So an
+ * unreachable server here fails every message outright, which is exactly the
condition worth surfacing before traffic
+ * arrives rather than after.
+ */
+public class OpaSecurityPolicyHealthCheck extends AbstractHealthCheck {
+
+ private final String serverUrl;
+ private final String bearerToken;
+ private final String policyPath;
+
+ public OpaSecurityPolicyHealthCheck(String serverUrl, String bearerToken,
String policyPath) {
+ // serverUrl and policyPath together identify the decision this policy
enforces, so two policies pointing at
+ // different servers stay distinct; sanitized because the id is
published in the health output
+ super("camel", "security-policy:opa-" +
URISupport.sanitizeUri(serverUrl + "/" + policyPath));
+ this.serverUrl = serverUrl;
+ this.bearerToken = bearerToken;
+ this.policyPath = policyPath;
+ }
+
+ @Override
+ protected void doCall(HealthCheckResultBuilder builder, Map<String,
Object> options) {
+ OpaHealthProbe.probe(builder, serverUrl, bearerToken, policyPath);
+ }
+}
diff --git
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheckTest.java
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheckTest.java
new file mode 100644
index 000000000000..123f9fe90a0c
--- /dev/null
+++
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/security/OpaSecurityPolicyHealthCheckTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.opa.security;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.util.List;
+import java.util.Map;
+
+import com.styra.opa.OPAClient;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.health.HealthCheck;
+import org.apache.camel.health.HealthCheckRegistry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+/**
+ * The security policy hard-fails every exchange when OPA is unreachable, so
it needs the same readiness signal the
+ * producer got.
+ */
+public class OpaSecurityPolicyHealthCheckTest extends CamelTestSupport {
+
+ private static final String TOKEN = "s3cr3t-token";
+
+ private static HttpServer server;
+ private static String serverUrl;
+
+ private final OpaSecurityPolicy policy = new OpaSecurityPolicy();
+
+ @AfterEach
+ void stopServer() {
+ if (server != null) {
+ server.stop(0);
+ server = null;
+ }
+ }
+
+ private static String startHealthyServer() throws IOException {
+ server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+ server.createContext("/health", exchange -> {
+ exchange.sendResponseHeaders(200, -1);
+ try (OutputStream out = exchange.getResponseBody()) {
+ out.flush();
+ }
+ });
+ server.start();
+ return "http://localhost:" + server.getAddress().getPort();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() throws Exception {
+ serverUrl = startHealthyServer();
+ policy.setPolicyPath("authz/allow");
+ policy.setServerUrl(serverUrl);
+ policy.setBearerToken(TOKEN);
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start").policy(policy).to("mock:result");
+ }
+ };
+ }
+
+ private List<HealthCheck> registered() {
+ HealthCheckRegistry registry = HealthCheckRegistry.get(context);
+ assertThat(registry).isNotNull();
+ return registry.stream()
+ .filter(hc -> hc.getId().startsWith("security-policy:opa-"))
+ .toList();
+ }
+
+ @Test
+ void registersAReadinessCheckForTheServerItEnforces() {
+ assertThat(registered()).hasSize(1);
+ HealthCheck check = registered().get(0);
+
+ HealthCheck.Result result = check.call(Map.of());
+ assertThat(result.getState()).isEqualTo(HealthCheck.State.UP);
+ assertThat(result.getDetails()).containsEntry("opa.policyPath",
"authz/allow");
+ }
+
+ @Test
+ void neverPublishesTheBearerTokenInTheCheckId() {
+ // the id reaches the health output, and serverUrl/policyPath are
enough to identify the decision
+ assertThat(registered().get(0).getId()).doesNotContain(TOKEN);
+ }
+
+ @Test
+ void toleratesATrailingSlashOnTheServerUrl() {
+ // concatenating "/health" onto a base that already ends in one builds
//health. OPA's router answers a
+ // non-canonical path with a redirect, and the probe's client does not
follow redirects, so a healthy
+ // server was reported DOWN. Every other test here uses a slash-free
URL, which is how it went unnoticed.
+ OpaSecurityPolicyHealthCheck check
+ = new OpaSecurityPolicyHealthCheck(serverUrl + "/", null,
"authz/allow");
+ check.setEnabled(true);
+
+ HealthCheck.Result result = check.call(Map.of());
+
+ assertThat(result.getState()).isEqualTo(HealthCheck.State.UP);
+ }
+
+ @Test
+ void reportsDownWhenTheServerCannotBeReached() {
+ OpaSecurityPolicyHealthCheck check
+ = new OpaSecurityPolicyHealthCheck("http://localhost:1", null,
"authz/allow");
+ check.setEnabled(true);
+
+ HealthCheck.Result result = check.call(Map.of());
+
+ assertThat(result.getState()).isEqualTo(HealthCheck.State.DOWN);
+ assertThat(result.getMessage()).get().asString().contains("Cannot
reach the OPA server");
+ }
+
+ @Test
+ void registersNothingWhenTheCheckIsDisabled() {
+ OpaSecurityPolicy disabled = new OpaSecurityPolicy();
+ disabled.setPolicyPath("authz/allow");
+ disabled.setServerUrl("http://unused:8181");
+ disabled.setHealthCheckEnabled(false);
+
+ disabled.beforeWrap(context.getRoutes().get(0), null);
+
+ // still only the one the route under test registered
+ assertThat(registered()).hasSize(1);
+ }
+
+ @Test
+ void registersNothingForAPolicyThatFailsValidation() {
+ // registration used to run before notEmpty(policyPath), which left a
".../null" check in the registry
+ // of a policy whose route then never started
+ OpaSecurityPolicy misconfigured = new OpaSecurityPolicy();
+ misconfigured.setServerUrl("http://unused:8181");
+
+ assertThatThrownBy(() ->
misconfigured.beforeWrap(context.getRoutes().get(0), null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("policyPath");
+
+ assertThat(registered()).hasSize(1);
+ assertThat(registered().get(0).getId()).doesNotContain("null");
+ }
+
+ @Test
+ void registersNothingWhenAnOpaClientWasInjected() {
+ OpaSecurityPolicy injected = new OpaSecurityPolicy();
+ injected.setPolicyPath("authz/allow");
+ injected.setServerUrl("http://unused:8181");
+ injected.setOpaClient(mock(OPAClient.class));
+
+ injected.beforeWrap(context.getRoutes().get(0), null);
+
+ // an injected client may point anywhere, so probing serverUrl would
report on the wrong server
+ assertThat(registered()).hasSize(1);
+ }
+}