From df910a892a69326f09a20aa76c19ef6e034759b3 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Fri, 7 Aug 2026 10:30:21 -0400 Subject: [PATCH 1/2] @W-22899437 fix: set default OCAPI/WebDAV permissions on VS Code sandbox creation The VS Code extension's Create Sandbox command sent only {realm, ttl, analyticsEnabled} and omitted the settings object, so new sandboxes had no OCAPI/WebDAV permissions for the configured client -- unlike the CLI's sandbox create. Downstream code deploys and job executions then failed with authorization errors until permissions were set manually. Extract the default-permission logic into a shared SDK helper (buildSandboxSettings) so the CLI and the extension apply the same defaults. The CLI now delegates to it, and the extension builds settings from the configured clientId and passes them in the POST body. --- .../sandbox-create-default-permissions.md | 6 ++ .../b2c-cli/src/commands/sandbox/create.ts | 58 +++--------- packages/b2c-tooling-sdk/src/index.ts | 4 + .../src/operations/ods/index.ts | 4 + .../src/operations/ods/sandbox-settings.ts | 94 +++++++++++++++++++ .../operations/ods/sandbox-settings.test.ts | 52 ++++++++++ .../src/sandbox-tree/sandbox-commands.ts | 9 +- 7 files changed, 179 insertions(+), 48 deletions(-) create mode 100644 .changeset/sandbox-create-default-permissions.md create mode 100644 packages/b2c-tooling-sdk/src/operations/ods/sandbox-settings.ts create mode 100644 packages/b2c-tooling-sdk/test/operations/ods/sandbox-settings.test.ts diff --git a/.changeset/sandbox-create-default-permissions.md b/.changeset/sandbox-create-default-permissions.md new file mode 100644 index 000000000..1fa4b2729 --- /dev/null +++ b/.changeset/sandbox-create-default-permissions.md @@ -0,0 +1,6 @@ +--- +'@salesforce/b2c-tooling-sdk': patch +'b2c-vs-extension': patch +--- + +Fix sandbox creation in the VS Code extension not granting default OCAPI/WebDAV permissions. New sandboxes created from the extension now grant the configured client ID the same default permissions as the CLI's `sandbox create`, so code deployment and job execution work without manual permission setup. The shared defaults are now provided by the SDK via `buildSandboxSettings`. diff --git a/packages/b2c-cli/src/commands/sandbox/create.ts b/packages/b2c-cli/src/commands/sandbox/create.ts index 8fa5d8f33..933aa3572 100644 --- a/packages/b2c-cli/src/commands/sandbox/create.ts +++ b/packages/b2c-cli/src/commands/sandbox/create.ts @@ -7,6 +7,7 @@ import {Flags, ux} from '@oclif/core'; import cliui from 'cliui'; import {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli'; import { + buildSandboxSettings, getApiErrorMessage, SandboxPollingError, SandboxPollingTimeoutError, @@ -22,29 +23,6 @@ type OcapiSettings = OdsComponents['schemas']['OcapiSettings']; type WebDavSettings = OdsComponents['schemas']['WebDavSettings']; type SandboxSettings = OdsComponents['schemas']['SandboxSettings']; -/** - * Default OCAPI resources to grant the client ID access to. - * These enable common CI/CD operations like code deployment and job execution. - */ - -const DEFAULT_OCAPI_RESOURCES: NonNullable = [ - {resource_id: '/code_versions', methods: ['get'], read_attributes: '(**)', write_attributes: '(**)'}, - {resource_id: '/code_versions/*', methods: ['patch', 'delete'], read_attributes: '(**)', write_attributes: '(**)'}, - {resource_id: '/jobs/*/executions', methods: ['post'], read_attributes: '(**)', write_attributes: '(**)'}, - {resource_id: '/jobs/*/executions/*', methods: ['get'], read_attributes: '(**)', write_attributes: '(**)'}, - {resource_id: '/sites/*/cartridges', methods: ['post'], read_attributes: '(**)', write_attributes: '(**)'}, -]; - -/** - * Default WebDAV permissions to grant the client ID. - * These enable common operations like code upload and data import/export. - */ -const DEFAULT_WEBDAV_PERMISSIONS: WebDavSettings[number]['permissions'] = [ - {path: '/impex', operations: ['read_write']}, - {path: '/cartridges', operations: ['read_write']}, - {path: '/static', operations: ['read_write']}, -]; - /** * Command to create a new on-demand sandbox. */ @@ -286,29 +264,17 @@ export default class SandboxCreate extends OdsCommand { return undefined; } - const hasCustomOcapi = options.ocapiSettings !== undefined; - const hasCustomWebdav = options.webdavSettings !== undefined; - - const clientId = options.permissionsClientId || this.resolvedConfig.values.clientId; - - // If no custom settings and no client ID, we can't build defaults - if (!hasCustomOcapi && !hasCustomWebdav && !clientId) { - return undefined; - } - - const ocapi: OcapiSettings = hasCustomOcapi - ? this.parseJsonFlag('ocapi-settings', options.ocapiSettings!) - : clientId - ? [{client_id: clientId, resources: DEFAULT_OCAPI_RESOURCES}] - : []; - - const webdav: WebDavSettings = hasCustomWebdav - ? this.parseJsonFlag('webdav-settings', options.webdavSettings!) - : clientId - ? [{client_id: clientId, permissions: DEFAULT_WEBDAV_PERMISSIONS}] - : []; - - return {ocapi, webdav}; + return buildSandboxSettings({ + clientId: options.permissionsClientId || this.resolvedConfig.values.clientId, + ocapiSettings: + options.ocapiSettings === undefined + ? undefined + : this.parseJsonFlag('ocapi-settings', options.ocapiSettings), + webdavSettings: + options.webdavSettings === undefined + ? undefined + : this.parseJsonFlag('webdav-settings', options.webdavSettings), + }); } private parseJsonFlag(flagName: string, value: string): T { diff --git a/packages/b2c-tooling-sdk/src/index.ts b/packages/b2c-tooling-sdk/src/index.ts index 73681e96e..49a7efb88 100644 --- a/packages/b2c-tooling-sdk/src/index.ts +++ b/packages/b2c-tooling-sdk/src/index.ts @@ -307,10 +307,14 @@ export { ClonePollingTimeoutError, ClonePollingError, CloneFailedError, + buildSandboxSettings, + DEFAULT_OCAPI_RESOURCES, + DEFAULT_WEBDAV_PERMISSIONS, } from './operations/ods/index.js'; export type {SandboxState, WaitForSandboxOptions, WaitForSandboxPollInfo} from './operations/ods/index.js'; export type {CloneState, WaitForCloneOptions, WaitForClonePollInfo} from './operations/ods/index.js'; +export type {BuildSandboxSettingsOptions} from './operations/ods/index.js'; // Operations - CIP export { diff --git a/packages/b2c-tooling-sdk/src/operations/ods/index.ts b/packages/b2c-tooling-sdk/src/operations/ods/index.ts index 2145e6420..9a5296fb7 100644 --- a/packages/b2c-tooling-sdk/src/operations/ods/index.ts +++ b/packages/b2c-tooling-sdk/src/operations/ods/index.ts @@ -29,3 +29,7 @@ export type {SandboxState, WaitForSandboxOptions, WaitForSandboxPollInfo} from ' export {waitForClone, ClonePollingTimeoutError, ClonePollingError, CloneFailedError} from './wait-for-clone.js'; export type {CloneState, WaitForCloneOptions, WaitForClonePollInfo} from './wait-for-clone.js'; + +export {buildSandboxSettings, DEFAULT_OCAPI_RESOURCES, DEFAULT_WEBDAV_PERMISSIONS} from './sandbox-settings.js'; + +export type {BuildSandboxSettingsOptions} from './sandbox-settings.js'; diff --git a/packages/b2c-tooling-sdk/src/operations/ods/sandbox-settings.ts b/packages/b2c-tooling-sdk/src/operations/ods/sandbox-settings.ts new file mode 100644 index 000000000..ea48db003 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/ods/sandbox-settings.ts @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import type {components} from '../../clients/ods.generated.js'; + +type OcapiSettings = components['schemas']['OcapiSettings']; +type WebDavSettings = components['schemas']['WebDavSettings']; +type SandboxSettings = components['schemas']['SandboxSettings']; + +/** + * Default OCAPI resources to grant the client ID access to. + * These enable common CI/CD operations like code deployment and job execution. + */ +export const DEFAULT_OCAPI_RESOURCES: NonNullable = [ + {resource_id: '/code_versions', methods: ['get'], read_attributes: '(**)', write_attributes: '(**)'}, + {resource_id: '/code_versions/*', methods: ['patch', 'delete'], read_attributes: '(**)', write_attributes: '(**)'}, + {resource_id: '/jobs/*/executions', methods: ['post'], read_attributes: '(**)', write_attributes: '(**)'}, + {resource_id: '/jobs/*/executions/*', methods: ['get'], read_attributes: '(**)', write_attributes: '(**)'}, + {resource_id: '/sites/*/cartridges', methods: ['post'], read_attributes: '(**)', write_attributes: '(**)'}, +]; + +/** + * Default WebDAV permissions to grant the client ID. + * These enable common operations like code upload and data import/export. + */ +export const DEFAULT_WEBDAV_PERMISSIONS: WebDavSettings[number]['permissions'] = [ + {path: '/impex', operations: ['read_write']}, + {path: '/cartridges', operations: ['read_write']}, + {path: '/static', operations: ['read_write']}, +]; + +/** + * Options for {@link buildSandboxSettings}. + */ +export interface BuildSandboxSettingsOptions { + /** + * Client ID to grant default OCAPI/WebDAV permissions. When provided (and no + * custom settings are supplied), the defaults are applied for this client. + */ + clientId?: string; + /** + * Custom OCAPI settings array that fully replaces {@link DEFAULT_OCAPI_RESOURCES}. + */ + ocapiSettings?: OcapiSettings; + /** + * Custom WebDAV settings array that fully replaces {@link DEFAULT_WEBDAV_PERMISSIONS}. + */ + webdavSettings?: WebDavSettings; +} + +/** + * Builds the sandbox `settings` object granting OCAPI and WebDAV permissions to + * a client ID. New sandboxes have no API permissions by default, so the client + * used to create the sandbox (e.g. for code deployment) must be granted access + * explicitly or subsequent operations will fail with authorization errors. + * + * When `ocapiSettings`/`webdavSettings` are provided they fully replace the + * defaults. Otherwise, when a `clientId` is provided, the client is granted the + * default resources/permissions. + * + * @returns The settings object, or `undefined` when there is nothing to set + * (no client ID and no custom settings). + * + * @example + * const settings = buildSandboxSettings({clientId: config.values.clientId}); + * await odsClient.POST('/sandboxes', {body: {realm, ttl, settings}}); + */ +export function buildSandboxSettings(options: BuildSandboxSettingsOptions): SandboxSettings | undefined { + const hasCustomOcapi = options.ocapiSettings !== undefined; + const hasCustomWebdav = options.webdavSettings !== undefined; + const {clientId} = options; + + // Nothing to apply: no custom settings and no client ID for defaults. + if (!hasCustomOcapi && !hasCustomWebdav && !clientId) { + return undefined; + } + + const ocapi: OcapiSettings = hasCustomOcapi + ? options.ocapiSettings! + : clientId + ? [{client_id: clientId, resources: DEFAULT_OCAPI_RESOURCES}] + : []; + + const webdav: WebDavSettings = hasCustomWebdav + ? options.webdavSettings! + : clientId + ? [{client_id: clientId, permissions: DEFAULT_WEBDAV_PERMISSIONS}] + : []; + + return {ocapi, webdav}; +} diff --git a/packages/b2c-tooling-sdk/test/operations/ods/sandbox-settings.test.ts b/packages/b2c-tooling-sdk/test/operations/ods/sandbox-settings.test.ts new file mode 100644 index 000000000..170544b54 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/operations/ods/sandbox-settings.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import { + buildSandboxSettings, + DEFAULT_OCAPI_RESOURCES, + DEFAULT_WEBDAV_PERMISSIONS, +} from '../../../src/operations/ods/sandbox-settings.js'; + +describe('buildSandboxSettings', () => { + it('grants default OCAPI and WebDAV permissions to the client ID', () => { + const settings = buildSandboxSettings({clientId: 'client-123'}); + + expect(settings).to.not.be.undefined; + expect(settings!.ocapi).to.deep.equal([{client_id: 'client-123', resources: DEFAULT_OCAPI_RESOURCES}]); + expect(settings!.webdav).to.deep.equal([{client_id: 'client-123', permissions: DEFAULT_WEBDAV_PERMISSIONS}]); + }); + + it('returns undefined when no client ID and no custom settings are provided', () => { + expect(buildSandboxSettings({})).to.be.undefined; + expect(buildSandboxSettings({clientId: undefined})).to.be.undefined; + }); + + it('uses custom OCAPI settings in place of the defaults', () => { + const custom = [{client_id: 'other', resources: [{resource_id: '/foo', methods: ['get'] as const}]}]; + const settings = buildSandboxSettings({clientId: 'client-123', ocapiSettings: custom}); + + expect(settings!.ocapi).to.deep.equal(custom); + // WebDAV still falls back to defaults for the client ID + expect(settings!.webdav).to.deep.equal([{client_id: 'client-123', permissions: DEFAULT_WEBDAV_PERMISSIONS}]); + }); + + it('uses custom WebDAV settings in place of the defaults', () => { + const custom = [{client_id: 'other', permissions: [{path: '/impex', operations: ['read'] as const}]}]; + const settings = buildSandboxSettings({clientId: 'client-123', webdavSettings: custom}); + + expect(settings!.webdav).to.deep.equal(custom); + expect(settings!.ocapi).to.deep.equal([{client_id: 'client-123', resources: DEFAULT_OCAPI_RESOURCES}]); + }); + + it('builds settings from custom values even without a client ID', () => { + const ocapi = [{client_id: 'a', resources: []}]; + const webdav = [{client_id: 'a', permissions: []}]; + const settings = buildSandboxSettings({ocapiSettings: ocapi, webdavSettings: webdav}); + + expect(settings).to.deep.equal({ocapi, webdav}); + }); +}); diff --git a/packages/b2c-vs-extension/src/sandbox-tree/sandbox-commands.ts b/packages/b2c-vs-extension/src/sandbox-tree/sandbox-commands.ts index 0c8c9d5bc..9eb06f1c5 100644 --- a/packages/b2c-vs-extension/src/sandbox-tree/sandbox-commands.ts +++ b/packages/b2c-vs-extension/src/sandbox-tree/sandbox-commands.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2 * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ -import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk'; +import {buildSandboxSettings, getApiErrorMessage} from '@salesforce/b2c-tooling-sdk'; import {createOdsClient} from '@salesforce/b2c-tooling-sdk/clients'; import * as vscode from 'vscode'; import {registerSafeCommand, runWithSafety} from '../safety.js'; @@ -113,8 +113,13 @@ export function registerSandboxCommands( async () => { try { const odsClient = await getOdsClientFromConfig(configProvider); + // Grant the configured client the default OCAPI/WebDAV permissions so + // it can deploy code and run jobs against the new sandbox, matching the + // behavior of the CLI's `sandbox create` command. + const clientId = configProvider.getConfigProvider().getConfig()?.values.clientId; + const settings = buildSandboxSettings({clientId}); const result = await odsClient.POST('/sandboxes', { - body: {realm: realm!, ttl, analyticsEnabled: false}, + body: {realm: realm!, ttl, analyticsEnabled: false, settings}, }); if (result.error) { vscode.window.showErrorMessage( From 51c51b9a1fedced1a5dcd3514cad2601d3d00698 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Fri, 7 Aug 2026 10:54:18 -0400 Subject: [PATCH 2/2] test: fix OcapiSettings/WebDavSettings type mismatch in sandbox-settings test The custom test values used `as const`, producing readonly tuples that failed the SDK pretest type-check (tsc --noEmit -p test) in CI. Annotate them with the generated OcapiSettings/WebDavSettings types instead. --- .../test/operations/ods/sandbox-settings.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/b2c-tooling-sdk/test/operations/ods/sandbox-settings.test.ts b/packages/b2c-tooling-sdk/test/operations/ods/sandbox-settings.test.ts index 170544b54..1c3977669 100644 --- a/packages/b2c-tooling-sdk/test/operations/ods/sandbox-settings.test.ts +++ b/packages/b2c-tooling-sdk/test/operations/ods/sandbox-settings.test.ts @@ -5,12 +5,16 @@ */ import {expect} from 'chai'; +import type {components} from '../../../src/clients/ods.generated.js'; import { buildSandboxSettings, DEFAULT_OCAPI_RESOURCES, DEFAULT_WEBDAV_PERMISSIONS, } from '../../../src/operations/ods/sandbox-settings.js'; +type OcapiSettings = components['schemas']['OcapiSettings']; +type WebDavSettings = components['schemas']['WebDavSettings']; + describe('buildSandboxSettings', () => { it('grants default OCAPI and WebDAV permissions to the client ID', () => { const settings = buildSandboxSettings({clientId: 'client-123'}); @@ -26,7 +30,7 @@ describe('buildSandboxSettings', () => { }); it('uses custom OCAPI settings in place of the defaults', () => { - const custom = [{client_id: 'other', resources: [{resource_id: '/foo', methods: ['get'] as const}]}]; + const custom: OcapiSettings = [{client_id: 'other', resources: [{resource_id: '/foo', methods: ['get']}]}]; const settings = buildSandboxSettings({clientId: 'client-123', ocapiSettings: custom}); expect(settings!.ocapi).to.deep.equal(custom); @@ -35,7 +39,7 @@ describe('buildSandboxSettings', () => { }); it('uses custom WebDAV settings in place of the defaults', () => { - const custom = [{client_id: 'other', permissions: [{path: '/impex', operations: ['read'] as const}]}]; + const custom: WebDavSettings = [{client_id: 'other', permissions: [{path: '/impex', operations: ['read']}]}]; const settings = buildSandboxSettings({clientId: 'client-123', webdavSettings: custom}); expect(settings!.webdav).to.deep.equal(custom);