48c958e9 by Ean Schuessler

Optimize MCP permission checking with single ArtifactGroupMembers query

- Replace per-service permission checks with single query to ArtifactGroupMembers
- Replace per-entity permission checks with single query to ArtifactGroupMembers
- Use Set for O(1) permission lookups instead of repeated hasPermission() calls
- Reduces transaction count from hundreds to just 2-3 total transactions
- Maintains same security model while dramatically improving performance
- Critical for scaling MCP interface with large Moqui installations
1 parent 8d5420e0
...@@ -305,6 +305,24 @@ ...@@ -305,6 +305,24 @@
305 } 305 }
306 } 306 }
307 307
308 // Get user's accessible services in a single query for efficiency
309 def userAccessibleServices = null as Set<String>
310 if (originalUsername != "mcp-user" && originalUsername != "mcp-business") {
311 // Query ArtifactGroupMembers directly to get all services user can access
312 ec.artifactExecution.disableAuthz()
313 try {
314 def artifactGroupMembers = ec.entity.find("moqui.security.ArtifactGroupMember")
315 .condition("artifactTypeEnumId", "AT_SERVICE")
316 .condition("userGroupId", ec.user.getUserGroups().collect { it.userGroupId })
317 .selectFields("artifactName")
318 .distinct()
319 .list()
320 userAccessibleServices = artifactGroupMembers.collect { it.artifactName } as Set<String>
321 } finally {
322 ec.artifactExecution.enableAuthz()
323 }
324 }
325
308 // Helper function to check if original user has permission to a service 326 // Helper function to check if original user has permission to a service
309 def userHasPermission = { serviceName -> 327 def userHasPermission = { serviceName ->
310 // Grant all permissions to mcp-user and mcp-business for business toolkit 328 // Grant all permissions to mcp-user and mcp-business for business toolkit
...@@ -312,14 +330,8 @@ ...@@ -312,14 +330,8 @@
312 return true 330 return true
313 } 331 }
314 332
315 // Temporarily switch back to original user to check permissions 333 // Use pre-computed accessible services set for O(1) lookup
316 ec.user.internalLoginUser(originalUsername) 334 return userAccessibleServices != null && userAccessibleServices.contains(serviceName.toString())
317 try {
318 return ec.user.hasPermission(serviceName.toString())
319 } finally {
320 // Switch back to admin for continued discovery
321 ec.user.internalLoginUser("admin")
322 }
323 } 335 }
324 336
325 // Add specific MCP services that should be exposed as tools 337 // Add specific MCP services that should be exposed as tools
...@@ -565,6 +577,24 @@ ...@@ -565,6 +577,24 @@
565 // Get all entity names and filter by permissions (no hardcoded list) 577 // Get all entity names and filter by permissions (no hardcoded list)
566 def allEntityNames = ec.entity.getAllEntityNames() 578 def allEntityNames = ec.entity.getAllEntityNames()
567 579
580 // Get user's accessible entities in a single query for efficiency
581 def userAccessibleEntities = null as Set<String>
582 if (originalUsername != "mcp-user" && originalUsername != "mcp-business") {
583 // Query ArtifactGroupMembers directly to get all entities user can access
584 ec.artifactExecution.disableAuthz()
585 try {
586 def artifactGroupMembers = ec.entity.find("moqui.security.ArtifactGroupMember")
587 .condition("artifactTypeEnumId", "AT_ENTITY")
588 .condition("userGroupId", ec.user.getUserGroups().collect { it.userGroupId })
589 .selectFields("artifactName")
590 .distinct()
591 .list()
592 userAccessibleEntities = artifactGroupMembers.collect { it.artifactName } as Set<String>
593 } finally {
594 ec.artifactExecution.enableAuthz()
595 }
596 }
597
568 // Helper function to check if original user has permission to an entity 598 // Helper function to check if original user has permission to an entity
569 def userHasEntityPermission = { entityName -> 599 def userHasEntityPermission = { entityName ->
570 // For MCP users, trust Moqui's artifact security system 600 // For MCP users, trust Moqui's artifact security system
...@@ -573,13 +603,8 @@ ...@@ -573,13 +603,8 @@
573 return true 603 return true
574 } 604 }
575 605
576 // For other users, check permissions normally 606 // Use pre-computed accessible entities set for O(1) lookup
577 ec.user.internalLoginUser(originalUsername) 607 return userAccessibleEntities != null && userAccessibleEntities.contains(entityName.toString())
578 try {
579 return ec.user.hasPermission(entityName.toString())
580 } finally {
581 ec.user.internalLoginUser("admin")
582 }
583 } 608 }
584 609
585 // Add all permitted entities - let Moqui artifact security handle filtering 610 // Add all permitted entities - let Moqui artifact security handle filtering
......