oscerd commented on code in PR #29746:
URL: https://github.com/apache/camel/pull/29746#discussion_r4561575724


##########
components/camel-cedar/src/main/java/org/apache/camel/component/cedar/CedarEvaluator.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.cedar;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import com.cedarpolicy.AuthorizationEngine;
+import com.cedarpolicy.BasicAuthorizationEngine;
+import com.cedarpolicy.CedarJson;
+import com.cedarpolicy.model.AuthorizationRequest;
+import com.cedarpolicy.model.EntityValidationRequest;
+import com.cedarpolicy.model.ValidationRequest;
+import com.cedarpolicy.model.entity.Entities;
+import com.cedarpolicy.model.entity.Entity;
+import com.cedarpolicy.model.policy.PolicySet;
+import com.cedarpolicy.model.schema.Schema;
+import com.cedarpolicy.value.EntityUID;
+import com.cedarpolicy.value.Value;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+
+/** Owns parsed SDK objects privately. Every evaluation captures exactly one 
validated snapshot. */
+public final class CedarEvaluator extends ServiceSupport {

Review Comment:
   Added in `CAMEL-26847: Address review feedback - ...` (1fa62b19e8a1): 
`CedarEvaluator` has an SLF4J `LOG`; reload failures are logged at `WARN` with 
the step, the option and the exception class (or the evaluator's own finding), 
and evaluation failures with the exception class. SDK messages are not logged, 
as davsclaus's point 4 asks.
   
   _Claude Code on behalf of oscerd_



##########
components/camel-cedar/src/main/java/org/apache/camel/component/cedar/CedarEvaluator.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.cedar;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import com.cedarpolicy.AuthorizationEngine;
+import com.cedarpolicy.BasicAuthorizationEngine;
+import com.cedarpolicy.CedarJson;
+import com.cedarpolicy.model.AuthorizationRequest;
+import com.cedarpolicy.model.EntityValidationRequest;
+import com.cedarpolicy.model.ValidationRequest;
+import com.cedarpolicy.model.entity.Entities;
+import com.cedarpolicy.model.entity.Entity;
+import com.cedarpolicy.model.policy.PolicySet;
+import com.cedarpolicy.model.schema.Schema;
+import com.cedarpolicy.value.EntityUID;
+import com.cedarpolicy.value.Value;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+
+/** Owns parsed SDK objects privately. Every evaluation captures exactly one 
validated snapshot. */
+public final class CedarEvaluator extends ServiceSupport {
+    private static final TypeReference<Map<String, Value>> CONTEXT_TYPE = new 
TypeReference<>() {
+    };
+    private final CamelContext camelContext;
+    private final CedarConfiguration configuration;
+    private final Supplier<AuthorizationEngine> engineFactory;
+    private AuthorizationEngine engine;
+    private volatile Snapshot snapshot;
+
+    private record Snapshot(PolicySet policies, Schema schema, Entities 
entities, String revision) {
+    }
+
+    public CedarEvaluator(CamelContext camelContext, CedarConfiguration 
configuration) {
+        this(camelContext, configuration, BasicAuthorizationEngine::new);
+    }
+
+    CedarEvaluator(CamelContext camelContext, CedarConfiguration 
configuration, Supplier<AuthorizationEngine> engineFactory) {
+        this.camelContext = camelContext;
+        this.configuration = configuration;
+        this.engineFactory = engineFactory;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        if (configuration.getPrincipal() == null || configuration.getAction() 
== null || configuration.getResource() == null) {
+            throw new IllegalArgumentException("Cedar requires configured 
principal, action and resource expressions");
+        }
+        if (configuration.getPoliciesResource() == null || 
configuration.getPoliciesResource().isBlank()
+                || configuration.getSchemaResource() == null || 
configuration.getSchemaResource().isBlank()) {
+            throw new IllegalArgumentException("Cedar requires 
policiesResource and schemaResource");
+        }
+        if (configuration.getMaxResourceSize() < 1 || 
configuration.getMaxResourceSize() == Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cedar maxResourceSize must be 
between 1 and 2147483646 bytes");
+        }
+        configuration.getPrincipal().init(camelContext);
+        configuration.getAction().init(camelContext);
+        configuration.getResource().init(camelContext);
+        if (configuration.getContext() != null) {
+            configuration.getContext().init(camelContext);
+        }
+        try {
+            engine = engineFactory.get();
+        } catch (LinkageError | RuntimeException e) {
+            throw nativeUnavailable();
+        }
+        try {
+            reload();
+        } catch (LinkageError e) {
+            // Some JNI failures surface on the first validation call, after 
library construction succeeded.
+            throw nativeUnavailable();
+        }
+    }
+
+    private static IllegalStateException nativeUnavailable() {
+        return new IllegalStateException(
+                "Cedar Java 4.11.0 JNI initialization failed on "
+                                         + System.getProperty("os.name") + "/" 
+ System.getProperty("os.arch")
+                                         + ". Use a packaged JVM platform 
(tested: Linux amd64 glibc) and a writable executable native extraction 
location.");
+    }
+
+    @Override
+    protected synchronized void doStop() {
+        snapshot = null;
+        engine = null;
+    }
+
+    /** Reloads trusted configured resources; a failed candidate never 
replaces the active snapshot. */
+    public synchronized void reload() throws Exception {
+        if (engine == null) {
+            throw new IllegalStateException("Cedar evaluator is not started");
+        }
+        try {
+            String policiesText = read(configuration.getPoliciesResource());
+            String schemaText = read(configuration.getSchemaResource());
+            String entitiesText
+                    = configuration.getEntitiesResource() == null ? "[]" : 
read(configuration.getEntitiesResource());
+            Schema schema = Schema.parse(Schema.JsonOrCedar.Cedar, schemaText);
+            PolicySet policies = PolicySet.parsePolicies(policiesText);
+            if (!engine.validate(new ValidationRequest(schema, 
policies)).validationPassed()) {
+                throw new IllegalArgumentException("Policy schema validation 
failed");
+            }
+            Entities entities = Entities.parse(entitiesText);
+            engine.validateEntities(new EntityValidationRequest(schema, new 
ArrayList<>(entities.getEntities())));
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            for (String content : List.of(policiesText, schemaText, 
entitiesText)) {
+                byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
+                
digest.update(Integer.toString(bytes.length).getBytes(StandardCharsets.UTF_8));
+                digest.update((byte) 0);
+                digest.update(bytes);
+            }
+            snapshot = new Snapshot(policies, schema, entities, 
HexFormat.of().formatHex(digest.digest()));
+        } catch (Exception e) {
+            // SDK parse/validation messages may contain policy text or entity 
attributes. Do not retain them as causes.
+            throw new IllegalArgumentException(
+                    "Cedar resource loading or schema validation failed; 
previous revision retained");
+        }
+    }
+
+    public String getRevision() {
+        Snapshot current = snapshot;
+        return current == null ? null : current.revision();
+    }
+
+    private String read(String uri) throws Exception {
+        if (uri == null || uri.isBlank()) {
+            throw new IllegalArgumentException("Required Cedar resource is 
missing");
+        }
+        if (!uri.startsWith("classpath:") && !uri.startsWith("file:")) {
+            throw new IllegalArgumentException("Cedar resources require 
classpath: or file: URIs");
+        }
+        try (InputStream input = 
ResourceHelper.resolveMandatoryResourceAsInputStream(camelContext, uri)) {
+            byte[] bytes = input.readNBytes(configuration.getMaxResourceSize() 
+ 1);
+            if (bytes.length > configuration.getMaxResourceSize()) {
+                throw new IllegalArgumentException("Cedar resource too large");
+            }
+            return new String(bytes, StandardCharsets.UTF_8);
+        }
+    }
+
+    /** Always preserves the body. Operational and invalid-input failures are 
explicit ERROR results, never DENY. */
+    public CedarAuthorizationResult evaluate(Exchange exchange) {
+        exchange.removeProperty(CedarConstants.AUTHORIZATION_RESULT);
+        
exchange.getMessage().removeHeader(CedarConstants.AUTHORIZATION_RESULT);
+        Snapshot current = snapshot;
+        CedarAuthorizationResult result;
+        if (current == null) {
+            result = error(null, "NOT_STARTED");
+        } else {
+            try {
+                EntityUID principal = uid(configuration.getPrincipal(), 
exchange);
+                EntityUID action = uid(configuration.getAction(), exchange);
+                EntityUID resource = uid(configuration.getResource(), 
exchange);
+                Map<String, Value> context = Map.of();
+                if (configuration.getContext() != null) {
+                    Object value = 
configuration.getContext().evaluate(exchange, Object.class);
+                    if (value == null) {
+                        throw new IllegalArgumentException("Null Cedar 
context");
+                    }
+                    context = CedarJson.objectMapper().convertValue(value, 
CONTEXT_TYPE);
+                    if 
(CedarJson.objectMapper().writeValueAsBytes(context).length > 
configuration.getMaxResourceSize()) {
+                        throw new IllegalArgumentException("Cedar context too 
large");
+                    }
+                }
+                Entities entities = entities(current, exchange);
+                AuthorizationRequest request = new AuthorizationRequest(
+                        principal, action, resource,
+                        Optional.of(context), Optional.of(current.schema()), 
true);
+                var response = engine.isAuthorized(request, 
current.policies(), entities);
+                if (response.success.isEmpty()) {
+                    List<CedarAuthorizationResult.Diagnostic> diagnostics = 
response.errors
+                            .map(errors -> errors.stream().map(error -> new 
CedarAuthorizationResult.Diagnostic(
+                                    null, 
error.code.orElse("REQUEST_VALIDATION_OR_ENGINE_ERROR"))).toList())
+                            .orElse(List.of());
+                    if (diagnostics.isEmpty()) {
+                        result = error(current.revision(), 
"REQUEST_VALIDATION_OR_ENGINE_ERROR");
+                    } else {
+                        result = new CedarAuthorizationResult(
+                                CedarAuthorizationResult.Outcome.ERROR,
+                                CedarAuthorizationResult.Decision.UNKNOWN, 
Set.of(), diagnostics, current.revision());
+                    }
+                } else {
+                    var success = response.success.orElseThrow();
+                    List<CedarAuthorizationResult.Diagnostic> diagnostics = 
success.getErrors().stream()
+                            .map(e -> new CedarAuthorizationResult.Diagnostic(
+                                    e.getPolicyId(), 
e.getError().code.orElse("EVALUATION_ERROR")))
+                            .toList();
+                    var decision = success.isAllowed()
+                            ? CedarAuthorizationResult.Decision.ALLOW : 
CedarAuthorizationResult.Decision.DENY;
+                    var outcome = !diagnostics.isEmpty() && 
configuration.isStrict() ? CedarAuthorizationResult.Outcome.ERROR
+                            : success.isAllowed() ? 
CedarAuthorizationResult.Outcome.ALLOW
+                            : CedarAuthorizationResult.Outcome.DENY;
+                    result = new CedarAuthorizationResult(
+                            outcome, decision, success.getReason(), 
diagnostics, current.revision());
+                }
+            } catch (Exception e) {

Review Comment:
   The catch now logs `Cedar evaluation failed (<exception class>); the result 
is an ERROR outcome`, or `Cedar evaluation rejected its input (<reason>)` when 
the evaluator itself rejected the input (no entity UID from an expression, null 
or oversized context, invalid provider data). The exception message is left out 
on purpose: an SDK message can carry policy text or entity attributes 
(davsclaus's point 4). Applied in (1fa62b19e8a1).
   
   _Claude Code on behalf of oscerd_



##########
components/camel-cedar/src/main/java/org/apache/camel/component/cedar/CedarEvaluator.java:
##########
@@ -0,0 +1,270 @@
+/*
+ * 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.cedar;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import com.cedarpolicy.AuthorizationEngine;
+import com.cedarpolicy.BasicAuthorizationEngine;
+import com.cedarpolicy.CedarJson;
+import com.cedarpolicy.model.AuthorizationRequest;
+import com.cedarpolicy.model.EntityValidationRequest;
+import com.cedarpolicy.model.ValidationRequest;
+import com.cedarpolicy.model.entity.Entities;
+import com.cedarpolicy.model.entity.Entity;
+import com.cedarpolicy.model.policy.PolicySet;
+import com.cedarpolicy.model.schema.Schema;
+import com.cedarpolicy.value.EntityUID;
+import com.cedarpolicy.value.Value;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+
+/** Owns parsed SDK objects privately. Every evaluation captures exactly one 
validated snapshot. */
+public final class CedarEvaluator extends ServiceSupport {
+    private static final TypeReference<Map<String, Value>> CONTEXT_TYPE = new 
TypeReference<>() {
+    };
+    private final CamelContext camelContext;
+    private final CedarConfiguration configuration;
+    private final Supplier<AuthorizationEngine> engineFactory;
+    private AuthorizationEngine engine;
+    private volatile Snapshot snapshot;
+
+    private record Snapshot(PolicySet policies, Schema schema, Entities 
entities, String revision) {
+    }
+
+    public CedarEvaluator(CamelContext camelContext, CedarConfiguration 
configuration) {
+        this(camelContext, configuration, BasicAuthorizationEngine::new);
+    }
+
+    CedarEvaluator(CamelContext camelContext, CedarConfiguration 
configuration, Supplier<AuthorizationEngine> engineFactory) {
+        this.camelContext = camelContext;
+        this.configuration = configuration;
+        this.engineFactory = engineFactory;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        if (configuration.getPrincipal() == null || configuration.getAction() 
== null || configuration.getResource() == null) {
+            throw new IllegalArgumentException("Cedar requires configured 
principal, action and resource expressions");
+        }
+        if (configuration.getPoliciesResource() == null || 
configuration.getPoliciesResource().isBlank()
+                || configuration.getSchemaResource() == null || 
configuration.getSchemaResource().isBlank()) {
+            throw new IllegalArgumentException("Cedar requires 
policiesResource and schemaResource");
+        }
+        if (configuration.getMaxResourceSize() < 1 || 
configuration.getMaxResourceSize() == Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Cedar maxResourceSize must be 
between 1 and 2147483646 bytes");
+        }
+        configuration.getPrincipal().init(camelContext);
+        configuration.getAction().init(camelContext);
+        configuration.getResource().init(camelContext);
+        if (configuration.getContext() != null) {
+            configuration.getContext().init(camelContext);
+        }
+        try {
+            engine = engineFactory.get();
+        } catch (LinkageError | RuntimeException e) {
+            throw nativeUnavailable();
+        }
+        try {
+            reload();
+        } catch (LinkageError e) {
+            // Some JNI failures surface on the first validation call, after 
library construction succeeded.
+            throw nativeUnavailable();
+        }
+    }
+
+    private static IllegalStateException nativeUnavailable() {
+        return new IllegalStateException(
+                "Cedar Java 4.11.0 JNI initialization failed on "
+                                         + System.getProperty("os.name") + "/" 
+ System.getProperty("os.arch")
+                                         + ". Use a packaged JVM platform 
(tested: Linux amd64 glibc) and a writable executable native extraction 
location.");
+    }
+
+    @Override
+    protected synchronized void doStop() {
+        snapshot = null;
+        engine = null;
+    }
+
+    /** Reloads trusted configured resources; a failed candidate never 
replaces the active snapshot. */
+    public synchronized void reload() throws Exception {
+        if (engine == null) {
+            throw new IllegalStateException("Cedar evaluator is not started");
+        }
+        try {
+            String policiesText = read(configuration.getPoliciesResource());
+            String schemaText = read(configuration.getSchemaResource());
+            String entitiesText
+                    = configuration.getEntitiesResource() == null ? "[]" : 
read(configuration.getEntitiesResource());
+            Schema schema = Schema.parse(Schema.JsonOrCedar.Cedar, schemaText);
+            PolicySet policies = PolicySet.parsePolicies(policiesText);
+            if (!engine.validate(new ValidationRequest(schema, 
policies)).validationPassed()) {
+                throw new IllegalArgumentException("Policy schema validation 
failed");
+            }
+            Entities entities = Entities.parse(entitiesText);
+            engine.validateEntities(new EntityValidationRequest(schema, new 
ArrayList<>(entities.getEntities())));
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            for (String content : List.of(policiesText, schemaText, 
entitiesText)) {
+                byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
+                
digest.update(Integer.toString(bytes.length).getBytes(StandardCharsets.UTF_8));
+                digest.update((byte) 0);
+                digest.update(bytes);
+            }
+            snapshot = new Snapshot(policies, schema, entities, 
HexFormat.of().formatHex(digest.digest()));
+        } catch (Exception e) {
+            // SDK parse/validation messages may contain policy text or entity 
attributes. Do not retain them as causes.
+            throw new IllegalArgumentException(

Review Comment:
   The exception thrown by `reload()` now says which step and option failed and 
names the exception class, or the policy ids and error kinds of a failed 
validation, and the same line is logged at `WARN`. The cause stays stripped, 
since the SDK message is what may leak policy text; the class gives the 
machine-readable signal. Applied in (1fa62b19e8a1).
   
   _Claude Code on behalf of oscerd_



-- 
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]

Reply via email to