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 6387c74cc..c6f632c0b 100644 --- a/packages/b2c-tooling-sdk/src/index.ts +++ b/packages/b2c-tooling-sdk/src/index.ts @@ -307,6 +307,9 @@ export { ClonePollingTimeoutError, ClonePollingError, CloneFailedError, + buildSandboxSettings, + DEFAULT_OCAPI_RESOURCES, + DEFAULT_WEBDAV_PERMISSIONS, waitForClones, CloneBatchPollingTimeoutError, CloneBatchPollingError, @@ -315,6 +318,7 @@ export { 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'; export type {CloneBatchMemberStatus, WaitForClonesOptions, WaitForClonesPollInfo} from './operations/ods/index.js'; // Operations - CIP diff --git a/packages/b2c-tooling-sdk/src/operations/ods/index.ts b/packages/b2c-tooling-sdk/src/operations/ods/index.ts index dbdbe32de..676185706 100644 --- a/packages/b2c-tooling-sdk/src/operations/ods/index.ts +++ b/packages/b2c-tooling-sdk/src/operations/ods/index.ts @@ -30,6 +30,10 @@ export {waitForClone, ClonePollingTimeoutError, ClonePollingError, CloneFailedEr 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'; + export { waitForClones, CloneBatchPollingTimeoutError, 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..1c3977669 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/operations/ods/sandbox-settings.test.ts @@ -0,0 +1,56 @@ +/* + * 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 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'}); + + 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: 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); + // 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: WebDavSettings = [{client_id: 'other', permissions: [{path: '/impex', operations: ['read']}]}]; + 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 e3186e9d9..e324c36b2 100644 --- a/packages/b2c-vs-extension/src/sandbox-tree/sandbox-commands.ts +++ b/packages/b2c-vs-extension/src/sandbox-tree/sandbox-commands.ts @@ -4,6 +4,7 @@ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ import { + buildSandboxSettings, getApiErrorMessage, parseFriendlySandboxId, resolveSandboxId, @@ -218,8 +219,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(