Path: blob/main/src/vs/workbench/services/authentication/test/browser/authenticationMcpAccessService.test.ts
5221 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';7import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';8import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';9import { IProductService } from '../../../../../platform/product/common/productService.js';10import { TestStorageService, TestProductService } from '../../../../test/common/workbenchTestServices.js';11import { AuthenticationMcpAccessService, AllowedMcpServer, IAuthenticationMcpAccessService } from '../../browser/authenticationMcpAccessService.js';1213suite('AuthenticationMcpAccessService', () => {14const disposables = ensureNoDisposablesAreLeakedInTestSuite();1516let instantiationService: TestInstantiationService;17let storageService: TestStorageService;18let productService: IProductService & { trustedMcpAuthAccess?: string[] | Record<string, string[]> };19let authenticationMcpAccessService: IAuthenticationMcpAccessService;2021setup(() => {22instantiationService = disposables.add(new TestInstantiationService());2324// Set up storage service25storageService = disposables.add(new TestStorageService());26instantiationService.stub(IStorageService, storageService);2728// Set up product service with no trusted servers by default29productService = { ...TestProductService };30instantiationService.stub(IProductService, productService);3132// Create the service instance33authenticationMcpAccessService = disposables.add(instantiationService.createInstance(AuthenticationMcpAccessService));34});3536suite('isAccessAllowed', () => {37test('returns undefined for unknown MCP server with no product configuration', () => {38const result = authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'unknown-server');39assert.strictEqual(result, undefined);40});4142test('returns true for trusted MCP server from product.json (array format)', () => {43productService.trustedMcpAuthAccess = ['trusted-server-1', 'trusted-server-2'];4445const result = authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'trusted-server-1');46assert.strictEqual(result, true);47});4849test('returns true for trusted MCP server from product.json (object format)', () => {50productService.trustedMcpAuthAccess = {51'github': ['github-server'],52'microsoft': ['microsoft-server']53};5455const result1 = authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'github-server');56assert.strictEqual(result1, true);5758const result2 = authenticationMcpAccessService.isAccessAllowed('microsoft', '[email protected]', 'microsoft-server');59assert.strictEqual(result2, true);60});6162test('returns undefined for MCP server not in trusted list', () => {63productService.trustedMcpAuthAccess = ['trusted-server'];6465const result = authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'untrusted-server');66assert.strictEqual(result, undefined);67});6869test('returns stored allowed state when server is in storage', () => {70// Add server to storage71authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [{72id: 'stored-server',73name: 'Stored Server',74allowed: false75}]);7677const result = authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'stored-server');78assert.strictEqual(result, false);79});8081test('returns true for server in storage with allowed=true', () => {82authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [{83id: 'allowed-server',84name: 'Allowed Server',85allowed: true86}]);8788const result = authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'allowed-server');89assert.strictEqual(result, true);90});9192test('returns true for server in storage with undefined allowed property (legacy behavior)', () => {93// Simulate legacy data where allowed property didn't exist94const legacyServer: AllowedMcpServer = {95id: 'legacy-server',96name: 'Legacy Server'97// allowed property is undefined98};99100authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [legacyServer]);101102const result = authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'legacy-server');103assert.strictEqual(result, true);104});105106test('product.json trusted servers take precedence over storage', () => {107productService.trustedMcpAuthAccess = ['product-trusted-server'];108109// Try to store the same server as not allowed110authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [{111id: 'product-trusted-server',112name: 'Product Trusted Server',113allowed: false114}]);115116// Product.json should take precedence117const result = authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'product-trusted-server');118assert.strictEqual(result, true);119});120});121122suite('readAllowedMcpServers', () => {123test('returns empty array when no data exists', () => {124const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');125assert.strictEqual(result.length, 0);126});127128test('returns stored MCP servers', () => {129const servers: AllowedMcpServer[] = [130{ id: 'server1', name: 'Server 1', allowed: true },131{ id: 'server2', name: 'Server 2', allowed: false }132];133134authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', servers);135136const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');137assert.strictEqual(result.length, 2);138assert.strictEqual(result[0].id, 'server1');139assert.strictEqual(result[0].allowed, true);140assert.strictEqual(result[1].id, 'server2');141assert.strictEqual(result[1].allowed, false);142});143144test('includes trusted servers from product.json (array format)', () => {145productService.trustedMcpAuthAccess = ['trusted-server-1', 'trusted-server-2'];146147const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');148assert.strictEqual(result.length, 2);149150const trustedServer1 = result.find(s => s.id === 'trusted-server-1');151assert.ok(trustedServer1);152assert.strictEqual(trustedServer1.allowed, true);153assert.strictEqual(trustedServer1.trusted, true);154assert.strictEqual(trustedServer1.name, 'trusted-server-1'); // Should default to ID155156const trustedServer2 = result.find(s => s.id === 'trusted-server-2');157assert.ok(trustedServer2);158assert.strictEqual(trustedServer2.allowed, true);159assert.strictEqual(trustedServer2.trusted, true);160});161162test('includes trusted servers from product.json (object format)', () => {163productService.trustedMcpAuthAccess = {164'github': ['github-server'],165'microsoft': ['microsoft-server']166};167168const githubResult = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');169assert.strictEqual(githubResult.length, 1);170assert.strictEqual(githubResult[0].id, 'github-server');171assert.strictEqual(githubResult[0].trusted, true);172173const microsoftResult = authenticationMcpAccessService.readAllowedMcpServers('microsoft', '[email protected]');174assert.strictEqual(microsoftResult.length, 1);175assert.strictEqual(microsoftResult[0].id, 'microsoft-server');176assert.strictEqual(microsoftResult[0].trusted, true);177178// Provider not in trusted list should return empty (no stored servers)179const unknownResult = authenticationMcpAccessService.readAllowedMcpServers('unknown', '[email protected]');180assert.strictEqual(unknownResult.length, 0);181});182183test('merges stored servers with trusted servers from product.json', () => {184productService.trustedMcpAuthAccess = ['trusted-server'];185186// Add some stored servers187authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [188{ id: 'stored-server', name: 'Stored Server', allowed: false }189]);190191const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');192assert.strictEqual(result.length, 2);193194const trustedServer = result.find(s => s.id === 'trusted-server');195assert.ok(trustedServer);196assert.strictEqual(trustedServer.trusted, true);197assert.strictEqual(trustedServer.allowed, true);198199const storedServer = result.find(s => s.id === 'stored-server');200assert.ok(storedServer);201assert.strictEqual(storedServer.trusted, undefined);202assert.strictEqual(storedServer.allowed, false);203});204205test('updates existing stored server to be trusted when it appears in product.json', () => {206// First add a server as stored (not trusted)207authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [208{ id: 'server-1', name: 'Server 1', allowed: false }209]);210211// Then make it trusted via product.json212productService.trustedMcpAuthAccess = ['server-1'];213214const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');215assert.strictEqual(result.length, 1);216217const server = result[0];218assert.strictEqual(server.id, 'server-1');219assert.strictEqual(server.allowed, true); // Should be overridden to true220assert.strictEqual(server.trusted, true); // Should be marked as trusted221assert.strictEqual(server.name, 'Server 1'); // Should keep existing name222});223224test('handles malformed JSON in storage gracefully', () => {225// Manually corrupt the storage226storageService.store('[email protected]', 'invalid json', StorageScope.APPLICATION, StorageTarget.USER);227228// Should return empty array instead of throwing229const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');230assert.strictEqual(result.length, 0);231});232233test('handles non-array product.json configuration gracefully', () => {234// Set up invalid configuration235// eslint-disable-next-line local/code-no-any-casts236productService.trustedMcpAuthAccess = 'invalid-string' as any;237238const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');239assert.strictEqual(result.length, 0);240});241});242243suite('updateAllowedMcpServers', () => {244test('stores new MCP servers', () => {245const servers: AllowedMcpServer[] = [246{ id: 'server1', name: 'Server 1', allowed: true },247{ id: 'server2', name: 'Server 2', allowed: false }248];249250authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', servers);251252const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');253assert.strictEqual(result.length, 2);254assert.strictEqual(result[0].id, 'server1');255assert.strictEqual(result[1].id, 'server2');256});257258test('updates existing MCP server allowed status', () => {259// First add a server260authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [261{ id: 'server1', name: 'Server 1', allowed: true }262]);263264// Then update its allowed status265authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [266{ id: 'server1', name: 'Server 1', allowed: false }267]);268269const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');270assert.strictEqual(result.length, 1);271assert.strictEqual(result[0].allowed, false);272});273274test('updates existing MCP server name when new name is provided', () => {275// First add a server with default name276authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [277{ id: 'server1', name: 'server1', allowed: true }278]);279280// Then update with a proper name281authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [282{ id: 'server1', name: 'My Server', allowed: true }283]);284285const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');286assert.strictEqual(result.length, 1);287assert.strictEqual(result[0].name, 'My Server');288});289290test('does not update name when new name is same as ID', () => {291// First add a server with a proper name292authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [293{ id: 'server1', name: 'My Server', allowed: true }294]);295296// Then try to update with ID as name (should keep existing name)297authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [298{ id: 'server1', name: 'server1', allowed: false }299]);300301const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');302assert.strictEqual(result.length, 1);303assert.strictEqual(result[0].name, 'My Server'); // Should keep original name304assert.strictEqual(result[0].allowed, false); // But allowed status should update305});306307test('adds new servers while preserving existing ones', () => {308// First add one server309authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [310{ id: 'server1', name: 'Server 1', allowed: true }311]);312313// Then add another server314authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [315{ id: 'server2', name: 'Server 2', allowed: false }316]);317318const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');319assert.strictEqual(result.length, 2);320321const server1 = result.find(s => s.id === 'server1');322const server2 = result.find(s => s.id === 'server2');323assert.ok(server1);324assert.ok(server2);325assert.strictEqual(server1.allowed, true);326assert.strictEqual(server2.allowed, false);327});328329test('does not store trusted servers from product.json', () => {330productService.trustedMcpAuthAccess = ['trusted-server'];331332// Try to update a trusted server333authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [334{ id: 'trusted-server', name: 'Trusted Server', allowed: false, trusted: true },335{ id: 'user-server', name: 'User Server', allowed: true }336]);337338// Check what's actually stored in storage (not including product.json servers)339const storageKey = '[email protected]';340const storedData = JSON.parse(storageService.get(storageKey, StorageScope.APPLICATION) || '[]');341342// Should only contain the user-managed server, not the trusted one343assert.strictEqual(storedData.length, 1);344assert.strictEqual(storedData[0].id, 'user-server');345346// But readAllowedMcpServers should return both (including trusted from product.json)347const allServers = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');348assert.strictEqual(allServers.length, 2);349});350351test('fires onDidChangeMcpSessionAccess event', () => {352let eventFired = false;353let eventData: { providerId: string; accountName: string } | undefined;354355const disposable = authenticationMcpAccessService.onDidChangeMcpSessionAccess(event => {356eventFired = true;357eventData = event;358});359360try {361authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [362{ id: 'server1', name: 'Server 1', allowed: true }363]);364365assert.strictEqual(eventFired, true);366assert.ok(eventData);367assert.strictEqual(eventData.providerId, 'github');368assert.strictEqual(eventData.accountName, '[email protected]');369} finally {370disposable.dispose();371}372});373});374375suite('removeAllowedMcpServers', () => {376test('removes all stored MCP servers for account', () => {377// First add some servers378authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [379{ id: 'server1', name: 'Server 1', allowed: true },380{ id: 'server2', name: 'Server 2', allowed: false }381]);382383// Verify they exist384let result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');385assert.strictEqual(result.length, 2);386387// Remove them388authenticationMcpAccessService.removeAllowedMcpServers('github', '[email protected]');389390// Verify they're gone391result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');392assert.strictEqual(result.length, 0);393});394395test('does not affect trusted servers from product.json', () => {396productService.trustedMcpAuthAccess = ['trusted-server'];397398// Add some user-managed servers399authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [400{ id: 'user-server', name: 'User Server', allowed: true }401]);402403// Verify both trusted and user servers exist404let result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');405assert.strictEqual(result.length, 2);406407// Remove user servers408authenticationMcpAccessService.removeAllowedMcpServers('github', '[email protected]');409410// Should still have trusted server411result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');412assert.strictEqual(result.length, 1);413assert.strictEqual(result[0].id, 'trusted-server');414assert.strictEqual(result[0].trusted, true);415});416417test('fires onDidChangeMcpSessionAccess event', () => {418let eventFired = false;419let eventData: { providerId: string; accountName: string } | undefined;420421const disposable = authenticationMcpAccessService.onDidChangeMcpSessionAccess(event => {422eventFired = true;423eventData = event;424});425426try {427authenticationMcpAccessService.removeAllowedMcpServers('github', '[email protected]');428429assert.strictEqual(eventFired, true);430assert.ok(eventData);431assert.strictEqual(eventData.providerId, 'github');432assert.strictEqual(eventData.accountName, '[email protected]');433} finally {434disposable.dispose();435}436});437438test('handles removal of non-existent data gracefully', () => {439// Should not throw when trying to remove data that doesn't exist440assert.doesNotThrow(() => {441authenticationMcpAccessService.removeAllowedMcpServers('nonexistent', '[email protected]');442});443});444});445446suite('onDidChangeMcpSessionAccess event', () => {447test('event is fired for each update operation', () => {448const events: Array<{ providerId: string; accountName: string }> = [];449450const disposable = authenticationMcpAccessService.onDidChangeMcpSessionAccess(event => {451events.push(event);452});453454try {455// Should fire for update456authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [457{ id: 'server1', name: 'Server 1', allowed: true }458]);459460// Should fire for remove461authenticationMcpAccessService.removeAllowedMcpServers('github', '[email protected]');462463// Should fire for different account464authenticationMcpAccessService.updateAllowedMcpServers('microsoft', '[email protected]', [465{ id: 'server2', name: 'Server 2', allowed: false }466]);467468assert.strictEqual(events.length, 3);469assert.strictEqual(events[0].providerId, 'github');470assert.strictEqual(events[0].accountName, '[email protected]');471assert.strictEqual(events[1].providerId, 'github');472assert.strictEqual(events[1].accountName, '[email protected]');473assert.strictEqual(events[2].providerId, 'microsoft');474assert.strictEqual(events[2].accountName, '[email protected]');475} finally {476disposable.dispose();477}478});479480test('multiple listeners receive events', () => {481let listener1Fired = false;482let listener2Fired = false;483484const disposable1 = authenticationMcpAccessService.onDidChangeMcpSessionAccess(() => {485listener1Fired = true;486});487488const disposable2 = authenticationMcpAccessService.onDidChangeMcpSessionAccess(() => {489listener2Fired = true;490});491492try {493authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [494{ id: 'server1', name: 'Server 1', allowed: true }495]);496497assert.strictEqual(listener1Fired, true);498assert.strictEqual(listener2Fired, true);499} finally {500disposable1.dispose();501disposable2.dispose();502}503});504});505506suite('integration scenarios', () => {507test('complete workflow: add, update, query, remove', () => {508const providerId = 'github';509const accountName = '[email protected]';510const serverId = 'test-server';511512// Initially unknown513assert.strictEqual(514authenticationMcpAccessService.isAccessAllowed(providerId, accountName, serverId),515undefined516);517518// Add server as allowed519authenticationMcpAccessService.updateAllowedMcpServers(providerId, accountName, [520{ id: serverId, name: 'Test Server', allowed: true }521]);522523assert.strictEqual(524authenticationMcpAccessService.isAccessAllowed(providerId, accountName, serverId),525true526);527528// Update to disallowed529authenticationMcpAccessService.updateAllowedMcpServers(providerId, accountName, [530{ id: serverId, name: 'Test Server', allowed: false }531]);532533assert.strictEqual(534authenticationMcpAccessService.isAccessAllowed(providerId, accountName, serverId),535false536);537538// Remove all539authenticationMcpAccessService.removeAllowedMcpServers(providerId, accountName);540541assert.strictEqual(542authenticationMcpAccessService.isAccessAllowed(providerId, accountName, serverId),543undefined544);545});546547test('multiple providers and accounts are isolated', () => {548// Add data for different combinations549authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [550{ id: 'server1', name: 'Server 1', allowed: true }551]);552553authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [554{ id: 'server1', name: 'Server 1', allowed: false }555]);556557authenticationMcpAccessService.updateAllowedMcpServers('microsoft', '[email protected]', [558{ id: 'server1', name: 'Server 1', allowed: true }559]);560561// Verify isolation562assert.strictEqual(563authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'server1'),564true565);566assert.strictEqual(567authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'server1'),568false569);570assert.strictEqual(571authenticationMcpAccessService.isAccessAllowed('microsoft', '[email protected]', 'server1'),572true573);574575// Non-existent combinations should return undefined576assert.strictEqual(577authenticationMcpAccessService.isAccessAllowed('microsoft', '[email protected]', 'server1'),578undefined579);580});581582test('product.json configuration takes precedence in all scenarios', () => {583productService.trustedMcpAuthAccess = {584'github': ['trusted-server'],585'microsoft': ['microsoft-trusted']586};587588// Trusted servers should always return true regardless of storage589assert.strictEqual(590authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'trusted-server'),591true592);593594// Try to override via storage595authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [596{ id: 'trusted-server', name: 'Trusted Server', allowed: false }597]);598599// Should still return true600assert.strictEqual(601authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'trusted-server'),602true603);604605// But non-trusted servers should still respect storage606authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [607{ id: 'user-server', name: 'User Server', allowed: false }608]);609610assert.strictEqual(611authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'user-server'),612false613);614});615616test('handles edge cases with empty or null values', () => {617// Empty provider/account names618assert.doesNotThrow(() => {619authenticationMcpAccessService.isAccessAllowed('', '', 'server1');620});621622// Empty server arrays623assert.doesNotThrow(() => {624authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', []);625});626627// Empty server ID/name628assert.doesNotThrow(() => {629authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [630{ id: '', name: '', allowed: true }631]);632});633});634});635});636637638