Skip to content

Commit 2df6adf

Browse files
blaissao : apply suggested fixs (code refactoring)
1 parent a48424d commit 2df6adf

3 files changed

Lines changed: 107 additions & 82 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: 37 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -52,52 +52,29 @@ export default class DeleteSandbox extends SfCommand<SandboxDeleteResponse> {
5252
throw error;
5353
}
5454

55-
// The StateAggregator identifies sandbox auth files with a pattern of
56-
// <sandbox_ID>.sandbox.json. E.g., 00DZ0000009T3VZMA0.sandbox.json
5755
const stateAggregator = await StateAggregator.getInstance();
58-
const cliCreatedSandbox = await stateAggregator.sandboxes.hasFile(orgId);
5956

60-
if (!cliCreatedSandbox) {
61-
throw messages.createError('error.unknownSandbox', [username]);
62-
}
57+
await this.verifyDeletePermission(stateAggregator, orgId, username);
6358

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-
}
59+
const org = await Org.create({ aliasOrUsername: username });
8660

8761
if (flags['no-prompt'] || (await this.confirm({ message: messages.getMessage('prompt.confirm', [username]) }))) {
8862
try {
89-
const org = await Org.create({ aliasOrUsername: username });
9063
await org.delete();
9164
this.logSuccess(messages.getMessage('success', [username]));
9265
} catch (e) {
9366
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
9667
const authRemover = await AuthRemover.create();
9768
await authRemover.removeAuth(username);
9869
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
9970
} else if (e instanceof Error && e.name === 'SandboxNotFound') {
10071
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
72+
} else if (
73+
e instanceof Error &&
74+
'errorCode' in e &&
75+
(e as { errorCode: string }).errorCode === 'INSUFFICIENT_ACCESS_OR_READONLY'
76+
) {
77+
throw messages.createError('error.insufficientAccess');
10178
} else {
10279
throw e;
10380
}
@@ -107,42 +84,39 @@ export default class DeleteSandbox extends SfCommand<SandboxDeleteResponse> {
10784
}
10885

10986
/**
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
87+
* Verifies the current user has the 'DeleteSandbox' PermissionSet assigned
88+
* in the production org that owns the sandbox.
11589
*/
11690
// 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-
}
91+
private async verifyDeletePermission(
92+
stateAggregator: StateAggregator,
93+
orgId: string,
94+
sandboxUsername: string
95+
): Promise<void> {
96+
const sandboxConfig = await stateAggregator.sandboxes.read(orgId);
97+
const prodOrgUsername = sandboxConfig?.prodOrgUsername;
12898

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-
`;
99+
if (!prodOrgUsername) {
100+
return;
101+
}
136102

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;
103+
const prodOrg = await Org.create({ aliasOrUsername: prodOrgUsername });
104+
const connection = prodOrg.getConnection();
105+
await prodOrg.refreshAuth();
106+
107+
const identity = await connection.identity();
108+
const userId = identity.user_id;
109+
110+
if (!userId) {
111+
throw messages.createError('error.insufficientPermissions', [sandboxUsername]);
112+
}
113+
114+
const result = await connection.query(
115+
`SELECT Id FROM PermissionSetAssignment WHERE AssigneeId = '${userId}' AND PermissionSet.Name = 'DeleteSandbox'`
116+
);
117+
118+
if (result.totalSize === 0) {
119+
throw messages.createError('error.insufficientPermissions', [sandboxUsername]);
146120
}
147121
}
148122
}

test/unit/org/delete.test.ts

Lines changed: 62 additions & 16 deletions
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 {
@@ -67,21 +73,6 @@ describe('org delete', () => {
6773
}
6874
});
6975

70-
it('will throw an error when the org is not identified as a sandbox', async () => {
71-
$$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(false);
72-
await $$.stubConfig({ 'target-org': testOrg.username });
73-
try {
74-
await DeleteSandbox.run(['--target-org', testOrg.username]);
75-
expect.fail('should have thrown UnknownSandboxError');
76-
} catch (e) {
77-
const err = e as SfError;
78-
expect(err.name).to.equal('UnknownSandboxError');
79-
const expectedActions = sbxOrgMessages.getMessage('error.unknownSandbox.actions');
80-
const errorActions = err.actions ?? [];
81-
expect(errorActions[0]).to.equal(expectedActions);
82-
}
83-
});
84-
8576
it('will prompt before attempting to delete by username', async () => {
8677
$$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true);
8778
await $$.stubConfig({ 'target-org': testOrg.username });
@@ -128,6 +119,61 @@ describe('org delete', () => {
128119
JSON.stringify(sfCommandUxStubs.logSuccess.getCalls().flatMap((call) => call.args))
129120
).to.deep.include(sbxOrgMessages.getMessage('success.Idempotent', [testOrg.username]));
130121
});
122+
123+
it('will throw a clean SfError when the user lacks Manage Sandboxes permission', async () => {
124+
$$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true);
125+
orgDeleteStub.restore();
126+
const insufficientAccessError = Object.assign(new Error('INSUFFICIENT_ACCESS_OR_READONLY'), {
127+
errorCode: 'INSUFFICIENT_ACCESS_OR_READONLY',
128+
});
129+
$$.SANDBOX.stub(Org.prototype, 'delete').throws(insufficientAccessError);
130+
try {
131+
await DeleteSandbox.run(['--no-prompt', '--target-org', testOrg.username]);
132+
expect.fail('should have thrown InsufficientAccessError');
133+
} catch (e) {
134+
const err = e as SfError;
135+
expect(err.name).to.equal('InsufficientAccessError');
136+
expect(err.message).to.equal(sbxOrgMessages.getMessage('error.insufficientAccess'));
137+
}
138+
});
139+
140+
describe('DeleteSandbox permission check', () => {
141+
let identityStub: SinonStub;
142+
let queryStub: SinonStub;
143+
144+
beforeEach(() => {
145+
$$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true);
146+
$$.SANDBOX.stub(Org.prototype, 'refreshAuth').resolves();
147+
identityStub = $$.SANDBOX.stub(Connection.prototype, 'identity');
148+
queryStub = $$.SANDBOX.stub(Connection.prototype, 'query');
149+
});
150+
151+
it('will allow deletion when user has DeleteSandbox permission set', async () => {
152+
sandboxReadStub.resolves({ sandboxOrgId: testOrg.orgId, prodOrgUsername: testHub.username });
153+
// eslint-disable-next-line camelcase
154+
identityStub.resolves({ user_id: '005xx000001X' });
155+
queryStub.resolves({ totalSize: 1, records: [{ Id: '0Pa000000000001' }] });
156+
const res = await DeleteSandbox.run(['--no-prompt', '--target-org', testOrg.username]);
157+
expect(sfCommandUxStubs.logSuccess.callCount).to.equal(1);
158+
expect(res).to.deep.equal({ orgId: testOrg.orgId, username: testOrg.username });
159+
});
160+
161+
it('will block deletion when user lacks DeleteSandbox permission set', async () => {
162+
sandboxReadStub.resolves({ sandboxOrgId: testOrg.orgId, prodOrgUsername: testHub.username });
163+
// eslint-disable-next-line camelcase
164+
identityStub.resolves({ user_id: '005xx000001X' });
165+
queryStub.resolves({ totalSize: 0, records: [] });
166+
try {
167+
await DeleteSandbox.run(['--no-prompt', '--target-org', testOrg.username]);
168+
expect.fail('should have thrown InsufficientPermissionsError');
169+
} catch (e) {
170+
const err = e as SfError;
171+
expect(err.name).to.equal('InsufficientPermissionsError');
172+
expect(err.message).to.include(testOrg.username);
173+
expect(err.message).to.include('DeleteSandbox');
174+
}
175+
});
176+
});
131177
});
132178

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

0 commit comments

Comments
 (0)