Skip to content

Commit 4d15afb

Browse files
blaissao : apply suggested fixs (code refactoring)
1 parent a48424d commit 4d15afb

3 files changed

Lines changed: 108 additions & 61 deletions

File tree

messages/delete_sandbox.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,13 @@ Unable to determine the username of the org to delete. Specify the username with
5757

5858
# error.insufficientPermissions
5959

60-
You do not have the required permission to delete this sandbox: %s. Contact your administrator to assign you the "DeleteSandbox" PermissionSet.
60+
You don't have the required "DeleteSandbox" Permission Set assigned in the production org to delete sandbox "%s".
6161

62-
# warning.couldNotVerifyPermissions
62+
# error.insufficientPermissions.actions
6363

64-
Could not verify permissions in sandbox: %s. The delete operation will proceed, but may fail if you do not have the required permissions.
64+
- Ask your administrator to create a Permission Set named "DeleteSandbox" in the production org and assign it to your user.
65+
- Re-authenticate with the production org and try again.
66+
67+
# error.insufficientAccess
68+
69+
You don't have permission to delete this sandbox. Ask your Salesforce admin to grant you the "Manage Sandboxes" system permission on your profile or a permission set in the production org.

src/commands/org/delete/sandbox.ts

Lines changed: 38 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -61,43 +61,27 @@ export default class DeleteSandbox extends SfCommand<SandboxDeleteResponse> {
6161
throw messages.createError('error.unknownSandbox', [username]);
6262
}
6363

64-
// Check if user has the DeleteSandbox PermissionSet in the sandbox
65-
try {
66-
const sandboxOrg = await Org.create({ aliasOrUsername: username });
67-
const hasDeleteSandboxPermission = await this.hasPermission(sandboxOrg, 'DeleteSandbox');
68-
this.debug('hasDeleteSandboxPermission %s ', hasDeleteSandboxPermission);
69-
if (!hasDeleteSandboxPermission) {
70-
throw messages.createError('error.insufficientPermissions', [username]);
71-
}
72-
} catch (error) {
73-
// If it's a permission error we created, re-throw it
74-
if (error instanceof SfError) {
75-
const errorMessage = error.message || '';
76-
if (errorMessage.includes('required permission') || errorMessage.includes('DeleteSandbox')) {
77-
throw error;
78-
}
79-
}
80-
// For other errors (e.g., org connection issues), log a warning but continue
81-
// The actual delete operation will fail if permissions are truly insufficient
82-
if (error instanceof Error) {
83-
this.warn(messages.getMessage('warning.couldNotVerifyPermissions', [username]));
84-
}
85-
}
64+
await this.verifyDeletePermission(stateAggregator, orgId, username);
65+
66+
const org = await Org.create({ aliasOrUsername: username });
8667

8768
if (flags['no-prompt'] || (await this.confirm({ message: messages.getMessage('prompt.confirm', [username]) }))) {
8869
try {
89-
const org = await Org.create({ aliasOrUsername: username });
9070
await org.delete();
9171
this.logSuccess(messages.getMessage('success', [username]));
9272
} catch (e) {
9373
if (e instanceof Error && e.name === 'DomainNotFoundError') {
94-
// the org has expired, so remote operations won't work
95-
// let's clean up the files locally
9674
const authRemover = await AuthRemover.create();
9775
await authRemover.removeAuth(username);
9876
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
9977
} else if (e instanceof Error && e.name === 'SandboxNotFound') {
10078
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
79+
} else if (
80+
e instanceof Error &&
81+
'errorCode' in e &&
82+
(e as { errorCode: string }).errorCode === 'INSUFFICIENT_ACCESS_OR_READONLY'
83+
) {
84+
throw messages.createError('error.insufficientAccess');
10185
} else {
10286
throw e;
10387
}
@@ -107,42 +91,39 @@ export default class DeleteSandbox extends SfCommand<SandboxDeleteResponse> {
10791
}
10892

10993
/**
110-
* Checks if the current user has a PermissionSet with the specified name assigned.
111-
*
112-
* @param org The org to check permissions in
113-
* @param permissionSetName The name of the PermissionSet to check (e.g., 'DeleteSandbox')
114-
* @returns True if the user has the PermissionSet assigned, false otherwise
94+
* Verifies the current user has the 'DeleteSandbox' PermissionSet assigned
95+
* in the production org that owns the sandbox.
11596
*/
11697
// eslint-disable-next-line class-methods-use-this
117-
private async hasPermission(org: Org, permissionSetName: string): Promise<boolean> {
118-
try {
119-
const connection = org.getConnection();
120-
await org.refreshAuth();
121-
// try to get it from Identity API
122-
const identity = await connection.identity();
123-
const userId = identity.user_id;
124-
125-
if (!userId) {
126-
return false;
127-
}
98+
private async verifyDeletePermission(
99+
stateAggregator: StateAggregator,
100+
orgId: string,
101+
sandboxUsername: string
102+
): Promise<void> {
103+
const sandboxConfig = await stateAggregator.sandboxes.read(orgId);
104+
const prodOrgUsername = sandboxConfig?.prodOrgUsername;
128105

129-
// Check if user has the PermissionSet assigned
130-
const permissionSetAssignmentQuery = `
131-
SELECT Id
132-
FROM PermissionSetAssignment
133-
WHERE AssigneeId = '${userId.replace(/'/g, "\\'")}'
134-
AND PermissionSet.Name = '${permissionSetName.replace(/'/g, "\\'")}'
135-
`;
106+
if (!prodOrgUsername) {
107+
return;
108+
}
136109

137-
try {
138-
const permissionSetResult = await connection.query(permissionSetAssignmentQuery);
139-
return permissionSetResult.totalSize > 0;
140-
} catch {
141-
// If query fails, return false
142-
return false;
143-
}
144-
} catch {
145-
return false;
110+
const prodOrg = await Org.create({ aliasOrUsername: prodOrgUsername });
111+
const connection = prodOrg.getConnection();
112+
await prodOrg.refreshAuth();
113+
114+
const identity = await connection.identity();
115+
const userId = identity.user_id;
116+
117+
if (!userId) {
118+
throw messages.createError('error.insufficientPermissions', [sandboxUsername]);
119+
}
120+
121+
const result = await connection.query(
122+
`SELECT Id FROM PermissionSetAssignment WHERE AssigneeId = '${userId}' AND PermissionSet.Name = 'DeleteSandbox'`
123+
);
124+
125+
if (result.totalSize === 0) {
126+
throw messages.createError('error.insufficientPermissions', [sandboxUsername]);
146127
}
147128
}
148129
}

test/unit/org/delete.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import { AuthInfo, Messages, Org, SfError } from '@salesforce/core';
8+
import { AuthInfo, Connection, Messages, Org, SfError } from '@salesforce/core';
99
import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup';
1010
import { SinonStub } from 'sinon';
1111
import { config, expect } from 'chai';
@@ -38,6 +38,12 @@ describe('org delete', () => {
3838
});
3939

4040
describe('sandbox', () => {
41+
let sandboxReadStub: SinonStub;
42+
43+
beforeEach(() => {
44+
sandboxReadStub = $$.SANDBOX.stub(SandboxAccessor.prototype, 'read').resolves(null);
45+
});
46+
4147
it('will throw an error when no org provided', async () => {
4248
await $$.stubConfig({});
4349
try {
@@ -128,6 +134,61 @@ describe('org delete', () => {
128134
JSON.stringify(sfCommandUxStubs.logSuccess.getCalls().flatMap((call) => call.args))
129135
).to.deep.include(sbxOrgMessages.getMessage('success.Idempotent', [testOrg.username]));
130136
});
137+
138+
it('will throw a clean SfError when the user lacks Manage Sandboxes permission', async () => {
139+
$$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true);
140+
orgDeleteStub.restore();
141+
const insufficientAccessError = Object.assign(new Error('INSUFFICIENT_ACCESS_OR_READONLY'), {
142+
errorCode: 'INSUFFICIENT_ACCESS_OR_READONLY',
143+
});
144+
$$.SANDBOX.stub(Org.prototype, 'delete').throws(insufficientAccessError);
145+
try {
146+
await DeleteSandbox.run(['--no-prompt', '--target-org', testOrg.username]);
147+
expect.fail('should have thrown InsufficientAccessError');
148+
} catch (e) {
149+
const err = e as SfError;
150+
expect(err.name).to.equal('InsufficientAccessError');
151+
expect(err.message).to.equal(sbxOrgMessages.getMessage('error.insufficientAccess'));
152+
}
153+
});
154+
155+
describe('DeleteSandbox permission check', () => {
156+
let identityStub: SinonStub;
157+
let queryStub: SinonStub;
158+
159+
beforeEach(() => {
160+
$$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true);
161+
$$.SANDBOX.stub(Org.prototype, 'refreshAuth').resolves();
162+
identityStub = $$.SANDBOX.stub(Connection.prototype, 'identity');
163+
queryStub = $$.SANDBOX.stub(Connection.prototype, 'query');
164+
});
165+
166+
it('will allow deletion when user has DeleteSandbox permission set', async () => {
167+
sandboxReadStub.resolves({ sandboxOrgId: testOrg.orgId, prodOrgUsername: testHub.username });
168+
// eslint-disable-next-line camelcase
169+
identityStub.resolves({ user_id: '005xx000001X' });
170+
queryStub.resolves({ totalSize: 1, records: [{ Id: '0Pa000000000001' }] });
171+
const res = await DeleteSandbox.run(['--no-prompt', '--target-org', testOrg.username]);
172+
expect(sfCommandUxStubs.logSuccess.callCount).to.equal(1);
173+
expect(res).to.deep.equal({ orgId: testOrg.orgId, username: testOrg.username });
174+
});
175+
176+
it('will block deletion when user lacks DeleteSandbox permission set', async () => {
177+
sandboxReadStub.resolves({ sandboxOrgId: testOrg.orgId, prodOrgUsername: testHub.username });
178+
// eslint-disable-next-line camelcase
179+
identityStub.resolves({ user_id: '005xx000001X' });
180+
queryStub.resolves({ totalSize: 0, records: [] });
181+
try {
182+
await DeleteSandbox.run(['--no-prompt', '--target-org', testOrg.username]);
183+
expect.fail('should have thrown InsufficientPermissionsError');
184+
} catch (e) {
185+
const err = e as SfError;
186+
expect(err.name).to.equal('InsufficientPermissionsError');
187+
expect(err.message).to.include(testOrg.username);
188+
expect(err.message).to.include('DeleteSandbox');
189+
}
190+
});
191+
});
131192
});
132193

133194
describe('scratch', () => {

0 commit comments

Comments
 (0)