gnodet commented on code in PR #26165: URL: https://github.com/apache/camel/pull/26165#discussion_r3949846314
########## components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaEndpoint.java: ########## @@ -0,0 +1,107 @@ +/* + * 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; + +import com.styra.opa.OPAClient; +import org.apache.camel.Category; +import org.apache.camel.Component; +import org.apache.camel.Consumer; +import org.apache.camel.Processor; +import org.apache.camel.Producer; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.UriEndpoint; +import org.apache.camel.spi.UriParam; +import org.apache.camel.spi.UriPath; +import org.apache.camel.support.DefaultEndpoint; + +/** + * Evaluate Open Policy Agent (Rego) policies against an Exchange and record the allow/deny decision on it. + */ +@UriEndpoint(firstVersion = "4.23.0", scheme = "opa", title = "OPA", + syntax = "opa:policyPath", producerOnly = true, category = { Category.SECURITY }, + headersClass = OpaConstants.class) +public class OpaEndpoint extends DefaultEndpoint { + + @UriPath(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: it is deliberately not overridable by a" + + " message header, so that an inbound message cannot select which policy judges it.") + @Metadata(required = true) + private String policyPath; + + @UriParam + private OpaConfiguration configuration; + + private OPAClient opaClient; + private volatile OpaPolicyEvaluator evaluator; Review Comment: 💡 The `evaluator` field is `volatile` here and in `OpaSecurityPolicy`, which ensures visibility. Note that `doStop()` sets this to `null` before calling `super.doStop()` — in theory a racing `process()` call could see `null` and NPE. In practice this is safe because Camel's lifecycle stops the producer (draining in-flight exchanges) before stopping the endpoint. Just worth being aware of if the ordering ever changes. ########## components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java: ########## @@ -0,0 +1,197 @@ +/* + * 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; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import com.styra.opa.OPAClient; +import org.apache.camel.Exchange; +import org.apache.camel.util.ObjectHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Evaluates a Rego policy hosted by an OPA server against an {@link Exchange} and records the decision on it. + * <p/> + * Shared by the {@code opa:} producer and by {@code OpaSecurityPolicy} so that both build the same input document and + * read the verdict the same way. + */ +public class OpaPolicyEvaluator { + + private static final Logger LOG = LoggerFactory.getLogger(OpaPolicyEvaluator.class); + + private static final String ALL_HEADERS = "*"; + + private final OPAClient client; + private final String policyPath; + private final String allowKey; + private final Set<String> includedHeaders; + private final boolean includeBody; + private final boolean failOpen; + + public OpaPolicyEvaluator(OPAClient client, String policyPath, String allowKey, String includeHeaders, + boolean includeBody, boolean failOpen) { + this.client = ObjectHelper.notNull(client, "client"); + this.policyPath = ObjectHelper.notNull(policyPath, "policyPath"); + this.allowKey = ObjectHelper.isNotEmpty(allowKey) ? allowKey : "allow"; + this.includedHeaders = parseIncludedHeaders(includeHeaders); + this.includeBody = includeBody; + this.failOpen = failOpen; + } + + /** + * Creates a client for an OPA server, optionally authenticating with a bearer token. + * + * @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 + */ + public static OPAClient createClient(String serverUrl, String bearerToken) { + if (ObjectHelper.isNotEmpty(bearerToken)) { + return new OPAClient(serverUrl, Map.of("Authorization", "Bearer " + bearerToken)); + } + return new OPAClient(serverUrl); + } + + /** + * Evaluates the policy for the given exchange and sets the decision headers on it. + * + * @param exchange the exchange to build the OPA input document from + * @return true when the policy allows the exchange to proceed + * @throws OpaPolicyEvaluationException when the policy could not be evaluated and {@code failOpen} is false + */ + public boolean evaluate(Exchange exchange) throws OpaPolicyEvaluationException { + Object decision; + try { + decision = client.evaluate(policyPath, buildInput(exchange), Object.class); + } catch (Exception e) { + // any failure to reach a verdict is handled the same way, whether it comes from the OPA server + // (OPAException) or from building and serializing the input document; fail-closed must not depend + // on which layer gave up + if (failOpen) { + LOG.warn("Policy {} could not be evaluated, allowing the exchange to proceed because failOpen is" + + " enabled. Reason: {}", + policyPath, e.getMessage()); + setDecisionHeaders(exchange, null, true); + return true; + } + throw new OpaPolicyEvaluationException( + "Failed to evaluate policy " + policyPath + " at the OPA server", exchange, e); + } + + boolean allowed = isAllowed(decision); + setDecisionHeaders(exchange, decision, allowed); + return allowed; + } + + /** + * Builds the {@code input} document handed to OPA. + */ + protected Map<String, Object> buildInput(Exchange exchange) { + Map<String, Object> input = new LinkedHashMap<>(); + Map<String, Object> headers = new LinkedHashMap<>(); + for (Map.Entry<String, Object> entry : exchange.getMessage().getHeaders().entrySet()) { + String name = entry.getKey(); + // never feed our own decision headers back in: a policy must not be able to read a verdict Review Comment: 💡 `toJsonSafe` passes `Map` and `List` through as-is. If a header value is a `Map` containing non-JSON-native nested values (e.g., a `Map<String, Date>`), those nested values will be serialized by the OPA SDK and may produce unexpected JSON. This is fine for the common case (headers are mostly strings/numbers), but worth a note in the Javadoc if you want to be precise about the contract. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
