WIP switch to internal permissions
Showing
4 changed files
with
291 additions
and
200 deletions
| ... | @@ -44,9 +44,10 @@ | ... | @@ -44,9 +44,10 @@ |
| 44 | <moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="McpServices.list#Products" artifactTypeEnumId="AT_SERVICE"/> | 44 | <moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="McpServices.list#Products" artifactTypeEnumId="AT_SERVICE"/> |
| 45 | <moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="mantle.ledger.LedgerServices.find#GlAccount" artifactTypeEnumId="AT_SERVICE"/> | 45 | <moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="mantle.ledger.LedgerServices.find#GlAccount" artifactTypeEnumId="AT_SERVICE"/> |
| 46 | <!-- Entity Services --> | 46 | <!-- Entity Services --> |
| 47 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="org.moqui.impl.EntityServices.find#Map" artifactTypeEnumId="AT_SERVICE"/> | 47 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="org.moqui.impl.EntityServices.find#Entity" artifactTypeEnumId="AT_SERVICE"/> |
| 48 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="create#moqui.server.Visit" artifactTypeEnumId="AT_SERVICE"/> | 48 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="org.moqui.impl.EntityServices.create#Entity" artifactTypeEnumId="AT_SERVICE"/> |
| 49 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="update#moqui.server.Visit" artifactTypeEnumId="AT_SERVICE"/> | 49 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="org.moqui.impl.EntityServices.update#Entity" artifactTypeEnumId="AT_SERVICE"/> |
| 50 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="org.moqui.impl.EntityServices.delete#Entity" artifactTypeEnumId="AT_SERVICE"/> | ||
| 50 | 51 | ||
| 51 | <!-- Essential Business Entities --> | 52 | <!-- Essential Business Entities --> |
| 52 | <moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="mantle.order.OrderHeader" artifactTypeEnumId="AT_ENTITY"/> | 53 | <moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="mantle.order.OrderHeader" artifactTypeEnumId="AT_ENTITY"/> |
| ... | @@ -91,9 +92,4 @@ | ... | @@ -91,9 +92,4 @@ |
| 91 | <!-- Add existing demo users to MCP business group for focused testing --> | 92 | <!-- Add existing demo users to MCP business group for focused testing --> |
| 92 | <moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_JD" fromDate="2025-01-01 00:00:00.000"/> | 93 | <moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_JD" fromDate="2025-01-01 00:00:00.000"/> |
| 93 | <moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_BD" fromDate="2025-01-01 00:00:00.000"/> | 94 | <moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_BD" fromDate="2025-01-01 00:00:00.000"/> |
| 94 | |||
| 95 | <!-- Keep ADMIN access for system operations --> | ||
| 96 | <moqui.security.UserGroupMember userGroupId="ADMIN" userId="MCP_USER" fromDate="2025-01-01 00:00:00.000"/> | ||
| 97 | <moqui.security.UserGroupMember userGroupId="ADMIN" userId="MCP_BUSINESS" fromDate="2025-01-01 00:00:00.000"/> | ||
| 98 | |||
| 99 | </entity-facade-xml> | 95 | </entity-facade-xml> |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -16,7 +16,7 @@ | ... | @@ -16,7 +16,7 @@ |
| 16 | <!-- MCP Services using Moqui's built-in JSON-RPC support --> | 16 | <!-- MCP Services using Moqui's built-in JSON-RPC support --> |
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | <service verb="mcp" noun="Initialize" authenticate="true" allow-remote="true" transaction-timeout="30" authz-require="false"> | 19 | <service verb="mcp" noun="Initialize" authenticate="true" allow-remote="true" transaction-timeout="30"> |
| 20 | <description>Handle MCP initialize request using Moqui authentication</description> | 20 | <description>Handle MCP initialize request using Moqui authentication</description> |
| 21 | <in-parameters> | 21 | <in-parameters> |
| 22 | <parameter name="sessionId" required="false"/> | 22 | <parameter name="sessionId" required="false"/> |
| ... | @@ -30,15 +30,17 @@ | ... | @@ -30,15 +30,17 @@ |
| 30 | <actions> | 30 | <actions> |
| 31 | <script><![CDATA[ | 31 | <script><![CDATA[ |
| 32 | import org.moqui.context.ExecutionContext | 32 | import org.moqui.context.ExecutionContext |
| 33 | import org.moqui.impl.context.UserFacadeImpl.UserInfo | ||
| 33 | 34 | ||
| 34 | ExecutionContext ec = context.ec | 35 | ExecutionContext ec = context.ec |
| 35 | 36 | ||
| 37 | // Permissions are handled by Moqui's artifact authorization system | ||
| 38 | // Users must be in appropriate groups (McpUser, MCP_BUSINESS) with access to McpServices artifact group | ||
| 39 | |||
| 36 | // Get Visit (session) and validate access | 40 | // Get Visit (session) and validate access |
| 37 | def visit | 41 | def visit |
| 38 | if (sessionId) { | 42 | if (sessionId) { |
| 39 | // Existing session - run as ADMIN to access Visit entity | 43 | // Existing session - user can access their own visits |
| 40 | ec.artifactExecution.disableAuthz() | ||
| 41 | try { | ||
| 42 | visit = ec.entity.find("moqui.server.Visit") | 44 | visit = ec.entity.find("moqui.server.Visit") |
| 43 | .condition("visitId", sessionId) | 45 | .condition("visitId", sessionId) |
| 44 | .one() | 46 | .one() |
| ... | @@ -50,32 +52,26 @@ | ... | @@ -50,32 +52,26 @@ |
| 50 | if (visit.userId != ec.user.userId) { | 52 | if (visit.userId != ec.user.userId) { |
| 51 | throw new Exception("Access denied for session: ${sessionId}") | 53 | throw new Exception("Access denied for session: ${sessionId}") |
| 52 | } | 54 | } |
| 53 | } finally { | ||
| 54 | ec.artifactExecution.enableAuthz() | ||
| 55 | } | ||
| 56 | } else { | 55 | } else { |
| 57 | // New session - create or get current Visit | 56 | // New session - create or get current Visit |
| 58 | if (ec.user.visitId) { | 57 | if (ec.user.visitId) { |
| 59 | ec.artifactExecution.disableAuthz() | ||
| 60 | try { | ||
| 61 | visit = ec.entity.find("moqui.server.Visit") | 58 | visit = ec.entity.find("moqui.server.Visit") |
| 62 | .condition("visitId", ec.user.visitId) | 59 | .condition("visitId", ec.user.visitId) |
| 63 | .one() | 60 | .one() |
| 64 | } finally { | ||
| 65 | ec.artifactExecution.enableAuthz() | ||
| 66 | } | ||
| 67 | } | 61 | } |
| 68 | 62 | ||
| 69 | if (!visit) { | 63 | if (!visit) { |
| 70 | // Create a new Visit for this MCP session - run as ADMIN | 64 | // Create a new Visit for this MCP session for the actual authenticated user |
| 71 | // but set userId to the actual authenticated user passed from servlet | ||
| 72 | String actualUserId = parameters.actualUserId ?: ec.user.userId | 65 | String actualUserId = parameters.actualUserId ?: ec.user.userId |
| 73 | logger.info("Creating Visit - actualUserId: ${actualUserId}") | 66 | logger.info("Creating Visit - actualUserId: ${actualUserId}") |
| 74 | ec.artifactExecution.disableAuthz() | 67 | |
| 68 | // Use pushUser for admin-level Visit creation if needed | ||
| 69 | UserInfo adminUserInfo = null | ||
| 75 | try { | 70 | try { |
| 71 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 76 | visit = ec.entity.makeValue("moqui.server.Visit") | 72 | visit = ec.entity.makeValue("moqui.server.Visit") |
| 77 | visit.visitId = ec.entity.sequencedIdPrimaryEd(ec.entity.getEntityDefinition("moqui.server.Visit")) | 73 | visit.visitId = ec.entity.sequencedIdPrimaryEd(ec.entity.getEntityDefinition("moqui.server.Visit")) |
| 78 | visit.userId = "ADMIN" // Use ADMIN for privileged MCP access pattern | 74 | visit.userId = actualUserId // Use actual user, not ADMIN |
| 79 | visit.visitorId = null | 75 | visit.visitorId = null |
| 80 | visit.webappName = "mcp" | 76 | visit.webappName = "mcp" |
| 81 | visit.initialRequest = groovy.json.JsonOutput.toJson([mcpCreated: true, createdFor: "mcp-session"]) | 77 | visit.initialRequest = groovy.json.JsonOutput.toJson([mcpCreated: true, createdFor: "mcp-session"]) |
| ... | @@ -85,14 +81,17 @@ | ... | @@ -85,14 +81,17 @@ |
| 85 | visit.sessionId = null // No HTTP session for direct API calls | 81 | visit.sessionId = null // No HTTP session for direct API calls |
| 86 | visit.create() | 82 | visit.create() |
| 87 | } finally { | 83 | } finally { |
| 88 | ec.artifactExecution.enableAuthz() | 84 | if (adminUserInfo != null) { |
| 85 | ec.user.popUser() | ||
| 86 | } | ||
| 89 | } | 87 | } |
| 90 | } | 88 | } |
| 91 | } | 89 | } |
| 92 | 90 | ||
| 93 | // Update Visit with MCP initialization data - run as ADMIN | 91 | // Update Visit with MCP initialization data |
| 94 | ec.artifactExecution.disableAuthz() | 92 | UserInfo adminUserInfo = null |
| 95 | try { | 93 | try { |
| 94 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 96 | def metadata = [:] | 95 | def metadata = [:] |
| 97 | try { | 96 | try { |
| 98 | metadata = groovy.json.JsonSlurper().parseText(visit.initialRequest ?: "{}") as Map | 97 | metadata = groovy.json.JsonSlurper().parseText(visit.initialRequest ?: "{}") as Map |
| ... | @@ -109,7 +108,9 @@ | ... | @@ -109,7 +108,9 @@ |
| 109 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) | 108 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) |
| 110 | visit.update() | 109 | visit.update() |
| 111 | } finally { | 110 | } finally { |
| 112 | ec.artifactExecution.enableAuthz() | 111 | if (adminUserInfo != null) { |
| 112 | ec.user.popUser() | ||
| 113 | } | ||
| 113 | } | 114 | } |
| 114 | 115 | ||
| 115 | // Validate protocol version - support common MCP versions | 116 | // Validate protocol version - support common MCP versions |
| ... | @@ -152,7 +153,7 @@ | ... | @@ -152,7 +153,7 @@ |
| 152 | </actions> | 153 | </actions> |
| 153 | </service> | 154 | </service> |
| 154 | 155 | ||
| 155 | <service verb="mcp" noun="ToolsList" authenticate="true" allow-remote="true" transaction-timeout="60" authz-require="false"> | 156 | <service verb="mcp" noun="ToolsList" authenticate="true" allow-remote="true" transaction-timeout="60"> |
| 156 | <description>Handle MCP tools/list request with admin discovery but user permission filtering</description> | 157 | <description>Handle MCP tools/list request with admin discovery but user permission filtering</description> |
| 157 | <in-parameters> | 158 | <in-parameters> |
| 158 | <parameter name="sessionId"/> | 159 | <parameter name="sessionId"/> |
| ... | @@ -168,40 +169,26 @@ | ... | @@ -168,40 +169,26 @@ |
| 168 | 169 | ||
| 169 | ExecutionContext ec = context.ec | 170 | ExecutionContext ec = context.ec |
| 170 | 171 | ||
| 171 | // Store original user context before switching to admin for discovery | 172 | // Permissions are handled by Moqui's artifact authorization system |
| 172 | def originalUserId = ec.user.userId | 173 | // Users must be in appropriate groups (McpUser, MCP_BUSINESS) with access to McpServices artifact group |
| 173 | def originalUsername = ec.user.username | 174 | |
| 175 | // Permissions are handled by Moqui's artifact authorization system | ||
| 176 | // Users must be in appropriate groups (McpUser, MCP_BUSINESS) with access to McpServices artifact group | ||
| 174 | 177 | ||
| 175 | // Validate session if provided (run as original user for security) | 178 | // Validate session if provided |
| 176 | if (sessionId) { | 179 | if (sessionId) { |
| 177 | def visit = null | 180 | def visit = ec.entity.find("moqui.server.Visit") |
| 178 | // Temporarily disable authz to access Visit entity for session validation | ||
| 179 | ec.artifactExecution.disableAuthz() | ||
| 180 | try { | ||
| 181 | visit = ec.entity.find("moqui.server.Visit") | ||
| 182 | .condition("visitId", sessionId) | 181 | .condition("visitId", sessionId) |
| 183 | .one() | 182 | .one() |
| 184 | } finally { | ||
| 185 | ec.artifactExecution.enableAuthz() | ||
| 186 | } | ||
| 187 | 183 | ||
| 188 | // Validate session - allow special MCP case where Visit was created with ADMIN but accessed by MCP_USER | 184 | if (!visit || visit.userId != ec.user.userId) { |
| 189 | boolean sessionValid = false | ||
| 190 | if (visit) { | ||
| 191 | if (visit.userId == originalUserId) { | ||
| 192 | sessionValid = true | ||
| 193 | } else if (visit.userId == "ADMIN" && (originalUserId == "MCP_USER" || originalUserId == "MCP_BUSINESS")) { | ||
| 194 | // Special case: MCP services run with ADMIN privileges but authenticate as MCP_USER or MCP_BUSINESS | ||
| 195 | sessionValid = true | ||
| 196 | ec.logger.info("Allowing MCP service access: Visit created with ADMIN, accessed by ${originalUserId}") | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | if (!sessionValid) { | ||
| 201 | throw new Exception("Invalid session: ${sessionId}") | 185 | throw new Exception("Invalid session: ${sessionId}") |
| 202 | } | 186 | } |
| 187 | } | ||
| 203 | 188 | ||
| 189 | /* | ||
| 204 | // Update session activity | 190 | // Update session activity |
| 191 | if (sessionId && visit) { | ||
| 205 | def metadata = [:] | 192 | def metadata = [:] |
| 206 | try { | 193 | try { |
| 207 | metadata = groovy.json.JsonSlurper().parseText(visit.initialRequest ?: "{}") as Map | 194 | metadata = groovy.json.JsonSlurper().parseText(visit.initialRequest ?: "{}") as Map |
| ... | @@ -212,23 +199,75 @@ | ... | @@ -212,23 +199,75 @@ |
| 212 | metadata.mcpLastActivity = System.currentTimeMillis() | 199 | metadata.mcpLastActivity = System.currentTimeMillis() |
| 213 | metadata.mcpLastOperation = "tools/list" | 200 | metadata.mcpLastOperation = "tools/list" |
| 214 | 201 | ||
| 215 | // Update Visit with authz disabled | 202 | // Update Visit - need admin context for Visit updates |
| 216 | ec.artifactExecution.disableAuthz() | 203 | adminUserInfo = null |
| 217 | try { | 204 | try { |
| 205 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 218 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) | 206 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) |
| 219 | visit.update() | 207 | visit.update() |
| 220 | } finally { | 208 | } finally { |
| 221 | ec.artifactExecution.enableAuthz() | 209 | if (adminUserInfo != null) { |
| 210 | ec.user.popUser() | ||
| 211 | } | ||
| 222 | } | 212 | } |
| 223 | } | 213 | } |
| 214 | */ | ||
| 215 | |||
| 216 | // Store original user context before switching to ADMIN | ||
| 217 | def originalUsername = ec.user.username | ||
| 218 | def originalUserId = ec.user.userId | ||
| 219 | def userGroups = ec.user.getUserGroupIdSet().collect { it } | ||
| 220 | |||
| 221 | // Get user's accessible services in a single query for efficiency | ||
| 222 | def userAccessibleServices = null as Set<String> | ||
| 223 | adminUserInfo = null | ||
| 224 | try { | ||
| 225 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 226 | def artifactGroupMembers = ec.entity.find("moqui.security.ArtifactGroupMember") | ||
| 227 | .condition("artifactTypeEnumId", "AT_SERVICE") | ||
| 228 | .condition("userGroupId", userGroups) | ||
| 229 | .selectField("artifactName") | ||
| 230 | .distinct(true) | ||
| 231 | .list() | ||
| 232 | userAccessibleServices = artifactGroupMembers.collect { it.artifactName } as Set<String> | ||
| 233 | } finally { | ||
| 234 | if (adminUserInfo != null) { | ||
| 235 | ec.user.popUser() | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | // Helper function to check if user has permission to a service | ||
| 240 | def userHasPermission = { serviceName -> | ||
| 241 | // Use pre-computed accessible services set for O(1) lookup | ||
| 242 | return userAccessibleServices != null && userAccessibleServices.contains(serviceName.toString()) | ||
| 243 | } | ||
| 224 | 244 | ||
| 225 | // Switch to admin context for service discovery (to access all service definitions) | 245 | // Switch to admin context for service discovery (to access all service definitions) |
| 226 | ec.user.internalLoginUser("admin") | 246 | adminUserInfo = ec.user.pushUser("ADMIN") |
| 227 | 247 | ||
| 228 | try { | 248 | try { |
| 229 | def availableTools = [] | 249 | def availableTools = [] |
| 250 | |||
| 251 | // Get only services user has access to via artifact groups | ||
| 252 | def accessibleServiceNames = [] | ||
| 253 | for (serviceName in userAccessibleServices) { | ||
| 254 | // Handle wildcard patterns like "McpServices.*" | ||
| 255 | if (serviceName.contains("*")) { | ||
| 256 | def pattern = serviceName.replace("*", ".*") | ||
| 230 | def allServiceNames = ec.service.getKnownServiceNames() | 257 | def allServiceNames = ec.service.getKnownServiceNames() |
| 231 | ec.logger.info("MCP ToolsList: Admin discovered ${allServiceNames.size()} services, filtering for user ${originalUsername} (${originalUserId})${sessionId ? ' (session: ' + sessionId + ')' : ''}") | 258 | def matchingServices = allServiceNames.findAll { it.matches(pattern) } |
| 259 | // Only add services that actually exist | ||
| 260 | accessibleServiceNames.addAll(matchingServices.findAll { ec.service.isServiceDefined(it) }) | ||
| 261 | } else { | ||
| 262 | // Only add if service actually exists | ||
| 263 | if (ec.service.isServiceDefined(serviceName)) { | ||
| 264 | accessibleServiceNames << serviceName | ||
| 265 | } | ||
| 266 | } | ||
| 267 | } | ||
| 268 | accessibleServiceNames = accessibleServiceNames.unique() | ||
| 269 | |||
| 270 | ec.logger.info("MCP ToolsList: Found ${accessibleServiceNames.size()} accessible services for user ${originalUsername} (${originalUserId})${sessionId ? ' (session: ' + sessionId + ')' : ''}") | ||
| 232 | 271 | ||
| 233 | // Helper function to convert service to MCP tool | 272 | // Helper function to convert service to MCP tool |
| 234 | def convertServiceToTool = { serviceName -> | 273 | def convertServiceToTool = { serviceName -> |
| ... | @@ -305,60 +344,8 @@ | ... | @@ -305,60 +344,8 @@ |
| 305 | } | 344 | } |
| 306 | } | 345 | } |
| 307 | 346 | ||
| 308 | // Get user's accessible services in a single query for efficiency | 347 | // Add all accessible services as tools |
| 309 | def userAccessibleServices = null as Set<String> | 348 | for (serviceName in accessibleServiceNames) { |
| 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 | |||
| 326 | // Helper function to check if original user has permission to a service | ||
| 327 | def userHasPermission = { serviceName -> | ||
| 328 | // Grant all permissions to mcp-user and mcp-business for business toolkit | ||
| 329 | if (originalUsername == "mcp-user" || originalUsername == "mcp-business") { | ||
| 330 | return true | ||
| 331 | } | ||
| 332 | |||
| 333 | // Use pre-computed accessible services set for O(1) lookup | ||
| 334 | return userAccessibleServices != null && userAccessibleServices.contains(serviceName.toString()) | ||
| 335 | } | ||
| 336 | |||
| 337 | // Add specific MCP services that should be exposed as tools | ||
| 338 | def mcpToolServices = ["McpServices.mcp#Ping"] | ||
| 339 | for (serviceName in mcpToolServices) { | ||
| 340 | boolean hasPermission = userHasPermission(serviceName) | ||
| 341 | ec.logger.info("MCP ToolsList: MCP service ${serviceName} userHasPermission=${hasPermission}") | ||
| 342 | if (!hasPermission) { | ||
| 343 | continue | ||
| 344 | } | ||
| 345 | |||
| 346 | def tool = convertServiceToTool(serviceName) | ||
| 347 | if (tool) { | ||
| 348 | availableTools << tool | ||
| 349 | } | ||
| 350 | } | ||
| 351 | |||
| 352 | // Now add all other services the user has permission to access | ||
| 353 | for (serviceName in allServiceNames) { | ||
| 354 | // Permissions system already controls access, no need for artificial exclusions | ||
| 355 | |||
| 356 | // Check permission using original user context | ||
| 357 | boolean hasPermission = userHasPermission(serviceName) | ||
| 358 | if (!hasPermission) { | ||
| 359 | continue | ||
| 360 | } | ||
| 361 | |||
| 362 | def tool = convertServiceToTool(serviceName) | 349 | def tool = convertServiceToTool(serviceName) |
| 363 | if (tool) { | 350 | if (tool) { |
| 364 | availableTools << tool | 351 | availableTools << tool |
| ... | @@ -394,13 +381,15 @@ | ... | @@ -394,13 +381,15 @@ |
| 394 | 381 | ||
| 395 | } finally { | 382 | } finally { |
| 396 | // Always restore original user context | 383 | // Always restore original user context |
| 397 | ec.user.internalLoginUser(originalUsername) | 384 | if (adminUserInfo != null) { |
| 385 | ec.user.popUser() | ||
| 386 | } | ||
| 398 | } | 387 | } |
| 399 | ]]></script> | 388 | ]]></script> |
| 400 | </actions> | 389 | </actions> |
| 401 | </service> | 390 | </service> |
| 402 | 391 | ||
| 403 | <service verb="mcp" noun="ToolsCall" authenticate="true" allow-remote="true" transaction-timeout="300" authz-require="false"> | 392 | <service verb="mcp" noun="ToolsCall" authenticate="true" allow-remote="true" transaction-timeout="300"> |
| 404 | <description>Handle MCP tools/call request with direct Moqui service execution</description> | 393 | <description>Handle MCP tools/call request with direct Moqui service execution</description> |
| 405 | <in-parameters> | 394 | <in-parameters> |
| 406 | <parameter name="name" required="true"/> | 395 | <parameter name="name" required="true"/> |
| ... | @@ -427,13 +416,16 @@ | ... | @@ -427,13 +416,16 @@ |
| 427 | // Validate session if provided | 416 | // Validate session if provided |
| 428 | if (sessionId) { | 417 | if (sessionId) { |
| 429 | def visit = null | 418 | def visit = null |
| 430 | ec.artifactExecution.disableAuthz() | 419 | UserInfo adminUserInfo = null |
| 431 | try { | 420 | try { |
| 421 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 432 | visit = ec.entity.find("moqui.server.Visit") | 422 | visit = ec.entity.find("moqui.server.Visit") |
| 433 | .condition("visitId", sessionId) | 423 | .condition("visitId", sessionId) |
| 434 | .one() | 424 | .one() |
| 435 | } finally { | 425 | } finally { |
| 436 | ec.artifactExecution.enableAuthz() | 426 | if (adminUserInfo != null) { |
| 427 | ec.user.popUser() | ||
| 428 | } | ||
| 437 | } | 429 | } |
| 438 | 430 | ||
| 439 | // Validate session - allow special MCP case where Visit was created with ADMIN but accessed by MCP_USER or MCP_BUSINESS | 431 | // Validate session - allow special MCP case where Visit was created with ADMIN but accessed by MCP_USER or MCP_BUSINESS |
| ... | @@ -452,19 +444,24 @@ | ... | @@ -452,19 +444,24 @@ |
| 452 | } | 444 | } |
| 453 | } | 445 | } |
| 454 | 446 | ||
| 455 | // Note: Permission checking handled by elevated execution pattern | 447 | // Check permission using current user context (not elevated) |
| 456 | // MCP services run with ADMIN privileges but audit as MCP_USER | 448 | if (!ec.user.hasPermission("service:${name}".toString())) { |
| 449 | throw new Exception("Permission denied for service: ${name}") | ||
| 450 | } | ||
| 457 | 451 | ||
| 458 | def startTime = System.currentTimeMillis() | 452 | def startTime = System.currentTimeMillis() |
| 459 | try { | 453 | try { |
| 460 | // Execute service with elevated privileges for system access | 454 | // Execute service with elevated privileges for system access |
| 461 | // but maintain audit context with actual user (MCP_USER) | 455 | // but maintain audit context with actual user |
| 462 | def serviceResult | 456 | def serviceResult |
| 463 | ec.artifactExecution.disableAuthz() | 457 | UserInfo adminUserInfo = null |
| 464 | try { | 458 | try { |
| 459 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 465 | serviceResult = ec.service.sync().name(name).parameters(arguments ?: [:]).call() | 460 | serviceResult = ec.service.sync().name(name).parameters(arguments ?: [:]).call() |
| 466 | } finally { | 461 | } finally { |
| 467 | ec.artifactExecution.enableAuthz() | 462 | if (adminUserInfo != null) { |
| 463 | ec.user.popUser() | ||
| 464 | } | ||
| 468 | } | 465 | } |
| 469 | def executionTime = (System.currentTimeMillis() - startTime) / 1000.0 | 466 | def executionTime = (System.currentTimeMillis() - startTime) / 1000.0 |
| 470 | 467 | ||
| ... | @@ -497,13 +494,15 @@ | ... | @@ -497,13 +494,15 @@ |
| 497 | ec.logger.error("MCP tool execution error", e) | 494 | ec.logger.error("MCP tool execution error", e) |
| 498 | } finally { | 495 | } finally { |
| 499 | // Always restore original user context | 496 | // Always restore original user context |
| 500 | ec.user.internalLoginUser(originalUsername) | 497 | if (adminUserInfo != null) { |
| 498 | ec.user.popUser() | ||
| 499 | } | ||
| 501 | } | 500 | } |
| 502 | ]]></script> | 501 | ]]></script> |
| 503 | </actions> | 502 | </actions> |
| 504 | </service> | 503 | </service> |
| 505 | 504 | ||
| 506 | <service verb="mcp" noun="ResourcesList" authenticate="true" allow-remote="true" transaction-timeout="60" authz-require="false"> | 505 | <service verb="mcp" noun="ResourcesList" authenticate="true" allow-remote="true" transaction-timeout="60"> |
| 507 | <description>Handle MCP resources/list request with Moqui entity discovery</description> | 506 | <description>Handle MCP resources/list request with Moqui entity discovery</description> |
| 508 | <in-parameters> | 507 | <in-parameters> |
| 509 | <parameter name="sessionId"/> | 508 | <parameter name="sessionId"/> |
| ... | @@ -515,34 +514,29 @@ | ... | @@ -515,34 +514,29 @@ |
| 515 | <actions> | 514 | <actions> |
| 516 | <script><![CDATA[ | 515 | <script><![CDATA[ |
| 517 | import org.moqui.context.ExecutionContext | 516 | import org.moqui.context.ExecutionContext |
| 517 | import org.moqui.impl.context.UserFacadeImpl.UserInfo | ||
| 518 | 518 | ||
| 519 | ExecutionContext ec = context.ec | 519 | ExecutionContext ec = context.ec |
| 520 | 520 | ||
| 521 | // Permissions are handled by Moqui's artifact authorization system | ||
| 522 | // Users must be in appropriate groups (McpUser, MCP_BUSINESS) with access to McpServices artifact group | ||
| 523 | |||
| 521 | // Validate session if provided | 524 | // Validate session if provided |
| 522 | if (sessionId) { | 525 | if (sessionId) { |
| 523 | def visit = null | 526 | def visit = ec.entity.find("moqui.server.Visit") |
| 524 | ec.artifactExecution.disableAuthz() | ||
| 525 | try { | ||
| 526 | visit = ec.entity.find("moqui.server.Visit") | ||
| 527 | .condition("visitId", sessionId) | 527 | .condition("visitId", sessionId) |
| 528 | .one() | 528 | .one() |
| 529 | } finally { | ||
| 530 | ec.artifactExecution.enableAuthz() | ||
| 531 | } | ||
| 532 | 529 | ||
| 533 | // Validate session - allow special MCP case where Visit was created with ADMIN but accessed by MCP_USER or MCP_BUSINESS | 530 | if (!visit || visit.userId != ec.user.userId) { |
| 534 | boolean sessionValid = false | 531 | throw new Exception("Invalid session: ${sessionId}") |
| 535 | if (visit) { | ||
| 536 | if (visit.userId == ec.user.userId) { | ||
| 537 | sessionValid = true | ||
| 538 | } else if (visit.userId == "ADMIN" && (ec.user.userId == "MCP_USER" || ec.user.userId == "MCP_BUSINESS")) { | ||
| 539 | // Special case: MCP services run with ADMIN privileges but authenticate as MCP_USER or MCP_BUSINESS | ||
| 540 | sessionValid = true | ||
| 541 | ec.logger.info("Allowing MCP service access: Visit created with ADMIN, accessed by ${ec.user.userId}") | ||
| 542 | } | 532 | } |
| 543 | } | 533 | } |
| 544 | 534 | ||
| 545 | if (!sessionValid) { | 535 | // Build list of available entities as resources |
| 536 | def resources = [] | ||
| 537 | |||
| 538 | UserInfo adminUserInfo = null | ||
| 539 | try { | ||
| 546 | throw new Exception("Invalid session: ${sessionId}") | 540 | throw new Exception("Invalid session: ${sessionId}") |
| 547 | } | 541 | } |
| 548 | 542 | ||
| ... | @@ -557,17 +551,18 @@ | ... | @@ -557,17 +551,18 @@ |
| 557 | metadata.mcpLastActivity = System.currentTimeMillis() | 551 | metadata.mcpLastActivity = System.currentTimeMillis() |
| 558 | metadata.mcpLastOperation = "resources/list" | 552 | metadata.mcpLastOperation = "resources/list" |
| 559 | 553 | ||
| 560 | ec.artifactExecution.disableAuthz() | 554 | // Update Visit - need admin context for Visit updates |
| 555 | UserInfo adminUserInfo = null | ||
| 561 | try { | 556 | try { |
| 557 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 562 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) | 558 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) |
| 563 | visit.update() | 559 | visit.update() |
| 564 | } finally { | 560 | } finally { |
| 565 | ec.artifactExecution.enableAuthz() | 561 | if (adminUserInfo != null) { |
| 562 | ec.user.popUser() | ||
| 563 | } | ||
| 566 | } | 564 | } |
| 567 | } | 565 | } |
| 568 | |||
| 569 | // Store original username for permission checks | ||
| 570 | def originalUsername = ec.user.username | ||
| 571 | 566 | ||
| 572 | // Use curated list of commonly used entities instead of discovering all entities | 567 | // Use curated list of commonly used entities instead of discovering all entities |
| 573 | def availableResources = [] | 568 | def availableResources = [] |
| ... | @@ -577,32 +572,30 @@ | ... | @@ -577,32 +572,30 @@ |
| 577 | // Get all entity names and filter by permissions (no hardcoded list) | 572 | // Get all entity names and filter by permissions (no hardcoded list) |
| 578 | def allEntityNames = ec.entity.getAllEntityNames() | 573 | def allEntityNames = ec.entity.getAllEntityNames() |
| 579 | 574 | ||
| 575 | // Store original username for permission checks | ||
| 576 | def originalUsername = ec.user.username | ||
| 577 | |||
| 580 | // Get user's accessible entities in a single query for efficiency | 578 | // Get user's accessible entities in a single query for efficiency |
| 581 | def userAccessibleEntities = null as Set<String> | 579 | 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 | 580 | // Query ArtifactGroupMembers directly to get all entities user can access |
| 584 | ec.artifactExecution.disableAuthz() | 581 | UserInfo adminUserInfo = null |
| 585 | try { | 582 | try { |
| 583 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 586 | def artifactGroupMembers = ec.entity.find("moqui.security.ArtifactGroupMember") | 584 | def artifactGroupMembers = ec.entity.find("moqui.security.ArtifactGroupMember") |
| 587 | .condition("artifactTypeEnumId", "AT_ENTITY") | 585 | .condition("artifactTypeEnumId", "AT_ENTITY") |
| 588 | .condition("userGroupId", ec.user.getUserGroups().collect { it.userGroupId }) | 586 | .condition("userGroupId", ec.user.getUserGroupsIdSet().collect { it.userGroupId }) |
| 589 | .selectFields("artifactName") | 587 | .selectFields("artifactName") |
| 590 | .distinct() | 588 | .distinct(true) |
| 591 | .list() | 589 | .list() |
| 592 | userAccessibleEntities = artifactGroupMembers.collect { it.artifactName } as Set<String> | 590 | userAccessibleEntities = artifactGroupMembers.collect { it.artifactName } as Set<String> |
| 593 | } finally { | 591 | } finally { |
| 594 | ec.artifactExecution.enableAuthz() | 592 | if (adminUserInfo != null) { |
| 593 | ec.user.popUser() | ||
| 595 | } | 594 | } |
| 596 | } | 595 | } |
| 597 | 596 | ||
| 598 | // Helper function to check if original user has permission to an entity | 597 | // Helper function to check if user has permission to an entity |
| 599 | def userHasEntityPermission = { entityName -> | 598 | def userHasEntityPermission = { entityName -> |
| 600 | // For MCP users, trust Moqui's artifact security system | ||
| 601 | // The MCP_BUSINESS group has proper entity permissions through McpBusinessServices artifact group | ||
| 602 | if (originalUsername == "mcp-user" || originalUsername == "mcp-business") { | ||
| 603 | return true | ||
| 604 | } | ||
| 605 | |||
| 606 | // Use pre-computed accessible entities set for O(1) lookup | 599 | // Use pre-computed accessible entities set for O(1) lookup |
| 607 | return userAccessibleEntities != null && userAccessibleEntities.contains(entityName.toString()) | 600 | return userAccessibleEntities != null && userAccessibleEntities.contains(entityName.toString()) |
| 608 | } | 601 | } |
| ... | @@ -625,7 +618,7 @@ | ... | @@ -625,7 +618,7 @@ |
| 625 | </actions> | 618 | </actions> |
| 626 | </service> | 619 | </service> |
| 627 | 620 | ||
| 628 | <service verb="mcp" noun="ResourcesRead" authenticate="true" allow-remote="true" transaction-timeout="120" authz-require="false"> | 621 | <service verb="mcp" noun="ResourcesRead" authenticate="true" allow-remote="true" transaction-timeout="120"> |
| 629 | <description>Handle MCP resources/read request with Moqui entity queries</description> | 622 | <description>Handle MCP resources/read request with Moqui entity queries</description> |
| 630 | <in-parameters> | 623 | <in-parameters> |
| 631 | <parameter name="sessionId"/> | 624 | <parameter name="sessionId"/> |
| ... | @@ -644,13 +637,16 @@ | ... | @@ -644,13 +637,16 @@ |
| 644 | // Validate session if provided | 637 | // Validate session if provided |
| 645 | if (sessionId) { | 638 | if (sessionId) { |
| 646 | def visit = null | 639 | def visit = null |
| 647 | ec.artifactExecution.disableAuthz() | 640 | UserInfo adminUserInfo = null |
| 648 | try { | 641 | try { |
| 642 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 649 | visit = ec.entity.find("moqui.server.Visit") | 643 | visit = ec.entity.find("moqui.server.Visit") |
| 650 | .condition("visitId", sessionId) | 644 | .condition("visitId", sessionId) |
| 651 | .one() | 645 | .one() |
| 652 | } finally { | 646 | } finally { |
| 653 | ec.artifactExecution.enableAuthz() | 647 | if (adminUserInfo != null) { |
| 648 | ec.user.popUser() | ||
| 649 | } | ||
| 654 | } | 650 | } |
| 655 | 651 | ||
| 656 | if (!visit || visit.userId != ec.user.userId) { | 652 | if (!visit || visit.userId != ec.user.userId) { |
| ... | @@ -669,12 +665,15 @@ | ... | @@ -669,12 +665,15 @@ |
| 669 | metadata.mcpLastOperation = "resources/read" | 665 | metadata.mcpLastOperation = "resources/read" |
| 670 | metadata.mcpLastResource = uri | 666 | metadata.mcpLastResource = uri |
| 671 | 667 | ||
| 672 | ec.artifactExecution.disableAuthz() | 668 | UserInfo adminUserInfo = null |
| 673 | try { | 669 | try { |
| 670 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 674 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) | 671 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) |
| 675 | visit.update() | 672 | visit.update() |
| 676 | } finally { | 673 | } finally { |
| 677 | ec.artifactExecution.enableAuthz() | 674 | if (adminUserInfo != null) { |
| 675 | ec.user.popUser() | ||
| 676 | } | ||
| 678 | } | 677 | } |
| 679 | } | 678 | } |
| 680 | 679 | ||
| ... | @@ -690,10 +689,7 @@ | ... | @@ -690,10 +689,7 @@ |
| 690 | throw new Exception("Entity not found: ${entityName}") | 689 | throw new Exception("Entity not found: ${entityName}") |
| 691 | } | 690 | } |
| 692 | 691 | ||
| 693 | // Check permission | 692 | // Permission checking is handled by Moqui's artifact authorization system through artifact groups |
| 694 | if (false && ec.user.username != "mcp-user" && !ec.user.hasPermission("entity:${entityName}".toString())) { | ||
| 695 | throw new Exception("Permission denied for entity: ${entityName}") | ||
| 696 | } | ||
| 697 | 693 | ||
| 698 | def startTime = System.currentTimeMillis() | 694 | def startTime = System.currentTimeMillis() |
| 699 | try { | 695 | try { |
| ... | @@ -749,7 +745,7 @@ | ... | @@ -749,7 +745,7 @@ |
| 749 | </actions> | 745 | </actions> |
| 750 | </service> | 746 | </service> |
| 751 | 747 | ||
| 752 | <service verb="mcp" noun="Ping" authenticate="true" allow-remote="true" transaction-timeout="10" authz-require="false"> | 748 | <service verb="mcp" noun="Ping" authenticate="true" allow-remote="true" transaction-timeout="10"> |
| 753 | <description>Handle MCP ping request for health check</description> | 749 | <description>Handle MCP ping request for health check</description> |
| 754 | <in-parameters> | 750 | <in-parameters> |
| 755 | <parameter name="sessionId"/> | 751 | <parameter name="sessionId"/> |
| ... | @@ -759,16 +755,41 @@ | ... | @@ -759,16 +755,41 @@ |
| 759 | </out-parameters> | 755 | </out-parameters> |
| 760 | <actions> | 756 | <actions> |
| 761 | <script><![CDATA[ | 757 | <script><![CDATA[ |
| 758 | // Permissions are handled by Moqui's artifact authorization system | ||
| 759 | // Users must be in appropriate groups (McpUser, MCP_BUSINESS) with access to McpServices artifact group | ||
| 760 | |||
| 762 | // Validate session if provided | 761 | // Validate session if provided |
| 763 | if (sessionId) { | 762 | if (sessionId) { |
| 764 | def visit = null | 763 | def visit = ec.entity.find("moqui.server.Visit") |
| 765 | ec.artifactExecution.disableAuthz() | ||
| 766 | try { | ||
| 767 | visit = ec.entity.find("moqui.server.Visit") | ||
| 768 | .condition("visitId", sessionId) | 764 | .condition("visitId", sessionId) |
| 769 | .one() | 765 | .one() |
| 766 | |||
| 767 | if (!visit || visit.userId != ec.user.userId) { | ||
| 768 | throw new Exception("Invalid session: ${sessionId}") | ||
| 769 | } | ||
| 770 | |||
| 771 | // Update session activity | ||
| 772 | def metadata = [:] | ||
| 773 | try { | ||
| 774 | metadata = groovy.json.JsonSlurper().parseText(visit.initialRequest ?: "{}") as Map | ||
| 775 | } catch (Exception e) { | ||
| 776 | ec.logger.debug("Failed to parse Visit metadata: ${e.message}") | ||
| 777 | } | ||
| 778 | |||
| 779 | metadata.mcpLastActivity = System.currentTimeMillis() | ||
| 780 | metadata.mcpLastOperation = "ping" | ||
| 781 | |||
| 782 | // Update Visit - need admin context for Visit updates | ||
| 783 | UserInfo adminUserInfo = null | ||
| 784 | try { | ||
| 785 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 786 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) | ||
| 787 | visit.update() | ||
| 770 | } finally { | 788 | } finally { |
| 771 | ec.artifactExecution.enableAuthz() | 789 | if (adminUserInfo != null) { |
| 790 | ec.user.popUser() | ||
| 791 | } | ||
| 792 | } | ||
| 772 | } | 793 | } |
| 773 | 794 | ||
| 774 | if (!visit || visit.userId != ec.user.userId) { | 795 | if (!visit || visit.userId != ec.user.userId) { |
| ... | @@ -786,12 +807,15 @@ | ... | @@ -786,12 +807,15 @@ |
| 786 | metadata.mcpLastActivity = System.currentTimeMillis() | 807 | metadata.mcpLastActivity = System.currentTimeMillis() |
| 787 | metadata.mcpLastOperation = "ping" | 808 | metadata.mcpLastOperation = "ping" |
| 788 | 809 | ||
| 789 | ec.artifactExecution.disableAuthz() | 810 | UserInfo adminUserInfo = null |
| 790 | try { | 811 | try { |
| 812 | adminUserInfo = ec.user.pushUser("ADMIN") | ||
| 791 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) | 813 | visit.initialRequest = groovy.json.JsonOutput.toJson(metadata) |
| 792 | visit.update() | 814 | visit.update() |
| 793 | } finally { | 815 | } finally { |
| 794 | ec.artifactExecution.enableAuthz() | 816 | if (adminUserInfo != null) { |
| 817 | ec.user.popUser() | ||
| 818 | } | ||
| 795 | } | 819 | } |
| 796 | } | 820 | } |
| 797 | 821 | ||
| ... | @@ -817,6 +841,7 @@ | ... | @@ -817,6 +841,7 @@ |
| 817 | <actions> | 841 | <actions> |
| 818 | <script><![CDATA[ | 842 | <script><![CDATA[ |
| 819 | import org.moqui.context.ExecutionContext | 843 | import org.moqui.context.ExecutionContext |
| 844 | import org.moqui.impl.context.UserFacadeImpl.UserInfo | ||
| 820 | 845 | ||
| 821 | ExecutionContext ec = context.ec | 846 | ExecutionContext ec = context.ec |
| 822 | 847 | ||
| ... | @@ -854,6 +879,7 @@ | ... | @@ -854,6 +879,7 @@ |
| 854 | <actions> | 879 | <actions> |
| 855 | <script><![CDATA[ | 880 | <script><![CDATA[ |
| 856 | import org.moqui.context.ExecutionContext | 881 | import org.moqui.context.ExecutionContext |
| 882 | import org.moqui.impl.context.UserFacadeImpl.UserInfo | ||
| 857 | 883 | ||
| 858 | ExecutionContext ec = context.ec | 884 | ExecutionContext ec = context.ec |
| 859 | 885 | ||
| ... | @@ -946,6 +972,7 @@ | ... | @@ -946,6 +972,7 @@ |
| 946 | <actions> | 972 | <actions> |
| 947 | <script><![CDATA[ | 973 | <script><![CDATA[ |
| 948 | import org.moqui.context.ExecutionContext | 974 | import org.moqui.context.ExecutionContext |
| 975 | import org.moqui.impl.context.UserFacadeImpl.UserInfo | ||
| 949 | 976 | ||
| 950 | ExecutionContext ec = context.ec | 977 | ExecutionContext ec = context.ec |
| 951 | 978 | ... | ... |
| ... | @@ -416,20 +416,10 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -416,20 +416,10 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 416 | return | 416 | return |
| 417 | } | 417 | } |
| 418 | 418 | ||
| 419 | // Verify user has access to this Visit - more permissive for testing | 419 | // Verify user has access to this Visit - rely on Moqui security |
| 420 | logger.info("Session validation: visit.userId=${visit.userId}, ec.user.userId=${ec.user.userId}, ec.user.username=${ec.user.username}") | 420 | logger.info("Session validation: visit.userId=${visit.userId}, ec.user.userId=${ec.user.userId}, ec.user.username=${ec.user.username}") |
| 421 | logger.info("DEBUG2: visit.userId exists=${visit.userId != null}, ec.user.userId exists=${ec.user.userId != null}, notEqual=${visit.userId?.toString() != ec.user.userId?.toString()}") | ||
| 422 | if (visit.userId && ec.user.userId && visit.userId.toString() != ec.user.userId.toString()) { | 421 | if (visit.userId && ec.user.userId && visit.userId.toString() != ec.user.userId.toString()) { |
| 423 | logger.warn("Visit userId ${visit.userId} doesn't match current user userId ${ec.user.userId}") | 422 | logger.warn("Visit userId ${visit.userId} doesn't match current user userId ${ec.user.userId} - access denied") |
| 424 | |||
| 425 | // Special case: MCP services run with ADMIN privileges but authenticate as MCP_USER or MCP_BUSINESS | ||
| 426 | boolean specialMcpCase = visit.userId == "ADMIN" && (ec.user.userId == "MCP_USER" || ec.user.userId == "MCP_BUSINESS") | ||
| 427 | logger.info("DEBUG: visit.userId='${visit.userId}' (class: ${visit.userId?.class?.name}), ec.user.userId='${ec.user.userId}' (class: ${ec.user.userId?.class?.name}), specialMcpCase=${specialMcpCase}") | ||
| 428 | if (specialMcpCase) { | ||
| 429 | logger.info("Allowing MCP service access: Visit created with ADMIN, accessed by ${ec.user.userId}") | ||
| 430 | } else if (visit.userCreated == "Y" && ec.user.username) { | ||
| 431 | logger.info("Allowing access for user ${ec.user.username} to Visit ${sessionId}") | ||
| 432 | } else { | ||
| 433 | response.setContentType("application/json") | 423 | response.setContentType("application/json") |
| 434 | response.setCharacterEncoding("UTF-8") | 424 | response.setCharacterEncoding("UTF-8") |
| 435 | response.setStatus(HttpServletResponse.SC_FORBIDDEN) | 425 | response.setStatus(HttpServletResponse.SC_FORBIDDEN) |
| ... | @@ -439,7 +429,6 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -439,7 +429,6 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 439 | ])) | 429 | ])) |
| 440 | return | 430 | return |
| 441 | } | 431 | } |
| 442 | } | ||
| 443 | 432 | ||
| 444 | // Create session wrapper for this Visit | 433 | // Create session wrapper for this Visit |
| 445 | VisitBasedMcpSession session = new VisitBasedMcpSession(visit, response.writer, ec) | 434 | VisitBasedMcpSession session = new VisitBasedMcpSession(visit, response.writer, ec) |
| ... | @@ -689,18 +678,8 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -689,18 +678,8 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 689 | // Allow access if: | 678 | // Allow access if: |
| 690 | // 1. Visit userId matches current user, OR | 679 | // 1. Visit userId matches current user, OR |
| 691 | // 2. Visit was created with ADMIN (for privileged access) but current user is MCP_USER (actual authenticated user) | 680 | // 2. Visit was created with ADMIN (for privileged access) but current user is MCP_USER (actual authenticated user) |
| 692 | boolean accessAllowed = false | 681 | // Rely on Moqui security - only allow access if visit and current user match |
| 693 | if (visit.userId && ec.user.userId) { | 682 | if (!visit.userId || !ec.user.userId || visit.userId.toString() != ec.user.userId.toString()) { |
| 694 | if (visit.userId.toString() == ec.user.userId.toString()) { | ||
| 695 | accessAllowed = true | ||
| 696 | } else if (visit.userId.toString() == "ADMIN" && (ec.user.userId.toString() == "MCP_USER" || ec.user.userId.toString() == "MCP_BUSINESS")) { | ||
| 697 | // Special case: MCP services run with ADMIN privileges but authenticate as MCP_USER or MCP_BUSINESS | ||
| 698 | accessAllowed = true | ||
| 699 | logger.info("Allowing MCP privileged access: Visit created with ADMIN, accessed by ${ec.user.userId}") | ||
| 700 | } | ||
| 701 | } | ||
| 702 | |||
| 703 | if (!accessAllowed) { | ||
| 704 | response.setStatus(HttpServletResponse.SC_FORBIDDEN) | 683 | response.setStatus(HttpServletResponse.SC_FORBIDDEN) |
| 705 | response.setContentType("application/json") | 684 | response.setContentType("application/json") |
| 706 | response.writer.write(groovy.json.JsonOutput.toJson([ | 685 | response.writer.write(groovy.json.JsonOutput.toJson([ | ... | ... |
src/main/webapp/WEB-INF/web.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- | ||
| 3 | This software is in the public domain under CC0 1.0 Universal plus a | ||
| 4 | Grant of Patent License. | ||
| 5 | |||
| 6 | To the extent possible under law, author(s) have dedicated all | ||
| 7 | copyright and related and neighboring rights to this software to the | ||
| 8 | public domain worldwide. This software is distributed without any | ||
| 9 | warranty. | ||
| 10 | |||
| 11 | You should have received a copy of the CC0 Public Domain Dedication | ||
| 12 | along with this software (see the LICENSE.md file). If not, see | ||
| 13 | <http://creativecommons.org/publicdomain/zero/1.0/>. | ||
| 14 | --> | ||
| 15 | |||
| 16 | <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" | ||
| 17 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| 18 | xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee | ||
| 19 | http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" | ||
| 20 | version="4.0"> | ||
| 21 | |||
| 22 | <!-- Service-Based MCP Servlet Configuration --> | ||
| 23 | <servlet> | ||
| 24 | <servlet-name>EnhancedMcpServlet</servlet-name> | ||
| 25 | <servlet-class>org.moqui.mcp.EnhancedMcpServlet</servlet-class> | ||
| 26 | |||
| 27 | <init-param> | ||
| 28 | <param-name>keepAliveIntervalSeconds</param-name> | ||
| 29 | <param-value>30</param-value> | ||
| 30 | </init-param> | ||
| 31 | <init-param> | ||
| 32 | <param-name>maxConnections</param-name> | ||
| 33 | <param-value>100</param-value> | ||
| 34 | </init-param> | ||
| 35 | |||
| 36 | <!-- Enable async support for SSE --> | ||
| 37 | <async-supported>true</async-supported> | ||
| 38 | |||
| 39 | <!-- Load on startup --> | ||
| 40 | <load-on-startup>5</load-on-startup> | ||
| 41 | </servlet> | ||
| 42 | |||
| 43 | <servlet-mapping> | ||
| 44 | <servlet-name>EnhancedMcpServlet</servlet-name> | ||
| 45 | <url-pattern>/mcp/*</url-pattern> | ||
| 46 | </servlet-mapping> | ||
| 47 | |||
| 48 | <!-- Session Configuration --> | ||
| 49 | <session-config> | ||
| 50 | <session-timeout>30</session-timeout> | ||
| 51 | <cookie-config> | ||
| 52 | <http-only>true</http-only> | ||
| 53 | <secure>false</secure> | ||
| 54 | </cookie-config> | ||
| 55 | </session-config> | ||
| 56 | |||
| 57 | <!-- Security Constraints (optional - uncomment if needed) --> | ||
| 58 | <!-- | ||
| 59 | <security-constraint> | ||
| 60 | <web-resource-collection> | ||
| 61 | <web-resource-name>MCP Endpoints</web-resource-name> | ||
| 62 | <url-pattern>/sse/*</url-pattern> | ||
| 63 | <url-pattern>/mcp/message/*</url-pattern> | ||
| 64 | <url-pattern>/rpc/*</url-pattern> | ||
| 65 | </web-resource-collection> | ||
| 66 | <auth-constraint> | ||
| 67 | <role-name>admin</role-name> | ||
| 68 | </auth-constraint> | ||
| 69 | </security-constraint> | ||
| 70 | |||
| 71 | <login-config> | ||
| 72 | <auth-method>BASIC</auth-method> | ||
| 73 | <realm-name>Moqui MCP</realm-name> | ||
| 74 | </login-config> | ||
| 75 | --> | ||
| 76 | |||
| 77 | <!-- MIME Type Mappings --> | ||
| 78 | <mime-mapping> | ||
| 79 | <extension>json</extension> | ||
| 80 | <mime-type>application/json</mime-type> | ||
| 81 | </mime-mapping> | ||
| 82 | |||
| 83 | <!-- Default Welcome Files --> | ||
| 84 | <welcome-file-list> | ||
| 85 | <welcome-file>index.html</welcome-file> | ||
| 86 | <welcome-file>index.jsp</welcome-file> | ||
| 87 | </welcome-file-list> | ||
| 88 | |||
| 89 | </web-app> |
-
Please register or sign in to post a comment