Path: blob/main/src/vs/workbench/services/authentication/test/browser/authenticationMcpAccessService.test.ts
3296 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 configuration235productService.trustedMcpAuthAccess = 'invalid-string' as any;236237const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');238assert.strictEqual(result.length, 0);239});240});241242suite('updateAllowedMcpServers', () => {243test('stores new MCP servers', () => {244const servers: AllowedMcpServer[] = [245{ id: 'server1', name: 'Server 1', allowed: true },246{ id: 'server2', name: 'Server 2', allowed: false }247];248249authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', servers);250251const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');252assert.strictEqual(result.length, 2);253assert.strictEqual(result[0].id, 'server1');254assert.strictEqual(result[1].id, 'server2');255});256257test('updates existing MCP server allowed status', () => {258// First add a server259authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [260{ id: 'server1', name: 'Server 1', allowed: true }261]);262263// Then update its allowed status264authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [265{ id: 'server1', name: 'Server 1', allowed: false }266]);267268const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');269assert.strictEqual(result.length, 1);270assert.strictEqual(result[0].allowed, false);271});272273test('updates existing MCP server name when new name is provided', () => {274// First add a server with default name275authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [276{ id: 'server1', name: 'server1', allowed: true }277]);278279// Then update with a proper name280authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [281{ id: 'server1', name: 'My Server', allowed: true }282]);283284const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');285assert.strictEqual(result.length, 1);286assert.strictEqual(result[0].name, 'My Server');287});288289test('does not update name when new name is same as ID', () => {290// First add a server with a proper name291authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [292{ id: 'server1', name: 'My Server', allowed: true }293]);294295// Then try to update with ID as name (should keep existing name)296authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [297{ id: 'server1', name: 'server1', allowed: false }298]);299300const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');301assert.strictEqual(result.length, 1);302assert.strictEqual(result[0].name, 'My Server'); // Should keep original name303assert.strictEqual(result[0].allowed, false); // But allowed status should update304});305306test('adds new servers while preserving existing ones', () => {307// First add one server308authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [309{ id: 'server1', name: 'Server 1', allowed: true }310]);311312// Then add another server313authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [314{ id: 'server2', name: 'Server 2', allowed: false }315]);316317const result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');318assert.strictEqual(result.length, 2);319320const server1 = result.find(s => s.id === 'server1');321const server2 = result.find(s => s.id === 'server2');322assert.ok(server1);323assert.ok(server2);324assert.strictEqual(server1.allowed, true);325assert.strictEqual(server2.allowed, false);326});327328test('does not store trusted servers from product.json', () => {329productService.trustedMcpAuthAccess = ['trusted-server'];330331// Try to update a trusted server332authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [333{ id: 'trusted-server', name: 'Trusted Server', allowed: false, trusted: true },334{ id: 'user-server', name: 'User Server', allowed: true }335]);336337// Check what's actually stored in storage (not including product.json servers)338const storageKey = '[email protected]';339const storedData = JSON.parse(storageService.get(storageKey, StorageScope.APPLICATION) || '[]');340341// Should only contain the user-managed server, not the trusted one342assert.strictEqual(storedData.length, 1);343assert.strictEqual(storedData[0].id, 'user-server');344345// But readAllowedMcpServers should return both (including trusted from product.json)346const allServers = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');347assert.strictEqual(allServers.length, 2);348});349350test('fires onDidChangeMcpSessionAccess event', () => {351let eventFired = false;352let eventData: { providerId: string; accountName: string } | undefined;353354const disposable = authenticationMcpAccessService.onDidChangeMcpSessionAccess(event => {355eventFired = true;356eventData = event;357});358359try {360authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [361{ id: 'server1', name: 'Server 1', allowed: true }362]);363364assert.strictEqual(eventFired, true);365assert.ok(eventData);366assert.strictEqual(eventData.providerId, 'github');367assert.strictEqual(eventData.accountName, '[email protected]');368} finally {369disposable.dispose();370}371});372});373374suite('removeAllowedMcpServers', () => {375test('removes all stored MCP servers for account', () => {376// First add some servers377authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [378{ id: 'server1', name: 'Server 1', allowed: true },379{ id: 'server2', name: 'Server 2', allowed: false }380]);381382// Verify they exist383let result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');384assert.strictEqual(result.length, 2);385386// Remove them387authenticationMcpAccessService.removeAllowedMcpServers('github', '[email protected]');388389// Verify they're gone390result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');391assert.strictEqual(result.length, 0);392});393394test('does not affect trusted servers from product.json', () => {395productService.trustedMcpAuthAccess = ['trusted-server'];396397// Add some user-managed servers398authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [399{ id: 'user-server', name: 'User Server', allowed: true }400]);401402// Verify both trusted and user servers exist403let result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');404assert.strictEqual(result.length, 2);405406// Remove user servers407authenticationMcpAccessService.removeAllowedMcpServers('github', '[email protected]');408409// Should still have trusted server410result = authenticationMcpAccessService.readAllowedMcpServers('github', '[email protected]');411assert.strictEqual(result.length, 1);412assert.strictEqual(result[0].id, 'trusted-server');413assert.strictEqual(result[0].trusted, true);414});415416test('fires onDidChangeMcpSessionAccess event', () => {417let eventFired = false;418let eventData: { providerId: string; accountName: string } | undefined;419420const disposable = authenticationMcpAccessService.onDidChangeMcpSessionAccess(event => {421eventFired = true;422eventData = event;423});424425try {426authenticationMcpAccessService.removeAllowedMcpServers('github', '[email protected]');427428assert.strictEqual(eventFired, true);429assert.ok(eventData);430assert.strictEqual(eventData.providerId, 'github');431assert.strictEqual(eventData.accountName, '[email protected]');432} finally {433disposable.dispose();434}435});436437test('handles removal of non-existent data gracefully', () => {438// Should not throw when trying to remove data that doesn't exist439assert.doesNotThrow(() => {440authenticationMcpAccessService.removeAllowedMcpServers('nonexistent', '[email protected]');441});442});443});444445suite('onDidChangeMcpSessionAccess event', () => {446test('event is fired for each update operation', () => {447const events: Array<{ providerId: string; accountName: string }> = [];448449const disposable = authenticationMcpAccessService.onDidChangeMcpSessionAccess(event => {450events.push(event);451});452453try {454// Should fire for update455authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [456{ id: 'server1', name: 'Server 1', allowed: true }457]);458459// Should fire for remove460authenticationMcpAccessService.removeAllowedMcpServers('github', '[email protected]');461462// Should fire for different account463authenticationMcpAccessService.updateAllowedMcpServers('microsoft', '[email protected]', [464{ id: 'server2', name: 'Server 2', allowed: false }465]);466467assert.strictEqual(events.length, 3);468assert.strictEqual(events[0].providerId, 'github');469assert.strictEqual(events[0].accountName, '[email protected]');470assert.strictEqual(events[1].providerId, 'github');471assert.strictEqual(events[1].accountName, '[email protected]');472assert.strictEqual(events[2].providerId, 'microsoft');473assert.strictEqual(events[2].accountName, '[email protected]');474} finally {475disposable.dispose();476}477});478479test('multiple listeners receive events', () => {480let listener1Fired = false;481let listener2Fired = false;482483const disposable1 = authenticationMcpAccessService.onDidChangeMcpSessionAccess(() => {484listener1Fired = true;485});486487const disposable2 = authenticationMcpAccessService.onDidChangeMcpSessionAccess(() => {488listener2Fired = true;489});490491try {492authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [493{ id: 'server1', name: 'Server 1', allowed: true }494]);495496assert.strictEqual(listener1Fired, true);497assert.strictEqual(listener2Fired, true);498} finally {499disposable1.dispose();500disposable2.dispose();501}502});503});504505suite('integration scenarios', () => {506test('complete workflow: add, update, query, remove', () => {507const providerId = 'github';508const accountName = '[email protected]';509const serverId = 'test-server';510511// Initially unknown512assert.strictEqual(513authenticationMcpAccessService.isAccessAllowed(providerId, accountName, serverId),514undefined515);516517// Add server as allowed518authenticationMcpAccessService.updateAllowedMcpServers(providerId, accountName, [519{ id: serverId, name: 'Test Server', allowed: true }520]);521522assert.strictEqual(523authenticationMcpAccessService.isAccessAllowed(providerId, accountName, serverId),524true525);526527// Update to disallowed528authenticationMcpAccessService.updateAllowedMcpServers(providerId, accountName, [529{ id: serverId, name: 'Test Server', allowed: false }530]);531532assert.strictEqual(533authenticationMcpAccessService.isAccessAllowed(providerId, accountName, serverId),534false535);536537// Remove all538authenticationMcpAccessService.removeAllowedMcpServers(providerId, accountName);539540assert.strictEqual(541authenticationMcpAccessService.isAccessAllowed(providerId, accountName, serverId),542undefined543);544});545546test('multiple providers and accounts are isolated', () => {547// Add data for different combinations548authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [549{ id: 'server1', name: 'Server 1', allowed: true }550]);551552authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [553{ id: 'server1', name: 'Server 1', allowed: false }554]);555556authenticationMcpAccessService.updateAllowedMcpServers('microsoft', '[email protected]', [557{ id: 'server1', name: 'Server 1', allowed: true }558]);559560// Verify isolation561assert.strictEqual(562authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'server1'),563true564);565assert.strictEqual(566authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'server1'),567false568);569assert.strictEqual(570authenticationMcpAccessService.isAccessAllowed('microsoft', '[email protected]', 'server1'),571true572);573574// Non-existent combinations should return undefined575assert.strictEqual(576authenticationMcpAccessService.isAccessAllowed('microsoft', '[email protected]', 'server1'),577undefined578);579});580581test('product.json configuration takes precedence in all scenarios', () => {582productService.trustedMcpAuthAccess = {583'github': ['trusted-server'],584'microsoft': ['microsoft-trusted']585};586587// Trusted servers should always return true regardless of storage588assert.strictEqual(589authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'trusted-server'),590true591);592593// Try to override via storage594authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [595{ id: 'trusted-server', name: 'Trusted Server', allowed: false }596]);597598// Should still return true599assert.strictEqual(600authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'trusted-server'),601true602);603604// But non-trusted servers should still respect storage605authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [606{ id: 'user-server', name: 'User Server', allowed: false }607]);608609assert.strictEqual(610authenticationMcpAccessService.isAccessAllowed('github', '[email protected]', 'user-server'),611false612);613});614615test('handles edge cases with empty or null values', () => {616// Empty provider/account names617assert.doesNotThrow(() => {618authenticationMcpAccessService.isAccessAllowed('', '', 'server1');619});620621// Empty server arrays622assert.doesNotThrow(() => {623authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', []);624});625626// Empty server ID/name627assert.doesNotThrow(() => {628authenticationMcpAccessService.updateAllowedMcpServers('github', '[email protected]', [629{ id: '', name: '', allowed: true }630]);631});632});633});634});635636637