You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by "oscerd (via GitHub)" <gi...@apache.org> on 2026/10/13 08:13:20 UTC

[PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

oscerd opened a new pull request, #27767:
URL: https://github.com/apache/camel/pull/27767

   ## Motivation
   
   Follow-up to CAMEL-24831 (MCP identity propagation, 4.23.0). Over the MCP server the authenticated caller reaches the tool route only as the **raw** transport principal — the `CamelMcpSecurityPrincipal` exchange property, which for the Vert.x streamable HTTP server is an `io.vertx.ext.auth.User`. Camel's shipped identity policies (Keycloak, Spring Security, Shiro) and most route guards cannot read that runtime-specific object, so an identity-based policy cannot authorize an MCP tool call out of the box.
   
   ## Change
   
   Expose a **runtime-neutral view** of the caller alongside the raw principal, derived by the engine from the transport user:
   
   - `CamelMcpSecurityPrincipalName` (`String`) — for the Vert.x server, the user subject (or the `username`/`sub` claim);
   - `CamelMcpSecurityPrincipalRoles` (`Collection<String>`) — read from the principal's `roles`, Keycloak `realm_access.roles`, or `groups` claim.
   
   A tool route or its `AuthorizationPolicy` can now authorize on a stable name and roles regardless of the MCP server runtime. Each property is stamped **only when the server could determine it**, so a policy should treat a missing name/roles as unauthorized. A custom policy can still read the raw `CamelMcpSecurityPrincipal` when it knows the runtime type.
   
   ### API
   
   `McpToolCallContext` gains `principalName` and `principalRoles` record components. The existing single-argument constructor is kept, so the 4.23 API stays backward compatible; the roles are defensively copied into an immutable collection.
   
   ### Scope
   
   This exposes the neutral view (for route authors and custom policies). Wiring the shipped policies (Keycloak, Spring Security, Shiro) to consume it automatically is intentionally left as a follow-up.
   
   ## Testing
   
   - `VertxMcpPrincipalTest` — unit tests for name/roles extraction: `username`/`sub` precedence, the `roles` / `realm_access.roles` / `groups` claims and their precedence, blank/non-string filtering, and null/empty → `null`.
   - `McpServerMainAuthenticationTest` — new end-to-end assertion that a tool route invoked over MCP sees `CamelMcpSecurityPrincipalName` for the basic-auth caller (`name=camel`).
   - `camel-mcp-server` + `camel-mcp-server-api` module build green (56 tests, 0 failures).
   
   Docs updated in `ai-tool-component.adoc` (+ catalog mirror). Targets 4.24.0.
   
   _Claude Code on behalf of oscerd_
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)


-- 
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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

Posted by "oscerd (via GitHub)" <gi...@apache.org>.
oscerd commented on code in PR #27767:
URL: https://github.com/apache/camel/pull/27767#discussion_r4268219499


##########
components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpPrincipal.java:
##########
@@ -0,0 +1,105 @@
+/*
+ * 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 non-null and non-blank, else the {@code username}
+     * or {@code sub} claim of the principal when non-null and non-blank, else {@code null}.

Review Comment:
   Already in place at `56c20f06`: the `name()` Javadoc reads "the Vert.x {@link User#subject() subject} **when non-null and non-blank**, else the `username` or `sub` claim of the principal **when non-null and non-blank**, else `null`" (VertxMcpPrincipal.java:37–38). It conveys that a blank subject is skipped — and also qualifies the `username`/`sub` fallback the same way, which is slightly more precise than the suggestion. So this is addressed.
   
   _Claude Code on behalf of oscerd_
   



##########
components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpPrincipalTest.java:
##########
@@ -0,0 +1,110 @@
+/*
+ * 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 io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.auth.User;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link VertxMcpPrincipal}: deriving a runtime-neutral name and roles from a Vert.x {@link User}.
+ */
+class VertxMcpPrincipalTest {
+
+    @Test
+    void nameFromUsernameClaim() {
+        User user = User.create(new JsonObject().put("username", "camel"));
+        assertThat(VertxMcpPrincipal.name(user)).isEqualTo("camel");
+    }
+
+    @Test
+    void nameFromSubClaimWhenNoUsername() {
+        User user = User.create(new JsonObject().put("sub", "user-123"));
+        assertThat(VertxMcpPrincipal.name(user)).isEqualTo("user-123");
+    }
+
+    @Test
+    void nameIsNullForNullUserOrNoClaims() {
+        assertThat(VertxMcpPrincipal.name(null)).isNull();
+        assertThat(VertxMcpPrincipal.name(User.create(new JsonObject()))).isNull();
+    }
+
+    @Test
+    void rolesFromTopLevelRolesClaim() {
+        User user = User.create(new JsonObject().put("roles", new JsonArray().add("admin").add("user")));
+        assertThat(VertxMcpPrincipal.roles(user)).containsExactly("admin", "user");
+    }
+
+    @Test
+    void rolesFromKeycloakRealmAccess() {
+        User user = User.create(new JsonObject()
+                .put("realm_access", new JsonObject().put("roles", new JsonArray().add("kc-role"))));
+        assertThat(VertxMcpPrincipal.roles(user)).containsExactly("kc-role");
+    }
+
+    @Test
+    void rolesFromGroupsClaim() {
+        User user = User.create(new JsonObject().put("groups", new JsonArray().add("g1").add("g2")));
+        assertThat(VertxMcpPrincipal.roles(user)).containsExactly("g1", "g2");
+    }
+
+    @Test
+    void topLevelRolesTakePrecedenceOverGroups() {
+        User user = User.create(new JsonObject()
+                .put("roles", new JsonArray().add("a"))
+                .put("groups", new JsonArray().add("b")));
+        assertThat(VertxMcpPrincipal.roles(user)).containsExactly("a");
+    }
+
+    @Test
+    void rolesSkipBlankAndNonStringEntries() {

Review Comment:
   Both the guard and the test are already in `56c20f06`:
   
   - `roles()` no longer calls `getJsonArray`/`getJsonObject` — it reads via `principal.getValue(k)` and checks `instanceof JsonArray`/`JsonObject` before use, so a scalar `"roles":"admin"` (or scalar `groups` / `realm_access.roles`, or a non-object `realm_access`) cannot throw `ClassCastException`.
   - `VertxMcpPrincipalTest.rolesIgnoreNonArrayClaimShapesInsteadOfThrowing` covers exactly those scalar shapes, and `rolesFallThroughFromNonArrayRolesToGroups` covers a scalar `roles` alongside a valid `groups` array.
   
   On null-vs-wrap: I deliberately **ignore** a scalar (it falls through `roles` → `realm_access.roles` → `groups` → `null`) rather than wrap it into a one-element list. This is an authorization input, so I prefer to fail safe: a non-standard scalar claim yields no roles (treated as unauthorized, per the docs) instead of synthesising a role the issuer may not have intended. An issuer that wants role-based authz should emit a JSON array.
   
   _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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27767:
URL: https://github.com/apache/camel/pull/27767#issuecomment-6143074139

   <!-- ci-tested-modules -->
   :test_tube: **CI tested the following changed modules:**
   
   - `catalog/camel-catalog`
   - `components/camel-ai/camel-ai-tool`
   - `components/camel-ai/camel-mcp-server-api`
   - `components/camel-ai/camel-mcp-server`
   
   ---
   
   <details><summary>:microscope: Scalpel shadow comparison — Scalpel: 13 of 703 tested, 26 compile-only — current: 13 all tested</summary>
   
   [Maveniverse Scalpel](https://github.com/maveniverse/scalpel) detected **13 affected modules** (current approach: 13).
   
   **Skip-tests mode would test 13 modules** (4 direct + 10 downstream), **skip tests for 26** (generated code, meta-modules)
   
   <details><summary>Modules Scalpel would test (13)</summary>
   
   - `camel-ai-tool` ← components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
   - `camel-jbang-mcp` ← downstream of org.apache.camel:camel-catalog
   - `camel-jbang-plugin-mcp` ← downstream of org.apache.camel:camel-jbang-core
   - `camel-jbang-plugin-tui` ← downstream of org.apache.camel:camel-catalog
   - `camel-jbang-plugin-validate` ← downstream of org.apache.camel:camel-yaml-dsl-validator
   - `camel-langchain4j-agent` ← downstream of org.apache.camel:camel-ai-tool
   - `camel-launcher-container` ← downstream of org.apache.camel:camel-launcher
   - `camel-mcp-server` ← components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpPrincipal.java, components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java, components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainAuthenticationTest.java, components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpPrincipalTest.java
   - `camel-mcp-server-api` ← components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java, components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpToolCallContext.java
   - `camel-openai` ← downstream of org.apache.camel:camel-ai-tool
   - `camel-spring-ai-chat` ← downstream of org.apache.camel:camel-ai-tool
   - `camel-yaml-dsl-validator` ← downstream of org.apache.camel:camel-catalog
   - `camel-yaml-dsl-validator-maven-plugin` ← downstream of org.apache.camel:camel-yaml-dsl-validator
   
   </details>
   
   <details><summary>Modules with tests skipped (26)</summary>
   
   - `apache-camel`
   - `camel-allcomponents`
   - `camel-catalog-console`
   - `camel-catalog-maven`
   - `camel-catalog-suggest`
   - `camel-componentdsl`
   - `camel-endpointdsl`
   - `camel-endpointdsl-support`
   - `camel-itest`
   - `camel-jbang-core`
   - `camel-jbang-it`
   - `camel-jbang-main`
   - `camel-jbang-plugin-edit`
   - `camel-jbang-plugin-generate`
   - `camel-jbang-plugin-kubernetes`
   - `camel-jbang-plugin-test`
   - `camel-kamelet-main`
   - `camel-launcher`
   - `camel-report-maven-plugin`
   - `camel-route-parser`
   - `camel-yaml-dsl`
   - `camel-yaml-dsl-deserializers`
   - `camel-yaml-dsl-maven-plugin`
   - `coverage`
   - `docs`
   - `dummy-component`
   
   </details>
   
   > :information_source: Shadow mode — Scalpel observes but does not affect test execution. [Learn more](https://github.com/maveniverse/scalpel)
   
   </details>
   
   :warning: **Some tests are disabled on GitHub Actions** (`@DisabledIfSystemProperty(named = "ci.env.name")`) and require manual verification:
   
   - `components/camel-ai/camel-mcp-server`: 1 test(s) disabled on GitHub Actions
   
   <details><summary>All tested modules (40 modules, 7m 14s total)</summary>
   
   **Total reactor time:** 7m 14s
   
   | Module | Duration | Status |
   | --- | --- | --- |
   | Camel :: AI :: OpenAI | 46.7s | SUCCESS |
   | Camel :: AI :: MCP Server | 45.2s | SUCCESS |
   | Camel :: Launcher | 43.0s | SUCCESS |
   | Camel :: JBang :: MCP | 38.8s | SUCCESS |
   | Camel :: Catalog :: Camel Catalog | 22.1s | SUCCESS |
   | Camel :: JBang :: Plugin :: Kubernetes | 19.4s | SUCCESS |
   | Camel :: Component DSL | 19.0s | SUCCESS |
   | Camel :: YAML DSL | 19.0s | SUCCESS |
   | Camel :: YAML DSL :: Validator | 19.0s | SUCCESS |
   | Camel :: AI :: MCP Server API | 17.7s | SUCCESS |
   | Camel :: JBang :: Plugin :: Validate | 16.0s | SUCCESS |
   | Camel :: AI :: LangChain4j :: Agent | 15.0s | SUCCESS |
   | Camel :: Kamelet Main | 14.9s | SUCCESS |
   | Camel :: Docs | 14.9s | SUCCESS |
   | Camel :: Spring AI :: Chat | 13.8s | SUCCESS |
   | Camel :: JBang :: Plugin :: Testing | 12.3s | SUCCESS |
   | Camel :: AI :: Tool | 10.7s | SUCCESS |
   | Camel :: Catalog :: Camel Report Maven Plugin | 7.9s | SUCCESS |
   | Camel :: Catalog :: Camel Route Parser | 6.6s | SUCCESS |
   | Camel :: YAML DSL :: Deserializers | 5.7s | SUCCESS |
   | Camel :: All Components Sync point | 4.5s | SUCCESS |
   | Camel :: YAML DSL :: Validator Maven Plugin | 3.5s | SUCCESS |
   | Camel :: Catalog :: Suggest (deprecated) | 2.4s | SUCCESS |
   | Camel :: YAML DSL :: Maven Plugins | 2.3s | SUCCESS |
   | Camel :: Catalog :: Maven | 2.2s | SUCCESS |
   | Camel :: Assembly | 1.7s | SUCCESS |
   | Camel :: JBang :: Plugin :: MCP | 1.4s | SUCCESS |
   | Camel :: JBang :: Plugin :: Edit | 1.4s | SUCCESS |
   | Camel :: JBang :: Integration tests | 1.3s | SUCCESS |
   | Camel :: JBang :: Plugin :: Generate | 1.0s | SUCCESS |
   | Camel :: Coverage | 0.9s | SUCCESS |
   | Camel :: Endpoint DSL :: Support | 0.9s | SUCCESS |
   | Camel :: Catalog :: Dummy Component | 0.8s | SUCCESS |
   | Camel :: JBang :: Main | 0.7s | SUCCESS |
   | Camel :: Launcher :: Container | 0.7s | SUCCESS |
   | Camel :: Catalog :: Console | 0.7s | SUCCESS |
   | Camel :: Endpoint DSL | n/a |  |
   | Camel :: Integration Tests | n/a |  |
   | Camel :: JBang :: Core | n/a |  |
   | Camel :: JBang :: Plugin :: TUI | n/a |  |
   
   **Top 20 slowest modules:**
   - `Camel :: AI :: OpenAI` (46.7s)
   - `Camel :: AI :: MCP Server` (45.2s)
   - `Camel :: Launcher` (43.0s)
   - `Camel :: JBang :: MCP` (38.8s)
   - `Camel :: Catalog :: Camel Catalog` (22.1s)
   - `Camel :: JBang :: Plugin :: Kubernetes` (19.4s)
   - `Camel :: Component DSL` (19.0s)
   - `Camel :: YAML DSL` (19.0s)
   - `Camel :: YAML DSL :: Validator` (19.0s)
   - `Camel :: AI :: MCP Server API` (17.7s)
   - `Camel :: JBang :: Plugin :: Validate` (16.0s)
   - `Camel :: AI :: LangChain4j :: Agent` (15.0s)
   - `Camel :: Kamelet Main` (14.9s)
   - `Camel :: Docs` (14.9s)
   - `Camel :: Spring AI :: Chat` (13.8s)
   - `Camel :: JBang :: Plugin :: Testing` (12.3s)
   - `Camel :: AI :: Tool` (10.7s)
   - `Camel :: Catalog :: Camel Report Maven Plugin` (7.9s)
   - `Camel :: Catalog :: Camel Route Parser` (6.6s)
   - `Camel :: YAML DSL :: Deserializers` (5.7s)
   
   </details>
   
   ---
   :gear: [View full build and test results](https://github.com/apache/camel/actions/runs/38367788802)


-- 
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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27767:
URL: https://github.com/apache/camel/pull/27767#discussion_r4256348388


##########
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}.
+     */

Review Comment:
   💡 **Javadoc precision:** The Javadoc says the fallback happens "when set", but `isSet()` treats blank-but-non-null as "not set" too. A caller who passes `" "` as the subject would see the fallback to `username`/`sub` without the Javadoc explaining why. Consider:
   
   ```suggestion
        * The caller name: the Vert.x {@link User#subject() subject} when non-null and non-blank, else the {@code username}
        * or {@code sub} claim of the principal when non-null and non-blank, else {@code null}.
   ```



-- 
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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus merged PR #27767:
URL: https://github.com/apache/camel/pull/27767


-- 
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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27767:
URL: https://github.com/apache/camel/pull/27767#discussion_r4268190106


##########
components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpPrincipalTest.java:
##########
@@ -0,0 +1,110 @@
+/*
+ * 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 io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.auth.User;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link VertxMcpPrincipal}: deriving a runtime-neutral name and roles from a Vert.x {@link User}.
+ */
+class VertxMcpPrincipalTest {
+
+    @Test
+    void nameFromUsernameClaim() {
+        User user = User.create(new JsonObject().put("username", "camel"));
+        assertThat(VertxMcpPrincipal.name(user)).isEqualTo("camel");
+    }
+
+    @Test
+    void nameFromSubClaimWhenNoUsername() {
+        User user = User.create(new JsonObject().put("sub", "user-123"));
+        assertThat(VertxMcpPrincipal.name(user)).isEqualTo("user-123");
+    }
+
+    @Test
+    void nameIsNullForNullUserOrNoClaims() {
+        assertThat(VertxMcpPrincipal.name(null)).isNull();
+        assertThat(VertxMcpPrincipal.name(User.create(new JsonObject()))).isNull();
+    }
+
+    @Test
+    void rolesFromTopLevelRolesClaim() {
+        User user = User.create(new JsonObject().put("roles", new JsonArray().add("admin").add("user")));
+        assertThat(VertxMcpPrincipal.roles(user)).containsExactly("admin", "user");
+    }
+
+    @Test
+    void rolesFromKeycloakRealmAccess() {
+        User user = User.create(new JsonObject()
+                .put("realm_access", new JsonObject().put("roles", new JsonArray().add("kc-role"))));
+        assertThat(VertxMcpPrincipal.roles(user)).containsExactly("kc-role");
+    }
+
+    @Test
+    void rolesFromGroupsClaim() {
+        User user = User.create(new JsonObject().put("groups", new JsonArray().add("g1").add("g2")));
+        assertThat(VertxMcpPrincipal.roles(user)).containsExactly("g1", "g2");
+    }
+
+    @Test
+    void topLevelRolesTakePrecedenceOverGroups() {
+        User user = User.create(new JsonObject()
+                .put("roles", new JsonArray().add("a"))
+                .put("groups", new JsonArray().add("b")));
+        assertThat(VertxMcpPrincipal.roles(user)).containsExactly("a");
+    }
+
+    @Test
+    void rolesSkipBlankAndNonStringEntries() {

Review Comment:
   ⚠️ **Missing test case — potential `ClassCastException`:** `getJsonArray("roles")` on a Vert.x `JsonObject` throws `ClassCastException` when the value is a plain `String` rather than a `JsonArray`. Some IdPs (e.g. certain Keycloak configurations) do emit a single-string `"roles": "admin"`. The same applies to `realm_access.roles` and `groups`.
   
   Please add a test for the scalar case (should it return `null` or wrap the value?) and guard the production code accordingly, e.g.:
   
   ```java
   Object rolesRaw = principal.getValue("roles");
   JsonArray roles = rolesRaw instanceof JsonArray ja ? ja
           : rolesRaw instanceof String s ? new JsonArray().add(s)
           : null;
   ```



##########
components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpPrincipal.java:
##########
@@ -0,0 +1,105 @@
+/*
+ * 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 non-null and non-blank, else the {@code username}
+     * or {@code sub} claim of the principal when non-null and non-blank, else {@code null}.

Review Comment:
   💡 **Javadoc precision:** the phrase "when set" is slightly misleading — `isSet()` rejects both `null` and blank strings (line 98: `value != null && !value.isBlank()`). A subject that is `" "` would also be skipped, which the current wording doesn't convey.
   
   Consider:
   ```suggestion
        * The caller name: the Vert.x {@link User#subject() subject} when non-null and non-blank,
        * else the {@code username} or {@code sub} claim of the principal, else {@code null}.
   ```



-- 
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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

Posted by "oscerd (via GitHub)" <gi...@apache.org>.
oscerd commented on code in PR #27767:
URL: https://github.com/apache/camel/pull/27767#discussion_r4268194677


##########
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:
   Fixed in the latest push (56c20f06). `roles()` now reads each claim with `getValue()` and checks `instanceof JsonArray` (and `JsonObject` for `realm_access`) before use — exactly as you suggested. A non-array `roles`/`groups`, or a non-object `realm_access`, is now ignored instead of cast, so a token in that shape no longer throws `ClassCastException` or fails the tool call in `toToolCallContext`. Added unit tests for the string-valued `roles`/`groups`, a non-object `realm_access`, a string `realm_access.roles`, and a fall-through-to-`groups` case. Thanks for catching this.
   
   _Claude Code on behalf of oscerd_
   



##########
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:
   Addressed by documenting (kept to the scoped view-exposure rather than deriving from `getAuthorizations`). The doc now states roles come only from token claims (JWT/OIDC), that a camel-main basic-auth caller carries no roles in its principal — property-file roles live in the Vert.x authorizations, which `BasicAuthHandler` never reads — so `CamelMcpSecurityPrincipalRoles` is not set for it, and that only the Vert.x server populates these today. Updated the source doc and the catalog mirror, and added an integration test asserting `no-roles` for the basic-auth caller.
   
   _Claude Code on behalf of oscerd_
   



##########
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}.
+     */

Review Comment:
   Done — the `name()` javadoc now reads "the subject when non-null and non-blank, else the `username` or `sub` claim … when non-null and non-blank", matching `isSet()`.
   
   _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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

Posted by "oscerd (via GitHub)" <gi...@apache.org>.
oscerd commented on PR #27767:
URL: https://github.com/apache/camel/pull/27767#issuecomment-6163696920

   Pushed `56c20f06` addressing all review threads (also rebased onto current `main`, which absorbed the adjacent CAMEL-25408 doc edit):
   
   - Hardened `VertxMcpPrincipal.roles()` against non-array claim shapes — no more `ClassCastException`, and a malformed `roles`/`groups`/`realm_access` token no longer fails the call. Added unit tests.
   - Documented that roles come only from token claims (a basic-auth caller has none) and that only the Vert.x server populates these today; regenerated the catalog mirror; added a basic-auth `no-roles` integration test.
   - Javadoc precision on `name()`.
   
   On the roles integration test (gnodet-bot): positive extraction is covered comprehensively by `VertxMcpPrincipalTest`, and the stamping wiring is shared with the integration-tested name path. A positive *end-to-end* roles test needs a JWT/OIDC server (basic-auth structurally carries no roles), which is disproportionate here and fits better once a token-claim runtime is wired (the CAMEL-25406 follow-up).
   
   _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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] CAMEL-25405: camel-mcp-server - expose a runtime-neutral MCP principal name and roles [camel]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27767:
URL: https://github.com/apache/camel/pull/27767#issuecomment-6141386174

   :star2: Thank you for your contribution to the Apache Camel project! :star2:
     :robot: CI automation will test this PR automatically.
   
     :camel: Apache Camel Committers, please review the following items:
   
     * First-time contributors **require MANUAL approval** for the GitHub Actions to run
     * You can use the command `/component-test (camel-)component-name1 (camel-)component-name2..` to request a test from the test bot although they are normally detected and executed by CI.
     * You can label PRs using `skip-tests` and `test-dependents` to fine-tune the checks executed by this PR.
     * Build and test logs are available in the summary page. **Only** [Apache Camel committers](https://camel.apache.org/community/team/#committers) have access to the summary.
     
     :warning: Be careful when sharing logs. Review their contents before sharing them publicly.


-- 
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: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org