Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ba9e162
Add extensions.gallery.authProvider policy, marketplace scope, and co…
mcumming Jul 7, 2026
3435ee8
Add Entra ID eligibility check to the gallery manifest service
mcumming Jul 7, 2026
7fb68cf
Add provider-aware marketplace sign-in and access-denied UX
mcumming Jul 7, 2026
bebf7f5
Add microsoft to trustedExtensionAuthAccess
mcumming Jul 7, 2026
0bfd24a
Add unit tests for marketplace provider routing and eligibility
mcumming Jul 7, 2026
b58ca78
Harden Entra marketplace access: cache scoping, race guards, error ha…
mcumming Jul 10, 2026
282c38d
Address Copilot PR review: policy export, cross-account leak, layerin…
mcumming Jul 10, 2026
27426c8
Avoid `any` casts in extensionGalleryManifestService test
mcumming Jul 14, 2026
2bcf8ee
Merge branch 'main' into mcumming-entra-id-vss-marketplace-review
mcumming Jul 15, 2026
49afc74
Merge branch 'main' into mcumming-entra-id-vss-marketplace-review
mcumming Jul 16, 2026
ef1af59
Merge branch 'main' into mcumming-entra-id-vss-marketplace-review
mcumming Jul 21, 2026
940d8e4
Merge branch 'main' into mcumming-entra-id-vss-marketplace-review
mcumming Jul 23, 2026
8020cbb
Merge remote-tracking branch 'upstream/main' into mcumming-entra-id-v…
mcumming Jul 29, 2026
184a33c
Refactor Private Marketplace access validation into a provider strategy
mcumming Jul 29, 2026
6a77d17
Surface AccessDenied instead of re-prompting sign-in on 401 for signe…
mcumming Jul 29, 2026
3a472db
Fix formatting in product.ts to satisfy hygiene check
mcumming Aug 3, 2026
4e27657
Merge branch 'main' into mcumming-entra-id-vss-marketplace-review
mcumming Aug 3, 2026
73562db
Fix policyData.jsonc ordering to match generated output
mcumming Aug 3, 2026
9658372
Merge branch 'main' into mcumming-entra-id-vss-marketplace-review
mcumming Aug 4, 2026
972e53f
Remove policy data from contributor PR
joshspicer Aug 4, 2026
c61e904
Add extension gallery auth provider policy
joshspicer Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions build/lib/policies/policyData.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,35 @@
"default": 2,
"included": true
},
{
"key": "extensions.gallery.authProvider",
"name": "ExtensionGalleryAuthProvider",
"category": "Extensions",
"minimumVersion": "1.133",
"localization": {
"description": {
"key": "extensions.gallery.authProvider",
"value": "Configure the authentication provider for the Extensions Marketplace"
},
"enumDescriptions": [
{
"key": "extensions.gallery.authProvider.github",
"value": "Authenticate to the Extensions Marketplace using GitHub."
},
{
"key": "extensions.gallery.authProvider.microsoft",
"value": "Authenticate to the Extensions Marketplace using a Microsoft (Entra ID) account."
}
]
},
"type": "string",
"default": "",
"enum": [
"github",
"microsoft"
],
"included": false
},
{
"key": "extensions.gallery.serviceUrl",
"name": "ExtensionGalleryServiceUrl",
Expand Down
3 changes: 2 additions & 1 deletion product.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@
],
"github-enterprise": [
"GitHub.copilot-chat"
]
],
"microsoft": []
},
"onboardingKeymaps": [
{
Expand Down
9 changes: 9 additions & 0 deletions src/vs/base/common/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ export interface IProductConfiguration {

readonly agentSdks?: { readonly [packageId: string]: IAgentSdkProductConfig };

/**
* Hard gate for the Entra ID (Microsoft) authentication path of the Extensions
* Marketplace. When falsy, the `extensions.gallery.authProvider: microsoft`
* setting is ignored and the GitHub/default auth path is used instead. This keeps
* the Entra path dormant on builds where the Private Marketplace has not yet been
* publicly released, independent of any admin policy configuration.
*/
readonly enableExtensionGalleryEntraAuth?: boolean;

readonly dictationRuntime?: IDictationRuntimeProductConfig;

readonly mcpGallery?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@

import { Event } from '../../../base/common/event.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { RawContextKey } from '../../contextkey/common/contextkey.js';

/**
* Context key exposing the effective Marketplace authentication provider (e.g. `github` or
* `microsoft`) for `when`-clause driven welcome content. Defined here in the platform layer so
* both the workbench service that sets it and the Extensions contribution that reads it can
* depend on it without a service-to-contribution dependency.
*/
export const CONTEXT_MARKETPLACE_AUTH_PROVIDER = new RawContextKey<string>('marketplaceAuthProvider', '');

export const enum ExtensionGalleryResourceType {
ExtensionQueryService = 'ExtensionQueryService',
Expand All @@ -15,6 +24,7 @@ export const enum ExtensionGalleryResourceType {
ExtensionRatingViewUri = 'ExtensionRatingViewUriTemplate',
ExtensionResourceUri = 'ExtensionResourceUriTemplate',
ContactSupportUri = 'ContactSupportUri',
EligibilityService = 'EligibilityService',
}

export const enum Flag {
Expand Down Expand Up @@ -68,7 +78,21 @@ export const enum ExtensionGalleryManifestStatus {
Available = 'available',
RequiresSignIn = 'requiresSignIn',
AccessDenied = 'accessDenied',
Unavailable = 'unavailable'
Unavailable = 'unavailable',
/**
* A marketplace is configured, and the user is (or is presumed) eligible, but its
* gallery manifest could not be fetched — a transient network/server error. Unlike
* {@link Unavailable} (which also means "no gallery configured"), this state is only
* ever set after a failed fetch of a configured marketplace, so it is safe to surface
* an informative message without affecting builds that have no gallery at all.
*/
Unreachable = 'unreachable',
/**
* The marketplace is configured for Microsoft (Entra ID) authentication, but the
* gallery manifest does not advertise an EligibilityService resource. Access is
* refused (no silent fallback to another provider) until the server is corrected.
*/
Misconfigured = 'misconfigured'
}

export const IExtensionGalleryManifestService = createDecorator<IExtensionGalleryManifestService>('IExtensionGalleryManifestService');
Expand Down Expand Up @@ -98,3 +122,17 @@ export function getExtensionGalleryManifestResourceUri(manifest: IExtensionGalle
}

export const ExtensionGalleryServiceUrlConfigKey = 'extensions.gallery.serviceUrl';

export const ExtensionGalleryAuthProviderConfigKey = 'extensions.gallery.authProvider';

/**
* Scopes requested when signing in with Microsoft (Entra ID) to establish the
* user's identity for the Private Marketplace eligibility check.
*
* Only standard OpenID Connect sign-in scopes are requested — enough to obtain a
* Microsoft session that identifies the user. This intentionally does NOT request a
* resource-scoped token (e.g. `api://<client-id>/access_as_user`). Acquiring resource
* tokens for Private Marketplace API calls, per the server's Protected Resource
* Metadata (RFC 9728), is deferred to a follow-up change.
Comment on lines +132 to +136
*/
export const PRIVATE_MARKETPLACE_SCOPES: string[] = ['openid', 'profile', 'email', 'offline_access'];
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { CommandsRegistry, ICommandService } from '../../../../platform/commands
import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { IDialogService, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { ExtensionGalleryManifestStatus, ExtensionGalleryResourceType, ExtensionGalleryServiceUrlConfigKey, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
import { ExtensionGalleryManifestStatus, ExtensionGalleryResourceType, ExtensionGalleryAuthProviderConfigKey, ExtensionGalleryServiceUrlConfigKey, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService, PRIVATE_MARKETPLACE_SCOPES } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
import { EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, ExtensionRequestsTimeoutConfigKey, ExtensionsLocalizedLabel, FilterType, IExtensionGalleryService, IExtensionManagementService, PreferencesLocalizedLabel, SortBy, VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js';
import { areSameExtensions, getIdAndVersion } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
import { ExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js';
Expand Down Expand Up @@ -69,6 +69,8 @@ import { IWebview } from '../../webview/browser/webview.js';
import { Query } from '../common/extensionQuery.js';
import { AutoRestartConfigurationKey, AutoUpdateConfigurationKey, CONTEXT_EXTENSIONS_GALLERY_STATUS, CONTEXT_HAS_GALLERY, DefaultViewsContext, ExtensionEditorTab, ExtensionRuntimeActionType, EXTENSIONS_CATEGORY, extensionsFilterSubMenu, extensionsSearchActionsMenu, HasOutdatedExtensionsContext, IExtensionArg, IExtensionsViewPaneContainer, IExtensionsWorkbenchService, INSTALL_ACTIONS_GROUP, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, IWorkspaceRecommendedExtensionsView, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, OUTDATED_EXTENSIONS_VIEW_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, TOGGLE_IGNORE_EXTENSION_ACTION_ID, UPDATE_ACTIONS_GROUP, VIEWLET_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID } from '../common/extensions.js';
import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from '../common/extensionsFileTemplate.js';
import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ExtensionsInput } from '../common/extensionsInput.js';
import { KeymapExtensions } from '../common/extensionsUtils.js';
import { SearchExtensionsTool, SearchExtensionsToolData } from '../common/searchExtensionsTool.js';
Expand Down Expand Up @@ -360,6 +362,39 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
}
},
},
[ExtensionGalleryAuthProviderConfigKey]: {
type: 'string',
enum: ['github', 'microsoft'],
enumDescriptions: [
localize('extensions.gallery.authProvider.github', "Authenticate to the Extensions Marketplace using GitHub."),
localize('extensions.gallery.authProvider.microsoft', "Authenticate to the Extensions Marketplace using a Microsoft (Entra ID) account."),
],
description: localize('extensions.gallery.authProvider', "Configure the authentication provider for the Extensions Marketplace"),
default: '',
scope: ConfigurationScope.APPLICATION,
included: false,
policy: {
name: 'ExtensionGalleryAuthProvider',
category: PolicyCategory.Extensions,
minimumVersion: '1.133',
localization: {
description: {
key: 'extensions.gallery.authProvider',
value: localize('extensions.gallery.authProvider', "Configure the authentication provider for the Extensions Marketplace"),
},
enumDescriptions: [
{
key: 'extensions.gallery.authProvider.github',
value: localize('extensions.gallery.authProvider.github', "Authenticate to the Extensions Marketplace using GitHub."),
},
{
key: 'extensions.gallery.authProvider.microsoft',
value: localize('extensions.gallery.authProvider.microsoft', "Authenticate to the Extensions Marketplace using a Microsoft (Entra ID) account."),
},
]
}
},
},
'extensions.supportNodeGlobalNavigator': {
type: 'boolean',
description: localize('extensionsSupportNodeGlobalNavigator', "When enabled, Node.js navigator object is exposed on the global scope."),
Expand Down Expand Up @@ -2118,12 +2153,27 @@ registerAction2(class ExtensionsGallerySignInAction extends Action2 {
title: localize2('signInToMarketplace', 'Sign in to access Extensions Marketplace'),
menu: {
id: MenuId.AccountsContext,
when: CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.RequiresSignIn)
when: ContextKeyExpr.or(
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.RequiresSignIn),
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.AccessDenied),
)
},
});
}
run(accessor: ServicesAccessor): Promise<void> {
return accessor.get(ICommandService).executeCommand(DEFAULT_ACCOUNT_SIGN_IN_COMMAND);
async run(accessor: ServicesAccessor): Promise<void> {
const configurationService = accessor.get(IConfigurationService);
const productService = accessor.get(IProductService);
const authProvider = configurationService.getValue<string>(ExtensionGalleryAuthProviderConfigKey);

if (authProvider === 'microsoft' && productService.enableExtensionGalleryEntraAuth) {
const authenticationService = accessor.get(IAuthenticationService);
await authenticationService.createSession(
'microsoft',
PRIVATE_MARKETPLACE_SCOPES);
} else {
const commandService = accessor.get(ICommandService);
await commandService.executeCommand(DEFAULT_ACCOUNT_SIGN_IN_COMMAND);
}
}
});

Expand Down
62 changes: 55 additions & 7 deletions src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, AutoRestartConfigurationKey, ExtensionRuntimeActionType, SearchMcpServersContext, SearchAgentPluginsContext, DefaultViewsContext, CONTEXT_EXTENSIONS_GALLERY_STATUS } from '../common/extensions.js';
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, AutoRestartConfigurationKey, ExtensionRuntimeActionType, SearchMcpServersContext, SearchAgentPluginsContext, DefaultViewsContext, CONTEXT_EXTENSIONS_GALLERY_STATUS, CONTEXT_MARKETPLACE_AUTH_PROVIDER } from '../common/extensions.js';
import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from './extensionsActions.js';
import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from '../../../services/extensionManagement/common/extensionManagement.js';
Expand Down Expand Up @@ -69,7 +69,6 @@ import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { IExtensionGalleryManifest, IExtensionGalleryManifestService, ExtensionGalleryManifestStatus } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
import { URI } from '../../../../base/common/uri.js';
import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/browser/defaultAccount.js';

export const ExtensionsSortByContext = new RawContextKey<string>('extensionsSortByValue', '');
export const SearchMarketplaceExtensionsContext = new RawContextKey<boolean>('searchMarketplaceExtensions', false);
Expand Down Expand Up @@ -146,7 +145,12 @@ export class ExtensionsViewletViewsContribution extends Disposable implements IW
ContextKeyExpr.or(
ContextKeyExpr.has('searchMarketplaceExtensions'), ContextKeyExpr.and(DefaultViewsContext)
),
ContextKeyExpr.or(CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.RequiresSignIn), CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.AccessDenied))
ContextKeyExpr.or(
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.RequiresSignIn),
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.AccessDenied),
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.Misconfigured),
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.Unreachable)
)
),
order: -1,
});
Expand All @@ -155,13 +159,51 @@ export class ExtensionsViewletViewsContribution extends Disposable implements IW
viewRegistry.registerViews(viewDescriptors, this.container);

viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
content: localize('sign in', "[Sign in to access Extensions Marketplace]({0})", `command:${DEFAULT_ACCOUNT_SIGN_IN_COMMAND}`),
when: CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.RequiresSignIn)
content: localize('sign in microsoft', "[Sign in with your Microsoft account]({0}) to access the Extensions Marketplace.", `command:workbench.extensions.actions.gallery.signIn`),
when: ContextKeyExpr.and(
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.RequiresSignIn),
CONTEXT_MARKETPLACE_AUTH_PROVIDER.isEqualTo('microsoft')
)
});

viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
content: localize('access denied', "Your account does not have access to the Extensions Marketplace. Please contact your administrator."),
when: CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.AccessDenied)
content: localize('sign in github', "[Sign in with GitHub]({0}) to access the Extensions Marketplace.", `command:workbench.extensions.actions.gallery.signIn`),
when: ContextKeyExpr.and(
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.RequiresSignIn),
ContextKeyExpr.or(
CONTEXT_MARKETPLACE_AUTH_PROVIDER.isEqualTo('github'),
ContextKeyExpr.not('marketplaceAuthProvider')
)
)
});

viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
content: localize('access denied microsoft', "Your Microsoft account does not have access to the Extensions Marketplace. Please contact your administrator."),
when: ContextKeyExpr.and(
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.AccessDenied),
CONTEXT_MARKETPLACE_AUTH_PROVIDER.isEqualTo('microsoft')
)
});

viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
content: localize('access denied github', "Your account does not have access to the Extensions Marketplace. Please contact your administrator."),
when: ContextKeyExpr.and(
CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.AccessDenied),
ContextKeyExpr.or(
CONTEXT_MARKETPLACE_AUTH_PROVIDER.isEqualTo('github'),
ContextKeyExpr.not('marketplaceAuthProvider')
)
)
});

viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
content: localize('marketplace misconfigured', "The Extensions Marketplace is not configured correctly and cannot be reached. Please contact your administrator."),
when: CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.Misconfigured)
});

viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
content: localize('marketplace unreachable', "The Extensions Marketplace is currently unavailable. Check your network connection and [try again]({0}).", `command:workbench.action.reloadWindow`),
when: CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.Unreachable)
});
}

Expand Down Expand Up @@ -1161,6 +1203,12 @@ export class ExtensionMarketplaceStatusUpdater extends Disposable implements IWo
case ExtensionGalleryManifestStatus.AccessDenied:
badge = new WarningBadge(() => localize('accessDenied', "Access denied to marketplace"));
break;
case ExtensionGalleryManifestStatus.Misconfigured:
badge = new WarningBadge(() => localize('marketplaceMisconfigured', "Marketplace is misconfigured"));
break;
case ExtensionGalleryManifestStatus.Unreachable:
badge = new WarningBadge(() => localize('marketplaceUnreachable', "Marketplace is currently unavailable"));
break;
}

if (badge) {
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/contrib/extensions/common/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ export const ExtensionResultsListFocused = new RawContextKey<boolean>('extension
export const SearchMcpServersContext = new RawContextKey<boolean>('searchMcpServers', false);
export const SearchAgentPluginsContext = new RawContextKey<boolean>('searchAgentPlugins', false);

// Marketplace Eligibility Context Keys
export { CONTEXT_MARKETPLACE_AUTH_PROVIDER } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';

// Context Menu Groups
export const THEME_ACTIONS_GROUP = '_theme_';
export const INSTALL_ACTIONS_GROUP = '0_install';
Expand Down
Loading
Loading