Skip to content
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d97c8a8
Add extensions.gallery.authProvider policy, marketplace scope, and co…
mcumming Jul 7, 2026
6f54fb9
Add Entra ID eligibility check to the gallery manifest service
mcumming Jul 7, 2026
ab9e525
Add provider-aware marketplace sign-in and access-denied UX
mcumming Jul 7, 2026
3481985
Add microsoft to trustedExtensionAuthAccess
mcumming Jul 7, 2026
94a70a7
Add unit tests for marketplace provider routing and eligibility
mcumming Jul 7, 2026
bd44656
Harden Entra marketplace access: cache scoping, race guards, error ha…
mcumming Jul 10, 2026
eec93cb
Address Copilot PR review: policy export, cross-account leak, layerin…
mcumming Jul 10, 2026
6434607
Avoid `any` casts in extensionGalleryManifestService test
mcumming Jul 14, 2026
ca0d56f
Refactor Private Marketplace access validation into a provider strategy
mcumming Jul 29, 2026
5510595
Surface AccessDenied instead of re-prompting sign-in on 401 for signe…
mcumming Jul 29, 2026
2f7a8f9
Remove policy data from contributor PR
joshspicer Aug 4, 2026
6ec632f
Dissolve access validator into account + service-index services
mcumming Aug 7, 2026
3aa3035
marketplace: thread CancellationToken guards, materialize index in ca…
mcumming Aug 10, 2026
988ffeb
marketplace: make getEffectiveAuthProvider dependency-free, cache res…
mcumming Aug 10, 2026
c0306bc
marketplace: collapse duplicate access-denied welcome content into on…
mcumming Aug 10, 2026
5ec451b
marketplace: fix telemetry provider scoping
mcumming Aug 10, 2026
0b48532
test: add gallery access unit and telemetry coverage
mcumming Aug 10, 2026
b5aaf89
Remove CONTEXT_MARKETPLACE_AUTH_PROVIDER re-export
mcumming Aug 12, 2026
3789654
Use Event.signal for onDidChangeAccount instead of an emitter relay
mcumming Aug 12, 2026
0630e80
Default extensions gallery auth provider to a valid enum member
mcumming Aug 12, 2026
322b2e3
Revert 'Add microsoft to trustedExtensionAuthAccess'
mcumming Aug 12, 2026
67955a3
Register account resolver as a Delayed singleton (Tyler #3, #4)
mcumming Aug 12, 2026
84ff6e0
Ground Private Marketplace account selection in a persisted slot
mcumming Aug 12, 2026
fc4e4a0
Show account quick pick on Microsoft marketplace sign-in
mcumming Aug 13, 2026
117928b
Clarify getAccount vs resolveCurrentAccount intent
mcumming Aug 13, 2026
d2626c5
Move Microsoft (Entra) marketplace eligibility check client-side
mcumming Aug 13, 2026
af73002
marketplace: break account->auth DI cycle via orchestrator wiring
mcumming Aug 13, 2026
c67ffce
marketplace: rename ExtensionGalleryServiceIndexService to ...Fetcher
mcumming Aug 13, 2026
cec950d
marketplace: drop obsolete microtask deferral in gallery manifest ser…
mcumming Aug 14, 2026
bbfd837
marketplace: move access resolution into the gallery account service
mcumming Aug 17, 2026
7c18968
marketplace: include the service index error body in the failure
mcumming Aug 17, 2026
72ec505
marketplace: pin that a signed-out user is never told to check the ne…
mcumming Aug 17, 2026
8da36d1
marketplace: report a rejected client as denied, not unreachable
mcumming Aug 17, 2026
082b6c7
marketplace: give the account service one job, and the manifest servi…
mcumming Aug 17, 2026
61ac56e
marketplace: cut comment volume across the PR
mcumming Aug 17, 2026
7250434
Fold the service index fetch back in and drop the access cache
mcumming Aug 19, 2026
fa1f726
Publish the catalog on every successful resolve
mcumming Aug 19, 2026
c41e451
refactor gallery manifest service to have minimal changes
sandy081 Aug 20, 2026
e61ce45
Adopt the reviewer's gallery manifest service
mcumming Aug 20, 2026
bdf2d21
Remove the Entra auth product flag
mcumming Aug 20, 2026
a65242b
Read marketplace auth scopes from product.json
mcumming Aug 21, 2026
3973ede
Route marketplace sign-in through the account service
mcumming Aug 21, 2026
cdf706a
Prompt for restart when the marketplace auth provider changes
mcumming Aug 28, 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
7 changes: 7 additions & 0 deletions src/vs/base/common/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,13 @@ export interface IAuthorizationJWTClaims {
*/
roles?: string[];

/**
* OPTIONAL. Microsoft Entra ID tenant id (`tid`). Present on Entra-issued tokens and
* absent from pure OIDC/non-Entra tokens. Used to distinguish work/school (Entra) accounts
* from personal Microsoft Accounts.
*/
tid?: string;

/**
* OPTIONAL. Handles optional claims that are not explicitly defined in the standard.
*/
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 @@ -159,6 +159,15 @@ export interface IProductConfiguration {

readonly dictationRuntime?: IDictationRuntimeProductConfig;

/**
* 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 Down Expand Up @@ -68,7 +77,22 @@ 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
* deployment is misconfigured — for example the service index URL is not HTTPS, so the
* Microsoft token cannot be safely transmitted. Access is refused (no silent fallback to
* another provider) until the server is corrected.
*/
Misconfigured = 'misconfigured'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets not introduce more status code unless necessay

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already done — Unreachable and Misconfigured came out in e61ce45, so the enum is back to the four values on main. The UI sites that switched on them went with it.


🤖 This reply was drafted by an AI agent on behalf of Michael Cummings (MSFT) (@mcumming).

}

export const IExtensionGalleryManifestService = createDecorator<IExtensionGalleryManifestService>('IExtensionGalleryManifestService');
Expand Down Expand Up @@ -98,3 +122,22 @@ 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.
*/
export const PRIVATE_MARKETPLACE_SCOPES: string[] = ['openid', 'profile', 'email', 'offline_access'];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this scopes to product.json just like scopes for github

@mcumming Michael Cummings (MSFT) (mcumming) Aug 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a65242bextensionsGallery.accessScopes, next to accessSKUs.

Followed defaultChatAgent.providerScopes exactly: no in-source fallback, so the product file is the only source. If a deployment turns on the Microsoft path without configuring scopes it now reports no account, rather than requesting a session it can't use.

These are plain OIDC scopes — we take the auth provider's default client id and organizations tenant, so no VSCODE_* overrides are involved. That's all the eligibility check needs: an ID token carrying a tid claim.

One asymmetry worth naming: providerScopes is required in the type, accessScopes can't be — extensionsGallery is itself optional, and requiring it would force scopes on GitHub-path deployments where they're meaningless. The fail-closed check covers that gap.


🤖 This reply was drafted by an AI agent on behalf of Michael Cummings (MSFT) (@mcumming).


/**
* Command that drives interactive Microsoft (Entra ID) sign-in for the Private Marketplace and
* remembers the account the user settles on. It is registered in the Electron account-service layer
* (which owns account selection and persistence); the browser-layer sign-in action invokes it by id
* so it does not have to reach across the layer boundary.
*/
export const ExtensionGalleryMicrosoftSignInCommandId = 'workbench.extensions.marketplace.signInWithMicrosoft';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THere should be just one command for sign in and account service should handle signing in to microsoft or github

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3973ede. ExtensionGalleryMicrosoftSignInCommandId is gone along with its CommandsRegistry.registerCommand, and the action is now just:

await accessor.get(IExtensionGalleryAccountService).signIn();

GitHub's signIn() delegates to defaultAccountService.signIn(); the Microsoft provider owns its account quick-pick. To make that callable I moved the service interface to services/extensionManagement/common/extensionGalleryAccount.ts, mirroring how IDefaultAccountService splits interface from implementation — the command id only existed because the browser layer couldn't reach electron-browser.


🤖 This reply was drafted by an AI agent on behalf of Michael Cummings (MSFT) (@mcumming).

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, ExtensionGalleryMicrosoftSignInCommandId } 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,7 @@ 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 { 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 +361,18 @@ 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: 'github',
scope: ConfigurationScope.APPLICATION,
included: false,
},
'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 +2131,25 @@ 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 commandService = accessor.get(ICommandService);
const authProvider = configurationService.getValue<string>(ExtensionGalleryAuthProviderConfigKey);

// Delegate to the provider-specific sign-in command. The Microsoft flow lives in the Electron
// account-service layer (account selection + persistence) and cannot be reached from here.
const commandId = (authProvider === 'microsoft' && productService.enableExtensionGalleryEntraAuth)
? ExtensionGalleryMicrosoftSignInCommandId
: DEFAULT_ACCOUNT_SIGN_IN_COMMAND;
await commandService.executeCommand(commandId);
}
});

Expand Down
45 changes: 40 additions & 5 deletions src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,8 @@ import { createActionViewItem } from '../../../../platform/actions/browser/menuE
import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js';
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 { IExtensionGalleryManifest, IExtensionGalleryManifestService, ExtensionGalleryManifestStatus, CONTEXT_MARKETPLACE_AUTH_PROVIDER } 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,14 +159,39 @@ 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')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid hardcoding provider ids. They should be read only at one place which should be gallery accout service. The sign in button should just say Sign In - why does user has to know if it is github or microsoft if they have to sign in anyway - the sign in page anyway shows right

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3973ede — back to a single status-gated welcome entry, same shape as main.

You were right that the fork bought nothing: both blocks already invoked the same command and differed only in the label. Collapsing them left CONTEXT_MARKETPLACE_AUTH_PROVIDER with no consumers, so the context key is deleted too. The browser layer no longer references either provider id, and it also stopped importing DEFAULT_ACCOUNT_SIGN_IN_COMMAND.


🤖 This reply was drafted by an AI agent on behalf of Michael Cummings (MSFT) (@mcumming).

)
});

viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
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')
)
)
});

// Access denied applies to every provider (microsoft/github/default), so gate on status alone.
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)
});

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)
});
}

private createDefaultExtensionsViewDescriptors(): IViewDescriptor[] {
Expand Down Expand Up @@ -1161,6 +1190,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { URI } from '../../../../base/common/uri.js';

/**
* Identifies which authentication provider gates Private Marketplace access.
*/
export type ExtensionGalleryAccessProviderId = 'github' | 'microsoft';

/**
* A persisted access verdict for a single account against a single marketplace.
*/
export interface ICachedAccess {
authProvider: ExtensionGalleryAccessProviderId;
accountId: string;
eligible: boolean;
/**
* The `extensions.gallery.serviceUrl` the verdict was computed against. A verdict is scoped
* to a specific marketplace (the eligibility endpoint is discovered per-marketplace), so a
* cache written for one service URL must never be applied after the admin points the client
* at a different marketplace.
*/
serviceUrl: string;
}

/**
* Thrown by the service-index (gallery manifest) fetch when the request is rejected for
* authentication/authorization reasons (HTTP 401/403). The service index MAY be protected
* at the administrator's discretion, so this is kept distinct from transient/network
* failures: callers on the Entra path use it to decide whether to prompt for sign-in
* (no token was presented) or to treat the identity as denied (a token was rejected),
* rather than mislabeling an auth-gated index as "unreachable".
*/
export class MarketplaceAuthRequiredError extends Error {
constructor(readonly statusCode: number) {
super(`Extension gallery request requires authentication (status ${statusCode}).`);
}
}

/**
* Thrown when the Private Marketplace deployment is misconfigured for the effective auth
* provider (e.g. a non-HTTPS service index under Entra auth, so the Microsoft token cannot be
* safely transmitted). Distinct from {@link MarketplaceAuthRequiredError} and transient failures
* so the validator can surface a durable "misconfigured" status rather than a sign-in prompt or
* an "unreachable" flash.
*/
export class MarketplaceMisconfiguredError extends Error {
constructor(message: string) {
super(message);
}
}

/**
* Resolves the effective marketplace auth provider, applying the Entra (microsoft) product gate.
* When Entra auth is not enabled in the product, a configured `microsoft` provider is downgraded to
* the GitHub/default provider so the Entra path stays dormant until the Private Marketplace is
* publicly released. Kept dependency-free (primitives in, verdict out) so it never reaches into a
* service; callers read `extensions.gallery.authProvider` and `product.enableExtensionGalleryEntraAuth`.
*/
export function getEffectiveAuthProvider(configuredProvider: string | undefined, entraAuthEnabled: boolean): ExtensionGalleryAccessProviderId {
return configuredProvider === 'microsoft' && entraAuthEnabled ? 'microsoft' : 'github';
}

/**
* Guards bearer-token transport. A token must only ever be attached to a request whose target is
* (a) HTTPS and (b) same-origin as the admin-configured service index URL. This prevents a
* compromised or misconfigured gallery manifest from redirecting a resource URL at a foreign or
* cleartext endpoint and exfiltrating the token. Returns false on any parse failure so callers
* fail closed.
*/
export function isSafeTokenTarget(targetUrl: string, baseUrl: string): boolean {
let target: URI;
let base: URI;
try {
target = URI.parse(targetUrl, true);
base = URI.parse(baseUrl, true);
} catch {
return false;
}
if (target.scheme !== 'https') {
return false;
}
// Same-origin: scheme + authority (host:port) must match exactly.
return target.scheme === base.scheme && target.authority.toLowerCase() === base.authority.toLowerCase();
}
Loading
Loading