UNPKG

@ithena-one/mcp-governance

Version:

Governance layer (Identity, RBAC, Credentials, Audit, Logging, Tracing) for Model Context Protocol (MCP) servers.

207 lines 10.3 kB
/** * Derives a permission string based on the MCP method and parameters. * Examples: * - `tool:call:<tool_name>` * - `resource:read:<uri>` (if fixed URI) * - `resource:read:<uri_template>` (if template URI) * - `resource:list` * - `resource:templates:list` * - `prompt:get:<prompt_name>` * - `prompt:list` * Returns null for protocol-level messages like 'initialize', 'ping'. */ export function defaultDerivePermission(request, _transportContext) { const method = request.method; const params = request.params; // --- MODIFICATION START --- // Handle tool calls more generically if (method.startsWith('tools/')) { // Convert 'tools/callSomething' to 'tool:callSomething' // Assumes the part after 'tools/' is the action/name // Avoid double 'call' if method is 'tools/call' and params.name exists if (method === 'tools/call' && params?.name) { return `tool:call:${params.name}`; } return `tool:${method.substring('tools/'.length)}`; } // Handle resource reads more generically if (method.startsWith('resources/')) { const action = method.substring('resources/'.length); // Handle specific cases first if they have different logic or param needs if (action === 'read') { // Append URI if present, handle case where uri might be missing but method is still 'resources/read' return `resource:read${params?.uri ? `:${params.uri}` : ''}`; } if (action === 'subscribe') { // Append URI only if present return `resource:subscribe${params?.uri ? `:${params.uri}` : ''}`; } if (action === 'unsubscribe') { // Append URI only if present return `resource:unsubscribe${params?.uri ? `:${params.uri}` : ''}`; } // General conversion for simple patterns like 'resources/list' -> 'resource:list' // Only replace the *first* slash if applicable, or just use the action if (action.includes('/')) { return `resource:${action.replace('/', ':')}`; // Basic conversion for cases like templates/list } else { return `resource:${action}`; // For simple cases like 'list' } } // Handle prompts more generically if (method.startsWith('prompts/')) { const action = method.substring('prompts/'.length); if (action === 'get' && params?.name) { return `prompt:get:${params.name}`; } // Convert 'prompts/list' to 'prompt:list' return `prompt:${action.replace('/', ':')}`; } // --- MODIFICATION END --- // Keep specific cases or fall back to original switch if needed switch (method) { // Cases handled by the generic logic above or simple enough not to need explicit handling here anymore // case 'tools/call': // Handled above // case 'tools/list': // Handled above (implicitly by 'tools/') // case 'resources/read': // Handled above // case 'resources/list': // Handled above // case 'resources/templates/list': // Handled above // case 'resources/subscribe': // Handled above // case 'resources/unsubscribe': // Handled above // case 'prompts/get': // Handled above // case 'prompts/list': // Handled above case 'completion/complete': { // Complex logic, keep specific const ref = params?.ref; if (ref?.type === 'ref/prompt') return `completion:prompt:${ref.name}:${params?.argument?.name ?? '*'}`; if (ref?.type === 'ref/resource') return `completion:resource:${ref.uri}:${params?.argument?.name ?? '*'}`; return 'completion:complete'; } // Other specific cases case 'sampling/createMessage': return 'sampling:createMessage'; case 'roots/list': return 'roots:list'; case 'logging/setLevel': return 'logging:setLevel'; // Protocol messages needing no permission check case 'initialize': case 'ping': return null; default: // Fallback for truly unknown methods (might still be wrong format) // Consider logging a warning here if reached console.warn(`[defaultDerivePermission] Applying default permission format for unknown method: ${method}`); // Attempt a basic conversion just in case if (method.includes('/')) { const parts = method.split('/'); return `${parts[0]}:${parts.slice(1).join(':')}`; } return method; // Or return null/throw error if unknown methods are disallowed } } // --- Default In-Memory Stores (for testing/development) --- /** * Simple in-memory RoleStore implementation. */ export class InMemoryRoleStore { constructor(initialRoles = {}) { this.rolesByUser = {}; for (const [userId, roles] of Object.entries(initialRoles)) { this.rolesByUser[userId] = new Set(roles); } } async getRoles(identity, _opCtx) { const userId = typeof identity === 'string' ? identity : identity?.id; if (!userId) { return []; } return Array.from(this.rolesByUser[userId] ?? []); } /** Adds roles to a user. */ addUserRoles(userId, roles) { if (!this.rolesByUser[userId]) { this.rolesByUser[userId] = new Set(); } roles.forEach(role => this.rolesByUser[userId].add(role)); } /** Removes roles from a user. */ removeUserRoles(userId, roles) { if (!this.rolesByUser[userId]) { return; } roles.forEach(role => this.rolesByUser[userId].delete(role)); } } /** * Simple in-memory PermissionStore implementation. */ export class InMemoryPermissionStore { constructor(initialPermissions = {}) { this.logger = console; // Keep using console for simplicity this.permissionsByRole = {}; for (const [role, permissions] of Object.entries(initialPermissions)) { this.permissionsByRole[role] = new Set(permissions); } this.logger.log(`[InMemoryPermissionStore CONSTRUCTOR] Initialized with permissions:`, JSON.stringify(this.permissionsByRole, (key, value) => value instanceof Set ? [...value] : value)); } async hasPermission(role, permission, opCtx) { const scopedLogger = opCtx?.logger || this.logger; scopedLogger.debug(`[InMemoryPermissionStore HASPERMISSION_ENTRY] Checking role="${role}" (type: ${typeof role}), permission="${permission}" (type: ${typeof permission})`); const permissionsForRole = this.permissionsByRole[role]; scopedLogger.debug(`[InMemoryPermissionStore HASPERMISSION_STATE] Permissions Set found for role "${role}": ${permissionsForRole ? `Set(${[...permissionsForRole].map(p => JSON.stringify(p)).join(', ')})` : 'undefined'}`); if (!permissionsForRole) { scopedLogger.debug(`[InMemoryPermissionStore HASPERMISSION_RESULT] No permissions found for role "${role}". Denying.`); return false; // Exit early if no set exists for the role } // Check for wildcard '*' separately if (permissionsForRole.has('*')) { scopedLogger.debug(`[InMemoryPermissionStore HASPERMISSION_RESULT] Role "${role}" has wildcard access. Granting.`); return true; } // *** DETAILED MANUAL CHECK *** let manualMatchFound = false; scopedLogger.debug(`[InMemoryPermissionStore MANUAL_CHECK] Iterating through permissions for role "${role}":`); for (const storedPermission of permissionsForRole) { const directComparison = storedPermission === permission; scopedLogger.debug(` - Comparing input "${permission}" (len: ${permission.length}) with stored "${storedPermission}" (len: ${storedPermission.length}). Strict Equal (===): ${directComparison}`); // Optional: Log character codes for very deep debugging // scopedLogger.debug(` Input char codes: ${permission.split('').map(c => c.charCodeAt(0)).join(',')}`); // scopedLogger.debug(` Stored char codes: ${storedPermission.split('').map(c => c.charCodeAt(0)).join(',')}`); if (directComparison) { manualMatchFound = true; // Don't break here yet, log all comparisons } } scopedLogger.debug(`[InMemoryPermissionStore MANUAL_CHECK_RESULT] Manual iteration found match: ${manualMatchFound}`); // *** END DETAILED MANUAL CHECK *** // Log the result of Set.has() again for comparison const setResult = permissionsForRole.has(permission); scopedLogger.debug(`[InMemoryPermissionStore SET_HAS_RESULT] Set.has("${permission}") returned: ${setResult}`); // Return the result of Set.has() as before, but now we have more logs return setResult; } /** Adds a permission to a role. */ addPermission(role, permission) { this.logger.debug(`[InMemoryPermissionStore] Adding permission "${permission}" to role "${role}"`); if (!this.permissionsByRole[role]) { this.permissionsByRole[role] = new Set(); this.logger.debug(`[InMemoryPermissionStore] Created new permission set for role "${role}"`); } this.permissionsByRole[role].add(permission); this.logger.debug(`[InMemoryPermissionStore] Successfully added permission "${permission}" to role "${role}"`); } /** Removes a permission from a role. */ removePermission(role, permission) { this.logger.debug(`[InMemoryPermissionStore] Attempting to remove permission "${permission}" from role "${role}"`); if (!this.permissionsByRole[role]) { this.logger.debug(`[InMemoryPermissionStore] No permissions found for role "${role}" - nothing to remove`); return; } this.permissionsByRole[role].delete(permission); this.logger.debug(`[InMemoryPermissionStore] Successfully removed permission "${permission}" from role "${role}"`); } } //# sourceMappingURL=permissions.js.map