-
Notifications
You must be signed in to change notification settings - Fork 41.9k
Enable Microsoft Entra ID sign-in for Private Marketplace access #325331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 30 commits
d97c8a8
6f54fb9
ab9e525
3481985
94a70a7
bd44656
eec93cb
6434607
ca0d56f
5510595
2f7a8f9
6ec632f
3aa3035
988ffeb
c0306bc
5ec451b
0b48532
b5aaf89
3789654
0630e80
322b2e3
67955a3
84ff6e0
fc4e4a0
117928b
d2626c5
af73002
c67ffce
cec950d
bbfd837
7c18968
72ec505
8da36d1
082b6c7
61ac56e
7250434
fa1f726
c41e451
e61ce45
bdf2d21
a65242b
3973ede
cdf706a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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', | ||
|
|
@@ -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' | ||
| } | ||
|
|
||
| export const IExtensionGalleryManifestService = createDecorator<IExtensionGalleryManifestService>('IExtensionGalleryManifestService'); | ||
|
|
@@ -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']; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move this scopes to product.json just like scopes for github
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in a65242b — Followed These are plain OIDC scopes — we take the auth provider's default client id and One asymmetry worth naming: 🤖 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'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 3973ede. await accessor.get(IExtensionGalleryAccountService).signIn();GitHub's 🤖 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 |
|---|---|---|
|
|
@@ -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); | ||
|
|
@@ -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, | ||
| }); | ||
|
|
@@ -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') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 You were right that the fork bought nothing: both blocks already invoked the same command and differed only in the label. Collapsing them left 🤖 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[] { | ||
|
|
@@ -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) { | ||
|
|
||
| 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(); | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already done —
UnreachableandMisconfiguredcame out in e61ce45, so the enum is back to the four values onmain. 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).