Skip to main content
Glama

SAP OData to MCP Server

by Raistlin82
test-ui-authorization.js19 kB
#!/usr/bin/env node /** * UI Tools Authorization Test Suite * Tests authorization enforcement for UI tools with different scopes */ import chalk from 'chalk'; export class UIAuthorizationTest { constructor(options = {}) { this.verbose = options.verbose || false; this.results = { passed: 0, failed: 0, tests: [] }; } log(message, type = 'info') { if (!this.verbose && type === 'debug') return; const prefix = { info: chalk.blue('ℹ'), success: chalk.green('✅'), error: chalk.red('❌'), debug: chalk.gray('🔍'), warning: chalk.yellow('⚠️') }[type] || ''; console.log(`${prefix} ${message}`); } recordResult(testName, result) { this.results.tests.push({ name: testName, ...result }); if (result.success) { this.results.passed++; this.log(`${testName}: PASSED`, 'success'); } else { this.results.failed++; this.log(`${testName}: FAILED - ${result.error}`, 'error'); if (result.details && this.verbose) { this.log(`Details: ${JSON.stringify(result.details, null, 2)}`, 'debug'); } } } async runTests() { this.log('🔒 Testing UI Tools Authorization Enforcement', 'info'); console.log(chalk.gray('=' .repeat(55))); try { // Test 1: MCPAuth getRequiredScope method for UI tools const requiredScopeResult = await this.testGetRequiredScope(); this.recordResult('UI Tools Required Scope Mapping', requiredScopeResult); // Test 2: hasRequiredScope validation for UI scopes const hasRequiredScopeResult = await this.testHasRequiredScope(); this.recordResult('UI Scope Authorization Validation', hasRequiredScopeResult); // Test 3: Authorization scenarios for different role collections const authScenariosResult = await this.testAuthorizationScenarios(); this.recordResult('Role Collection Authorization Scenarios', authScenariosResult); // Test 4: Cross-tool authorization (user with mixed permissions) const crossToolResult = await this.testCrossToolAuthorization(); this.recordResult('Cross-Tool Authorization', crossToolResult); return this.results; } catch (error) { this.log(`Test suite failed: ${error.message}`, 'error'); return this.results; } } async testGetRequiredScope() { try { this.log('🔍 Testing getRequiredScope method for UI tools...', 'debug'); const MockMCPAuth = await this.createMockMCPAuth(); const auth = new MockMCPAuth(); const testCases = [ { toolName: 'ui-form-generator', expectedScope: 'ui.forms' }, { toolName: 'ui-data-grid', expectedScope: 'ui.grids' }, { toolName: 'ui-dashboard-composer', expectedScope: 'ui.dashboards' }, { toolName: 'ui-workflow-builder', expectedScope: 'ui.workflows' }, { toolName: 'ui-report-builder', expectedScope: 'ui.reports' }, // Test non-UI tools still work { toolName: 'search-sap-services', expectedScope: null }, { toolName: 'execute-entity-operation', expectedScope: null } // Depends on operation parameter ]; for (const testCase of testCases) { const actualScope = auth.getRequiredScope(testCase.toolName); if (actualScope !== testCase.expectedScope) { return { success: false, error: `Wrong scope for ${testCase.toolName}`, details: { tool: testCase.toolName, expected: testCase.expectedScope, actual: actualScope } }; } } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async testHasRequiredScope() { try { this.log('🔍 Testing hasRequiredScope for UI authorization...', 'debug'); const MockMCPAuth = await this.createMockMCPAuth(); const auth = new MockMCPAuth(); const testCases = [ { name: 'Admin user accessing UI forms tool', userScopes: ['btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.admin'], requiredScope: 'ui.forms', expected: true }, { name: 'MCPUIUser accessing forms tool', userScopes: ['btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.forms'], requiredScope: 'ui.forms', expected: true }, { name: 'MCPUIUser accessing dashboards tool (denied)', userScopes: ['btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.forms'], requiredScope: 'ui.dashboards', expected: false }, { name: 'MCPUser accessing UI tool (denied)', userScopes: ['btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.read', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.write'], requiredScope: 'ui.forms', expected: false }, { name: 'MCPUIDesigner accessing all UI tools', userScopes: [ 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.forms', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.grids', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.dashboards', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.workflows', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.reports' ], requiredScope: 'ui.workflows', expected: true } ]; for (const testCase of testCases) { const result = auth.hasRequiredScope(testCase.userScopes, testCase.requiredScope); if (result !== testCase.expected) { return { success: false, error: `Authorization failed for: ${testCase.name}`, details: { testCase: testCase, actualResult: result } }; } } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async testAuthorizationScenarios() { try { this.log('🔍 Testing real-world authorization scenarios...', 'debug'); const MockMCPAuth = await this.createMockMCPAuth(); const auth = new MockMCPAuth(); // Scenario 1: Data analyst with dashboard and report access const analystScopes = [ 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.read', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.discover', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.dashboards', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.reports' ]; const analystTests = [ { tool: 'ui-dashboard-composer', scope: 'ui.dashboards', expected: true }, { tool: 'ui-report-builder', scope: 'ui.reports', expected: true }, { tool: 'ui-form-generator', scope: 'ui.forms', expected: false }, { tool: 'ui-workflow-builder', scope: 'ui.workflows', expected: false } ]; for (const test of analystTests) { const hasAccess = auth.hasRequiredScope(analystScopes, test.scope); if (hasAccess !== test.expected) { return { success: false, error: `Analyst authorization failed for ${test.tool}`, details: { test, actualResult: hasAccess } }; } } // Scenario 2: Form designer with form and grid access only const designerScopes = [ 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.read', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.discover', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.forms', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.grids' ]; const designerTests = [ { tool: 'ui-form-generator', scope: 'ui.forms', expected: true }, { tool: 'ui-data-grid', scope: 'ui.grids', expected: true }, { tool: 'ui-dashboard-composer', scope: 'ui.dashboards', expected: false }, { tool: 'ui-report-builder', scope: 'ui.reports', expected: false } ]; for (const test of designerTests) { const hasAccess = auth.hasRequiredScope(designerScopes, test.scope); if (hasAccess !== test.expected) { return { success: false, error: `Designer authorization failed for ${test.tool}`, details: { test, actualResult: hasAccess } }; } } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async testCrossToolAuthorization() { try { this.log('🔍 Testing cross-tool authorization scenarios...', 'debug'); const MockMCPAuth = await this.createMockMCPAuth(); const auth = new MockMCPAuth(); // User with mixed permissions: read/write for data + some UI tools const mixedScopes = [ 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.read', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.write', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.discover', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.forms', 'btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.dashboards' ]; const crossToolTests = [ // Should work: data operations { operation: 'read', scope: 'read', expected: true }, { operation: 'write', scope: 'write', expected: true }, { operation: 'discover', scope: 'discover', expected: true }, // Should work: allowed UI tools { operation: 'ui-form-generator', scope: 'ui.forms', expected: true }, { operation: 'ui-dashboard-composer', scope: 'ui.dashboards', expected: true }, // Should not work: unauthorized UI tools { operation: 'ui-data-grid', scope: 'ui.grids', expected: false }, { operation: 'ui-workflow-builder', scope: 'ui.workflows', expected: false }, { operation: 'ui-report-builder', scope: 'ui.reports', expected: false }, // Should not work: admin/delete operations { operation: 'delete', scope: 'delete', expected: false } ]; for (const test of crossToolTests) { const hasAccess = auth.hasRequiredScope(mixedScopes, test.scope); if (hasAccess !== test.expected) { return { success: false, error: `Cross-tool authorization failed for ${test.operation}`, details: { test, actualResult: hasAccess } }; } } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async createMockMCPAuth() { // Create a minimal mock of MCPAuthManager for testing class MockMCPAuth { getRequiredScope(toolName, args) { // Special handling for execute-entity-operation - check operation parameter if (toolName === 'execute-entity-operation' && args?.operation) { const operation = args.operation; switch (operation) { case 'read': case 'read-single': return 'read'; case 'create': case 'update': case 'patch': return 'write'; case 'delete': return 'delete'; default: return 'read'; } } const scopeMapping = { // Read operations 'sap_odata_read_entity': 'read', 'sap_odata_query_entities': 'read', 'sap_odata_get_metadata': 'read', // Write operations 'sap_odata_create_entity': 'write', 'sap_odata_update_entity': 'write', 'sap_odata_patch_entity': 'write', // Delete operations 'sap_odata_delete_entity': 'delete', // Admin operations 'sap_admin_*': 'admin', 'system_admin_*': 'admin', // UI Tools - require specific UI scopes 'ui-form-generator': 'ui.forms', 'ui-data-grid': 'ui.grids', 'ui-dashboard-composer': 'ui.dashboards', 'ui-workflow-builder': 'ui.workflows', 'ui-report-builder': 'ui.reports' }; // Check for exact match first if (scopeMapping[toolName]) { return scopeMapping[toolName]; } // Check for wildcard matches for (const [pattern, scope] of Object.entries(scopeMapping)) { if (pattern.endsWith('*') && toolName.startsWith(pattern.slice(0, -1))) { return scope; } } // Default to read for most operations if (toolName.startsWith('sap_')) { return 'read'; } return null; } hasRequiredScope(userScopes, requiredScope) { // Admin scope has all permissions if (userScopes.includes('admin')) { return true; } // Check for exact scope match (including full scope names with prefix) if (userScopes.includes(requiredScope)) { return true; } // For UI scopes, also check with full scope prefix if (requiredScope.startsWith('ui.')) { const xsappname = process.env.XSUAA_XSAPPNAME || 'btp-sap-odata-to-mcp-server'; const fullScopeName = `${xsappname}.${requiredScope}`; if (userScopes.includes(fullScopeName)) { return true; } } // Check scope hierarchy (write includes read, delete includes write and read) const scopeHierarchy = { 'read': ['read'], 'write': ['read', 'write'], 'delete': ['read', 'write', 'delete'], 'admin': ['read', 'write', 'delete', 'admin', 'discover', 'ui.forms', 'ui.grids', 'ui.dashboards', 'ui.workflows', 'ui.reports'] }; for (const userScope of userScopes) { // Handle full scope names with prefix (including complex BTP formats) let scopeToCheck = userScope; // Handle BTP scope format: app-name-space!tenant.scope or app-name.scope // Extract the scope part after the last occurrence of app name const lastDotIndex = userScope.lastIndexOf('.'); if (lastDotIndex !== -1) { const afterLastDot = userScope.substring(lastDotIndex + 1); // Check if this looks like a UI scope (contains ui prefix in the remaining part) if (userScope.includes('.ui.')) { // For UI scopes like "app.ui.forms", extract "ui.forms" const uiIndex = userScope.indexOf('.ui.'); scopeToCheck = userScope.substring(uiIndex + 1); // +1 to skip the first dot } else { // For simple scopes like "app.read", extract "read" scopeToCheck = afterLastDot; } } const allowedScopes = scopeHierarchy[scopeToCheck] || [scopeToCheck]; if (allowedScopes.includes(requiredScope)) { return true; } } return false; } } return MockMCPAuth; } } // Run tests if called directly if (import.meta.url === `file://${process.argv[1]}`) { const test = new UIAuthorizationTest({ verbose: true }); test.runTests().then(results => { console.log('\n' + chalk.bold('UI Authorization Test Results:')); console.log(chalk.green(`✅ Passed: ${results.passed}`)); console.log(chalk.red(`❌ Failed: ${results.failed}`)); console.log(chalk.blue(`📊 Total: ${results.tests.length}`)); if (results.failed > 0) { console.log('\n' + chalk.red('Failed Tests:')); results.tests.filter(test => !test.success).forEach(test => { console.log(chalk.red(` • ${test.name}: ${test.error}`)); }); process.exit(1); } else { console.log('\n' + chalk.green('🎉 All UI authorization tests passed!')); process.exit(0); } }).catch(error => { console.error(chalk.red('Test execution failed:'), error); process.exit(1); }); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Raistlin82/btp-sap-odata-to-mcp-server-optimized'

If you have feedback or need assistance with the MCP directory API, please join our Discord server