-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathsandbox.ts
More file actions
148 lines (134 loc) · 5.63 KB
/
sandbox.ts
File metadata and controls
148 lines (134 loc) · 5.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { AuthInfo, AuthRemover, Messages, Org, SfError, StateAggregator } from '@salesforce/core';
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { orgThatMightBeDeleted } from '../../../shared/flags.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete_sandbox');
export type SandboxDeleteResponse = {
orgId: string;
username: string;
};
export default class DeleteSandbox extends SfCommand<SandboxDeleteResponse> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');
public static readonly aliases = ['env:delete:sandbox'];
public static readonly deprecateAliases = true;
public static readonly flags = {
'target-org': orgThatMightBeDeleted({
summary: messages.getMessage('flags.target-org.summary'),
required: true,
}),
'no-prompt': Flags.boolean({
char: 'p',
summary: messages.getMessage('flags.no-prompt.summary'),
}),
};
public async run(): Promise<SandboxDeleteResponse> {
const flags = (await this.parse(DeleteSandbox)).flags;
const username = flags['target-org'];
let orgId: string;
try {
const sbxAuthFields = (await AuthInfo.create({ username })).getFields();
orgId = sbxAuthFields.orgId as string;
} catch (error) {
if (error instanceof SfError && error.name === 'NamedOrgNotFoundError') {
error.actions = [
`Ensure the alias or username for the ${username} org is correct.`,
`Ensure the ${username} org has been authenticated with the CLI.`,
];
}
throw error;
}
// The StateAggregator identifies sandbox auth files with a pattern of
// <sandbox_ID>.sandbox.json. E.g., 00DZ0000009T3VZMA0.sandbox.json
const stateAggregator = await StateAggregator.getInstance();
const cliCreatedSandbox = await stateAggregator.sandboxes.hasFile(orgId);
if (!cliCreatedSandbox) {
throw messages.createError('error.unknownSandbox', [username]);
}
// Check if user has the DeleteSandbox PermissionSet in the sandbox
try {
const sandboxOrg = await Org.create({ aliasOrUsername: username });
const hasDeleteSandboxPermission = await this.hasPermission(sandboxOrg, 'DeleteSandbox');
this.debug('hasDeleteSandboxPermission %s ', hasDeleteSandboxPermission);
if (!hasDeleteSandboxPermission) {
throw messages.createError('error.insufficientPermissions', [username]);
}
} catch (error) {
// If it's a permission error we created, re-throw it
if (error instanceof SfError) {
const errorMessage = error.message || '';
if (errorMessage.includes('required permission') || errorMessage.includes('DeleteSandbox')) {
throw error;
}
}
// For other errors (e.g., org connection issues), log a warning but continue
// The actual delete operation will fail if permissions are truly insufficient
if (error instanceof Error) {
this.warn(messages.getMessage('warning.couldNotVerifyPermissions', [username]));
}
}
if (flags['no-prompt'] || (await this.confirm({ message: messages.getMessage('prompt.confirm', [username]) }))) {
try {
const org = await Org.create({ aliasOrUsername: username });
await org.delete();
this.logSuccess(messages.getMessage('success', [username]));
} catch (e) {
if (e instanceof Error && e.name === 'DomainNotFoundError') {
// the org has expired, so remote operations won't work
// let's clean up the files locally
const authRemover = await AuthRemover.create();
await authRemover.removeAuth(username);
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
} else if (e instanceof Error && e.name === 'SandboxNotFound') {
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
} else {
throw e;
}
}
}
return { username, orgId };
}
/**
* Checks if the current user has a PermissionSet with the specified name assigned.
*
* @param org The org to check permissions in
* @param permissionSetName The name of the PermissionSet to check (e.g., 'DeleteSandbox')
* @returns True if the user has the PermissionSet assigned, false otherwise
*/
// eslint-disable-next-line class-methods-use-this
private async hasPermission(org: Org, permissionSetName: string): Promise<boolean> {
try {
const connection = org.getConnection();
await org.refreshAuth();
// try to get it from Identity API
const identity = await connection.identity();
const userId = identity.user_id;
if (!userId) {
return false;
}
// Check if user has the PermissionSet assigned
const permissionSetAssignmentQuery = `
SELECT Id
FROM PermissionSetAssignment
WHERE AssigneeId = '${userId.replace(/'/g, "\\'")}'
AND PermissionSet.Name = '${permissionSetName.replace(/'/g, "\\'")}'
`;
try {
const permissionSetResult = await connection.query(permissionSetAssignmentQuery);
return permissionSetResult.totalSize > 0;
} catch {
// If query fails, return false
return false;
}
} catch {
return false;
}
}
}