davsclaus commented on code in PR #27767:
URL: https://github.com/apache/camel/pull/27767#discussion_r4257436816


##########
components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpPrincipal.java:
##########
@@ -0,0 +1,100 @@
+/*
+ * 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.mcp.server.vertx;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.auth.User;
+
+/**
+ * Derives a runtime-neutral view — a caller name and role names — from a 
Vert.x {@link User}, so the MCP bridge can
+ * stamp a stable caller identity ({@code CamelMcpSecurityPrincipalName} / 
{@code CamelMcpSecurityPrincipalRoles}) that
+ * a tool route or its {@link org.apache.camel.spi.AuthorizationPolicy} can 
authorize on without depending on Vert.x.
+ */
+final class VertxMcpPrincipal {
+
+    private VertxMcpPrincipal() {
+    }
+
+    /**
+     * The caller name: the Vert.x {@link User#subject() subject} when set, 
else the {@code username} or {@code sub}
+     * claim of the principal, else {@code null}.
+     */
+    static String name(User user) {
+        if (user == null) {
+            return null;
+        }
+        String subject = user.subject();
+        if (isSet(subject)) {
+            return subject;
+        }
+        JsonObject principal = user.principal();
+        if (principal != null) {
+            String username = principal.getString("username");
+            if (isSet(username)) {
+                return username;
+            }
+            String sub = principal.getString("sub");
+            if (isSet(sub)) {
+                return sub;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * The caller roles, read from the principal's common role claims in 
order: a top-level {@code roles} array, the
+     * Keycloak {@code realm_access.roles} array, or a {@code groups} array. 
Returns {@code null} when the principal
+     * carries none, so the bridge does not stamp an empty roles property; 
blank and non-string entries are skipped.
+     */
+    static List<String> roles(User user) {
+        if (user == null) {
+            return null;
+        }
+        JsonObject principal = user.principal();
+        if (principal == null) {
+            return null;
+        }
+        JsonArray roles = principal.getJsonArray("roles");
+        if (roles == null) {
+            JsonObject realmAccess = principal.getJsonObject("realm_access");
+            if (realmAccess != null) {
+                roles = realmAccess.getJsonArray("roles");
+            }
+        }
+        if (roles == null) {
+            roles = principal.getJsonArray("groups");
+        }

Review Comment:
   `getJsonArray("roles")`, `getJsonObject("realm_access")` and 
`getJsonArray("groups")` cast the value, so any claim with a different type 
throws `ClassCastException`. Examples are a single-valued `"roles": "admin"` or 
`"groups": "devs"`, which some IdPs and custom mappers emit, or a non-object 
`realm_access`. The Vert.x JWT provider copies every non-reserved claim into 
`principal()`. So a valid, authenticated token in that shape makes every tool 
call fail in `toToolCallContext`, before the handler and any policy run. It 
fails closed, but it is hard to diagnose. Something like the following ignores 
(or adapts) unexpected shapes instead:
   
   ```java
   Object value = principal.getValue("roles");
   if (!(value instanceof JsonArray)) {
       Object ra = principal.getValue("realm_access");
       value = ra instanceof JsonObject jo ? jo.getValue("roles") : null;
   }
   if (!(value instanceof JsonArray)) {
       value = principal.getValue("groups");
   }
   if (!(value instanceof JsonArray roles) || roles.isEmpty()) {
       return null;
   }
   ```
   
   (`getValue` returns a `JsonArray`/`JsonObject` for nested lists/maps.) A 
unit test with a string-valued `roles` claim would cover it.



##########
components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc:
##########
@@ -444,10 +444,15 @@ Which runtimes carry the caller identity:
   <<The calling exchange>> above), so the identity property reaches the tool 
route.
 * Over the xref:others:mcp-server.adoc[MCP server] the authenticated transport 
caller is carried onto the tool
   exchange as the `CamelMcpSecurityPrincipal` property (the raw transport 
principal — for the Vert.x streamable HTTP
-  server, an `io.vertx.ext.auth.User`). A policy over MCP reads that property 
directly; Camel's shipped
-  identity/token policies (Keycloak, Spring Security, Shiro) do not read it 
yet, so MCP authorization needs a policy
-  that inspects `CamelMcpSecurityPrincipal`. Exposing a runtime-neutral 
principal name and roles for the shipped
-  policies is tracked as a follow-up.
+  server, an `io.vertx.ext.auth.User`). Alongside it the server stamps a 
runtime-neutral view derived from that
+  principal — `CamelMcpSecurityPrincipalName` (a `String`) and 
`CamelMcpSecurityPrincipalRoles` (a
+  `Collection<String>`) — so a tool route or a policy can authorize on a 
stable name and roles without knowing the
+  runtime type. For the Vert.x server the name is the user subject (or the 
`username`/`sub` claim) and the roles are
+  read from the principal's `roles`, Keycloak `realm_access.roles`, or 
`groups` claim; each property is set only when
+  the server could determine it, so treat a missing name or roles as 
unauthorized. Camel's shipped identity/token

Review Comment:
   This holds for token principals, but not for the default camel-main basic 
auth. `PropertyFileAuthentication.authenticate` builds the user with 
`User.fromName(name)`, so the principal is just `{username, amr}`. The 
properties-file roles (e.g. `admin` in this module's 
`mcp-basic-auth.properties`) only appear as `RoleBasedAuthorization`s through 
`getAuthorizations`, which `BasicAuthHandler` never calls. So 
`CamelMcpSecurityPrincipalRoles` is never set for basic-auth callers. A reader 
who configured roles in the properties file would expect them here. Could you 
either derive them for that case or add a sentence that roles come only from 
token claims (JWT/OIDC) and are not available for basic auth? (Regenerate the 
catalog doc mirror if the text changes.)



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