Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/sandbox-create-default-permissions.md
Original file line number Diff line number Diff line change
@@ -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`.
58 changes: 12 additions & 46 deletions packages/b2c-cli/src/commands/sandbox/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<OcapiSettings[number]['resources']> = [
{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.
*/
Expand Down Expand Up @@ -286,29 +264,17 @@ export default class SandboxCreate extends OdsCommand<typeof SandboxCreate> {
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<OcapiSettings>('ocapi-settings', options.ocapiSettings),
webdavSettings:
options.webdavSettings === undefined
? undefined
: this.parseJsonFlag<WebDavSettings>('webdav-settings', options.webdavSettings),
});
}

private parseJsonFlag<T>(flagName: string, value: string): T {
Expand Down
4 changes: 4 additions & 0 deletions packages/b2c-tooling-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ export {
ClonePollingTimeoutError,
ClonePollingError,
CloneFailedError,
buildSandboxSettings,
DEFAULT_OCAPI_RESOURCES,
DEFAULT_WEBDAV_PERMISSIONS,
waitForClones,
CloneBatchPollingTimeoutError,
CloneBatchPollingError,
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/b2c-tooling-sdk/src/operations/ods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
94 changes: 94 additions & 0 deletions packages/b2c-tooling-sdk/src/operations/ods/sandbox-settings.ts
Original file line number Diff line number Diff line change
@@ -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<OcapiSettings[number]['resources']> = [
{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};
}
Original file line number Diff line number Diff line change
@@ -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});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Loading