Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -173,6 +173,35 @@
"default": "",
"included": false
},
{
"key": "extensions.gallery.authProvider",
"name": "ExtensionGalleryAuthProvider",
"category": "Extensions",
"minimumVersion": "1.121",
"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.allowed",
"name": "AllowedExtensions",
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 @@ -139,6 +139,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 mcpGallery?: {
readonly serviceUrl: string;
readonly itemWebUrl: string;
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.
*/
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 @@ -359,6 +361,46 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
}
},
},
[ExtensionGalleryAuthProviderConfigKey]: {
type: 'string',
// The enum and its descriptions are intentionally NOT gated on
// `product.enableExtensionGalleryEntraAuth`: the policy metadata below always
// exports both enum descriptions, and the policy-artifact generator requires the
// schema `enum` and `enumDescriptions` to have equal length, so gating the enum
// would make a clean policy export invalid. The Entra product gate is enforced at
// runtime in `getEffectiveAuthProvider()` instead, and this setting is hidden
// (`included: false`), so listing `microsoft` here does not advertise it in the UI.
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.121',
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 @@ -2116,12 +2158,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
Loading
Loading