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


##########
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) {
+                result = error(current.revision(), 
"INPUT_OR_EVALUATION_ERROR");
+            }
+        }
+        exchange.setProperty(CedarConstants.AUTHORIZATION_RESULT, result);
+        return result;
+    }
+
+    private Entities entities(Snapshot current, Exchange exchange) throws 
Exception {
+        Set<Entity> entities = new HashSet<>(current.entities().getEntities());
+        if (configuration.getEntityProvider() != null) {
+            String json = configuration.getEntityProvider().entities(exchange);
+            if (json == null || json.getBytes(StandardCharsets.UTF_8).length > 
configuration.getMaxResourceSize()) {
+                throw new IllegalArgumentException("Invalid provider entity 
data");
+            }
+            Set<EntityUID> ids = new HashSet<>();
+            entities.forEach(entity -> ids.add(entity.getEUID()));
+            for (Entity entity : Entities.parse(json).getEntities()) {
+                if (!ids.add(entity.getEUID())) {
+                    throw new IllegalArgumentException("Duplicate entity UID");
+                }
+                entities.add(entity);
+            }
+        }
+        Entities requestEntities = new Entities(entities);
+        engine.validateEntities(new EntityValidationRequest(current.schema(), 
new ArrayList<>(entities)));
+        return requestEntities;
+    }
+
+    private static EntityUID uid(Expression expression, Exchange exchange) {
+        Object value = expression.evaluate(exchange, Object.class);
+        if (value instanceof EntityUID uid) {
+            return uid;
+        }
+        if (value instanceof String text) {

Review Comment:
   `orElseThrow` now carries a message: `an entity expression yielded text that 
is not an entity UID`. The text itself is left out of the message, as it is 
request input that must not reach the logs (the design's diagnostics rule); the 
`WARN` line of `evaluate()` reports that reason. 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