1use std::collections::{HashMap, HashSet};
81use std::convert::Infallible;
82use std::future::Future;
83use std::pin::Pin;
84use std::sync::Arc;
85use std::task::{Context, Poll};
86
87use tower::Service;
88
89use tower_mcp::protocol::{McpRequest, McpResponse};
90use tower_mcp::{RouterRequest, RouterResponse};
91use tower_mcp_types::JsonRpcError;
92
93use crate::config::{RoleConfig, RoleMappingConfig};
94
95enum RoleResolution {
97 NoClaims,
100 Role(String),
102 Unmapped,
105}
106
107#[derive(Clone)]
109pub struct RbacConfig {
110 claim: String,
112 claim_to_role: HashMap<String, String>,
114 role_allow: HashMap<String, HashSet<String>>,
116 role_deny: HashMap<String, HashSet<String>>,
118 default_deny: bool,
120}
121
122impl RbacConfig {
123 pub fn new(roles: &[RoleConfig], mapping: &RoleMappingConfig) -> Self {
125 let mut role_allow = HashMap::new();
126 let mut role_deny = HashMap::new();
127
128 for role in roles {
129 if !role.allow_tools.is_empty() {
130 role_allow.insert(
131 role.name.clone(),
132 role.allow_tools.iter().cloned().collect(),
133 );
134 }
135 if !role.deny_tools.is_empty() {
136 role_deny.insert(role.name.clone(), role.deny_tools.iter().cloned().collect());
137 }
138 }
139
140 Self {
141 claim: mapping.claim.clone(),
142 claim_to_role: mapping.mapping.clone(),
143 role_allow,
144 role_deny,
145 default_deny: mapping.default_deny,
146 }
147 }
148
149 fn resolve_role(&self, extensions: &tower_mcp::router::Extensions) -> RoleResolution {
155 let Some(claims) = extensions.get::<tower_mcp::oauth::token::TokenClaims>() else {
156 return RoleResolution::NoClaims;
157 };
158
159 if self.claim == "scope" {
161 let scopes = claims.scopes();
162 for scope in &scopes {
163 if let Some(role) = self.claim_to_role.get(scope) {
164 return RoleResolution::Role(role.clone());
165 }
166 }
167 return RoleResolution::Unmapped;
168 }
169
170 if let Some(value) = claims.extra.get(&self.claim) {
172 let claim_str = match value {
173 serde_json::Value::String(s) => s.clone(),
174 other => other.to_string(),
175 };
176 if let Some(role) = self.claim_to_role.get(&claim_str) {
178 return RoleResolution::Role(role.clone());
179 }
180 for part in claim_str.split_whitespace() {
182 if let Some(role) = self.claim_to_role.get(part) {
183 return RoleResolution::Role(role.clone());
184 }
185 }
186 }
187
188 RoleResolution::Unmapped
189 }
190
191 fn is_tool_allowed(&self, role: &str, tool_name: &str) -> bool {
193 if let Some(allowed) = self.role_allow.get(role)
195 && !allowed.contains(tool_name)
196 {
197 return false;
198 }
199 if let Some(denied) = self.role_deny.get(role)
201 && denied.contains(tool_name)
202 {
203 return false;
204 }
205 true
206 }
207}
208
209#[derive(Clone)]
211pub struct RbacService<S> {
212 inner: S,
213 config: Arc<RbacConfig>,
214}
215
216impl<S> RbacService<S> {
217 pub fn new(inner: S, config: RbacConfig) -> Self {
219 Self {
220 inner,
221 config: Arc::new(config),
222 }
223 }
224}
225
226impl<S> Service<RouterRequest> for RbacService<S>
227where
228 S: Service<RouterRequest, Response = RouterResponse, Error = Infallible>
229 + Clone
230 + Send
231 + 'static,
232 S::Future: Send,
233{
234 type Response = RouterResponse;
235 type Error = Infallible;
236 type Future = Pin<Box<dyn Future<Output = Result<RouterResponse, Infallible>> + Send>>;
237
238 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
239 self.inner.poll_ready(cx)
240 }
241
242 fn call(&mut self, req: RouterRequest) -> Self::Future {
243 let config = Arc::clone(&self.config);
244 let request_id = req.id.clone();
245
246 let role = match config.resolve_role(&req.extensions) {
248 RoleResolution::NoClaims => {
252 let fut = self.inner.call(req);
253 return Box::pin(fut);
254 }
255 RoleResolution::Unmapped => {
258 if config.default_deny {
259 return Box::pin(async move {
260 Ok(RouterResponse {
261 id: request_id,
262 inner: Err(JsonRpcError::invalid_params(
263 "Authenticated principal carries no recognized role; \
264 access denied (rbac default_deny)"
265 .to_string(),
266 )),
267 })
268 });
269 }
270 let fut = self.inner.call(req);
271 return Box::pin(fut);
272 }
273 RoleResolution::Role(role) => role,
274 };
275
276 let role_for_filter = role.clone();
277
278 if let McpRequest::CallTool(ref params) = req.inner
280 && !config.is_tool_allowed(&role, ¶ms.name)
281 {
282 let tool_name = params.name.clone();
283 return Box::pin(async move {
284 Ok(RouterResponse {
285 id: request_id,
286 inner: Err(JsonRpcError::invalid_params(format!(
287 "Role '{}' is not authorized to call tool: {}",
288 role, tool_name
289 ))),
290 })
291 });
292 }
293
294 let fut = self.inner.call(req);
295
296 Box::pin(async move {
297 let mut resp = fut.await?;
298
299 if let Ok(McpResponse::ListTools(ref mut result)) = resp.inner {
301 result
302 .tools
303 .retain(|tool| config.is_tool_allowed(&role_for_filter, &tool.name));
304 }
305
306 Ok(resp)
307 })
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use std::collections::HashMap;
314
315 use tower::Service;
316 use tower_mcp::oauth::token::TokenClaims;
317 use tower_mcp::protocol::{McpRequest, McpResponse, RequestId};
318 use tower_mcp::router::Extensions;
319
320 use super::{RbacConfig, RbacService};
321 use crate::config::{RoleConfig, RoleMappingConfig};
322 use crate::test_util::MockService;
323
324 fn test_rbac_config() -> RbacConfig {
325 rbac_config_with_default_deny(false)
326 }
327
328 fn rbac_config_with_default_deny(default_deny: bool) -> RbacConfig {
329 let roles = vec![
330 RoleConfig {
331 name: "admin".into(),
332 allow_tools: vec![],
333 deny_tools: vec![],
334 },
335 RoleConfig {
336 name: "reader".into(),
337 allow_tools: vec!["fs/read".into()],
338 deny_tools: vec![],
339 },
340 ];
341 let mapping = RoleMappingConfig {
342 claim: "scope".into(),
343 mapping: HashMap::from([
344 ("admin".into(), "admin".into()),
345 ("read-only".into(), "reader".into()),
346 ]),
347 default_deny,
348 };
349 RbacConfig::new(&roles, &mapping)
350 }
351
352 fn request_with_scope(scope: &str, inner: McpRequest) -> tower_mcp::RouterRequest {
353 let mut extensions = Extensions::new();
354 extensions.insert(TokenClaims {
355 sub: None,
356 iss: None,
357 aud: None,
358 exp: None,
359 scope: Some(scope.to_string()),
360 client_id: None,
361 extra: HashMap::new(),
362 });
363 tower_mcp::RouterRequest {
364 id: RequestId::Number(1),
365 inner,
366 extensions,
367 }
368 }
369
370 #[tokio::test]
371 async fn test_rbac_admin_can_call_any_tool() {
372 let mock = MockService::with_tools(&["fs/read", "fs/write"]);
373 let mut svc = RbacService::new(mock, test_rbac_config());
374
375 let req = request_with_scope(
376 "admin",
377 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
378 name: "fs/write".to_string(),
379 arguments: serde_json::json!({}),
380 input_responses: None,
381 request_state: None,
382 meta: None,
383 task: None,
384 }),
385 );
386 let resp = svc.call(req).await.unwrap();
387 assert!(resp.inner.is_ok(), "admin should call any tool");
388 }
389
390 #[tokio::test]
391 async fn test_rbac_reader_denied_write() {
392 let mock = MockService::with_tools(&["fs/read", "fs/write"]);
393 let mut svc = RbacService::new(mock, test_rbac_config());
394
395 let req = request_with_scope(
396 "read-only",
397 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
398 name: "fs/write".to_string(),
399 arguments: serde_json::json!({}),
400 input_responses: None,
401 request_state: None,
402 meta: None,
403 task: None,
404 }),
405 );
406 let resp = svc.call(req).await.unwrap();
407 let err = resp.inner.unwrap_err();
408 assert!(err.message.contains("not authorized"));
409 }
410
411 #[tokio::test]
412 async fn test_rbac_reader_allowed_read() {
413 let mock = MockService::with_tools(&["fs/read"]);
414 let mut svc = RbacService::new(mock, test_rbac_config());
415
416 let req = request_with_scope(
417 "read-only",
418 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
419 name: "fs/read".to_string(),
420 arguments: serde_json::json!({}),
421 input_responses: None,
422 request_state: None,
423 meta: None,
424 task: None,
425 }),
426 );
427 let resp = svc.call(req).await.unwrap();
428 assert!(resp.inner.is_ok(), "reader should call allowed tools");
429 }
430
431 #[tokio::test]
432 async fn test_rbac_filters_list_tools_for_role() {
433 let mock = MockService::with_tools(&["fs/read", "fs/write", "fs/delete"]);
434 let mut svc = RbacService::new(mock, test_rbac_config());
435
436 let req = request_with_scope("read-only", McpRequest::ListTools(Default::default()));
437 let resp = svc.call(req).await.unwrap();
438
439 match resp.inner.unwrap() {
440 McpResponse::ListTools(result) => {
441 let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
442 assert!(names.contains(&"fs/read"));
443 assert!(!names.contains(&"fs/write"));
444 assert!(!names.contains(&"fs/delete"));
445 }
446 other => panic!("expected ListTools, got: {:?}", other),
447 }
448 }
449
450 #[tokio::test]
451 async fn test_rbac_no_claims_passes_through() {
452 let mock = MockService::with_tools(&["fs/write"]);
453 let mut svc = RbacService::new(mock, test_rbac_config());
454
455 let req = tower_mcp::RouterRequest {
457 id: RequestId::Number(1),
458 inner: McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
459 name: "fs/write".to_string(),
460 arguments: serde_json::json!({}),
461 input_responses: None,
462 request_state: None,
463 meta: None,
464 task: None,
465 }),
466 extensions: Extensions::new(),
467 };
468 let resp = svc.call(req).await.unwrap();
469 assert!(resp.inner.is_ok(), "no claims should pass through");
470 }
471
472 #[tokio::test]
473 async fn test_rbac_unmapped_scope_passes_through_by_default() {
474 let mock = MockService::with_tools(&["fs/write"]);
477 let mut svc = RbacService::new(mock, rbac_config_with_default_deny(false));
478
479 let req = request_with_scope(
480 "unknown-scope",
481 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
482 name: "fs/write".to_string(),
483 arguments: serde_json::json!({}),
484 input_responses: None,
485 request_state: None,
486 meta: None,
487 task: None,
488 }),
489 );
490 let resp = svc.call(req).await.unwrap();
491 assert!(
492 resp.inner.is_ok(),
493 "unmapped scope should pass through when default_deny is false"
494 );
495 }
496
497 #[tokio::test]
498 async fn test_rbac_unmapped_scope_denied_with_default_deny() {
499 let mock = MockService::with_tools(&["fs/write"]);
501 let mut svc = RbacService::new(mock, rbac_config_with_default_deny(true));
502
503 let req = request_with_scope(
504 "unknown-scope",
505 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
506 name: "fs/write".to_string(),
507 arguments: serde_json::json!({}),
508 input_responses: None,
509 request_state: None,
510 meta: None,
511 task: None,
512 }),
513 );
514 let resp = svc.call(req).await.unwrap();
515 let err = resp.inner.unwrap_err();
516 assert!(
517 err.message.contains("default_deny"),
518 "unmapped scope should be denied when default_deny is true, got: {}",
519 err.message
520 );
521 }
522
523 #[tokio::test]
524 async fn test_rbac_mapped_scope_resolves_with_default_deny_enabled() {
525 let mock = MockService::with_tools(&["fs/read", "fs/write"]);
528 let mut svc = RbacService::new(mock, rbac_config_with_default_deny(true));
529
530 let read_req = request_with_scope(
531 "read-only",
532 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
533 name: "fs/read".to_string(),
534 arguments: serde_json::json!({}),
535 input_responses: None,
536 request_state: None,
537 meta: None,
538 task: None,
539 }),
540 );
541 let resp = svc.call(read_req).await.unwrap();
542 assert!(
543 resp.inner.is_ok(),
544 "mapped role should still resolve with default_deny enabled"
545 );
546
547 let write_req = request_with_scope(
548 "read-only",
549 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
550 name: "fs/write".to_string(),
551 arguments: serde_json::json!({}),
552 input_responses: None,
553 request_state: None,
554 meta: None,
555 task: None,
556 }),
557 );
558 let resp = svc.call(write_req).await.unwrap();
559 let err = resp.inner.unwrap_err();
560 assert!(
561 err.message.contains("not authorized"),
562 "reader should be denied write via role policy, got: {}",
563 err.message
564 );
565 }
566
567 #[tokio::test]
568 async fn test_rbac_no_claims_passes_through_with_default_deny() {
569 let mock = MockService::with_tools(&["fs/write"]);
572 let mut svc = RbacService::new(mock, rbac_config_with_default_deny(true));
573
574 let req = tower_mcp::RouterRequest {
575 id: RequestId::Number(1),
576 inner: McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
577 name: "fs/write".to_string(),
578 arguments: serde_json::json!({}),
579 input_responses: None,
580 request_state: None,
581 meta: None,
582 task: None,
583 }),
584 extensions: Extensions::new(),
585 };
586 let resp = svc.call(req).await.unwrap();
587 assert!(
588 resp.inner.is_ok(),
589 "no claims must pass through even when default_deny is true"
590 );
591 }
592}