cumulusci-plus 5.0.23__py3-none-any.whl → 5.0.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cumulusci-plus might be problematic. Click here for more details.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/task.py +17 -0
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_task.py +88 -2
- cumulusci/core/flowrunner.py +86 -6
- cumulusci/cumulusci.yml +24 -0
- cumulusci/tasks/create_package_version.py +14 -6
- cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
- cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
- cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/update_external_credential.py +562 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/users/permsets.py +63 -2
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +184 -0
- cumulusci/tasks/sfdmu/__init__.py +0 -0
- cumulusci/tasks/sfdmu/sfdmu.py +256 -0
- cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
- cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
- cumulusci/tasks/sfdmu/tests/test_sfdmu.py +443 -0
- cumulusci/utils/__init__.py +24 -2
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/METADATA +7 -5
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/RECORD +29 -16
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import root_validator
|
|
5
|
+
|
|
6
|
+
from cumulusci.core.exceptions import SalesforceDXException
|
|
7
|
+
from cumulusci.salesforce_api.utils import get_simple_salesforce_connection
|
|
8
|
+
from cumulusci.tasks.salesforce import BaseSalesforceApiTask
|
|
9
|
+
from cumulusci.utils.options import CCIOptions, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NamedCredentialCalloutOptions(CCIOptions):
|
|
13
|
+
# Callout options
|
|
14
|
+
allow_merge_fields_in_body: bool = Field(
|
|
15
|
+
None,
|
|
16
|
+
description="Allow merge fields in body. [default to None]",
|
|
17
|
+
)
|
|
18
|
+
allow_merge_fields_in_header: bool = Field(
|
|
19
|
+
None,
|
|
20
|
+
description="Allow merge fields in header. [default to None]",
|
|
21
|
+
)
|
|
22
|
+
generate_authorization_header: bool = Field(
|
|
23
|
+
None,
|
|
24
|
+
description="Generate authorization header. [default to None]",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NamedCredentialHttpHeader(CCIOptions):
|
|
29
|
+
name: str = Field(
|
|
30
|
+
None,
|
|
31
|
+
description="Name. [default to None]",
|
|
32
|
+
)
|
|
33
|
+
value: str = Field(
|
|
34
|
+
None,
|
|
35
|
+
description="Value. [default to None]",
|
|
36
|
+
)
|
|
37
|
+
sequence_number: int = Field(
|
|
38
|
+
None,
|
|
39
|
+
description="Sequence number. [default to None]",
|
|
40
|
+
)
|
|
41
|
+
secret: bool = Field(
|
|
42
|
+
False,
|
|
43
|
+
description="Is the value a secret. [default to False]",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NamedCredentialParameter(CCIOptions):
|
|
48
|
+
allowed_managed_package_namespaces: str = Field(
|
|
49
|
+
None,
|
|
50
|
+
description="Allowed managed package namespaces. [default to None]",
|
|
51
|
+
)
|
|
52
|
+
url: str = Field(
|
|
53
|
+
None,
|
|
54
|
+
description="Url. [default to None]",
|
|
55
|
+
)
|
|
56
|
+
authentication: str = Field(
|
|
57
|
+
None,
|
|
58
|
+
description="Authentication. [default to None]",
|
|
59
|
+
)
|
|
60
|
+
certificate: str = Field(
|
|
61
|
+
None,
|
|
62
|
+
description="Certificate. [default to None]",
|
|
63
|
+
)
|
|
64
|
+
http_header: List["NamedCredentialHttpHeader"] = Field(
|
|
65
|
+
[],
|
|
66
|
+
description="Http header. [default to empty list]",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@root_validator
|
|
70
|
+
def check_parameters(cls, values):
|
|
71
|
+
"""Check if only one of the parameters is provided"""
|
|
72
|
+
if (
|
|
73
|
+
len(
|
|
74
|
+
([values.get("url")] if values.get("url") else [])
|
|
75
|
+
+ (
|
|
76
|
+
[values.get("allowed_managed_package_namespaces")]
|
|
77
|
+
if values.get("allowed_managed_package_namespaces")
|
|
78
|
+
else []
|
|
79
|
+
)
|
|
80
|
+
+ (
|
|
81
|
+
[values.get("authentication")]
|
|
82
|
+
if values.get("authentication")
|
|
83
|
+
else []
|
|
84
|
+
)
|
|
85
|
+
+ ([values.get("certificate")] if values.get("certificate") else [])
|
|
86
|
+
+ (
|
|
87
|
+
[len(values.get("http_header"))]
|
|
88
|
+
if values.get("http_header")
|
|
89
|
+
else []
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
== 1
|
|
93
|
+
):
|
|
94
|
+
return values
|
|
95
|
+
raise ValueError("Only one of the parameters is required.")
|
|
96
|
+
|
|
97
|
+
def param_type(self):
|
|
98
|
+
"""Get the parameter type"""
|
|
99
|
+
if self.url:
|
|
100
|
+
return "Url"
|
|
101
|
+
if self.authentication:
|
|
102
|
+
return "Authentication"
|
|
103
|
+
if self.certificate:
|
|
104
|
+
return "ClientCertificate"
|
|
105
|
+
if self.allowed_managed_package_namespaces:
|
|
106
|
+
return "AllowedManagedPackageNamespaces"
|
|
107
|
+
if self.http_header:
|
|
108
|
+
return "HttpHeader"
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def param_value(self, http_header=None):
|
|
113
|
+
"""Get the parameter value"""
|
|
114
|
+
if self.url:
|
|
115
|
+
return self.url
|
|
116
|
+
if self.authentication:
|
|
117
|
+
return self.authentication
|
|
118
|
+
if self.certificate:
|
|
119
|
+
return self.certificate
|
|
120
|
+
if self.allowed_managed_package_namespaces:
|
|
121
|
+
return self.allowed_managed_package_namespaces
|
|
122
|
+
if http_header:
|
|
123
|
+
http_header_item = next(
|
|
124
|
+
(item for item in self.http_header if item.name == http_header), None
|
|
125
|
+
)
|
|
126
|
+
if http_header_item:
|
|
127
|
+
return http_header_item.value
|
|
128
|
+
|
|
129
|
+
def get_parameter_to_update(self):
|
|
130
|
+
"""Get the parameter to update"""
|
|
131
|
+
ret = []
|
|
132
|
+
|
|
133
|
+
if len(self.http_header) > 0:
|
|
134
|
+
for http_header_item in self.http_header:
|
|
135
|
+
param_to_update = {}
|
|
136
|
+
param_to_update["parameterName"] = http_header_item.name
|
|
137
|
+
param_to_update["parameterType"] = "HttpHeader"
|
|
138
|
+
param_to_update["parameterValue"] = self.param_value(
|
|
139
|
+
http_header=http_header_item.name
|
|
140
|
+
)
|
|
141
|
+
param_to_update["secret"] = http_header_item.secret
|
|
142
|
+
|
|
143
|
+
if http_header_item.sequence_number is not None:
|
|
144
|
+
param_to_update["sequenceNumber"] = http_header_item.sequence_number
|
|
145
|
+
|
|
146
|
+
ret.append(param_to_update.copy())
|
|
147
|
+
else:
|
|
148
|
+
param_to_update = {}
|
|
149
|
+
param_to_update["parameterType"] = self.param_type()
|
|
150
|
+
param_to_update["parameterValue"] = self.param_value()
|
|
151
|
+
|
|
152
|
+
if param_to_update["parameterType"] == "ClientCertificate":
|
|
153
|
+
param_to_update["parameterName"] = "ClientCertificate"
|
|
154
|
+
param_to_update["certificate"] = param_to_update["parameterValue"]
|
|
155
|
+
|
|
156
|
+
if param_to_update["parameterType"] == "Authentication":
|
|
157
|
+
param_to_update["parameterName"] = "ExternalCredential"
|
|
158
|
+
param_to_update["externalCredential"] = param_to_update[
|
|
159
|
+
"parameterValue"
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
ret.append(param_to_update.copy())
|
|
163
|
+
|
|
164
|
+
return ret
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TransformNamedCredentialParameter(NamedCredentialParameter):
|
|
168
|
+
def param_value(self, http_header=None):
|
|
169
|
+
value = super().param_value(http_header)
|
|
170
|
+
if value:
|
|
171
|
+
return os.getenv(value)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
NamedCredentialParameter.update_forward_refs()
|
|
176
|
+
TransformNamedCredentialParameter.update_forward_refs()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class UpdateNamedCredential(BaseSalesforceApiTask):
|
|
180
|
+
"""Custom task to update named credential parameters.
|
|
181
|
+
This task is based on the managability rules of named credentials.
|
|
182
|
+
https://developer.salesforce.com/docs/atlas.en-us.pkg2_dev.meta/pkg2_dev/packaging_packageable_components.htm#mdc_named_credential
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
class Options(CCIOptions):
|
|
186
|
+
name: str = Field(..., description="Name of the named credential to update.")
|
|
187
|
+
namespace: str = Field(
|
|
188
|
+
"",
|
|
189
|
+
description="Namespace of the named credential to update. [default to empty string]",
|
|
190
|
+
)
|
|
191
|
+
# Callout options
|
|
192
|
+
callout_options: NamedCredentialCalloutOptions = Field(
|
|
193
|
+
None,
|
|
194
|
+
description="Callout options. [default to None]",
|
|
195
|
+
)
|
|
196
|
+
# Named credential parameters
|
|
197
|
+
parameters: List[NamedCredentialParameter] = Field(
|
|
198
|
+
[],
|
|
199
|
+
description="Parameters to update. [default to empty list]",
|
|
200
|
+
)
|
|
201
|
+
# Named credential parameters
|
|
202
|
+
transform_parameters: List[TransformNamedCredentialParameter] = Field(
|
|
203
|
+
[],
|
|
204
|
+
description="Parameters to transform. [default to empty list]",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
parsed_options: Options
|
|
208
|
+
|
|
209
|
+
def _init_task(self):
|
|
210
|
+
self.tooling = get_simple_salesforce_connection(
|
|
211
|
+
self.project_config,
|
|
212
|
+
self.org_config,
|
|
213
|
+
api_version=self.project_config.project__package__api_version,
|
|
214
|
+
base_url="tooling",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _run_task(self):
|
|
218
|
+
# Step 1: Get the named credential id from the named credential name
|
|
219
|
+
named_credential_id = self._get_named_credential_id()
|
|
220
|
+
|
|
221
|
+
if not named_credential_id:
|
|
222
|
+
msg = f"Named credential '{self.parsed_options.name}' not found"
|
|
223
|
+
raise SalesforceDXException(msg)
|
|
224
|
+
|
|
225
|
+
# Step 2: Get the named credential object
|
|
226
|
+
named_credential = self._get_named_credential_object(named_credential_id)
|
|
227
|
+
|
|
228
|
+
if not named_credential:
|
|
229
|
+
msg = f"Failed to retrieve named credential object for '{self.parsed_options.name}'"
|
|
230
|
+
raise SalesforceDXException(msg)
|
|
231
|
+
|
|
232
|
+
if named_credential.get("namedCredentialType") != "SecuredEndpoint":
|
|
233
|
+
msg = f"Named credential '{self.parsed_options.name}' is not a secured endpoint, Aborting update. Only SecuredEndpoint is supported."
|
|
234
|
+
raise SalesforceDXException(msg)
|
|
235
|
+
|
|
236
|
+
# Step 3: Update the named credential parameters
|
|
237
|
+
self._update_named_credential_parameters(named_credential)
|
|
238
|
+
|
|
239
|
+
updated_named_credential = self._update_named_credential_object(
|
|
240
|
+
named_credential_id, named_credential
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if not updated_named_credential:
|
|
244
|
+
msg = f"Failed to update named credential object for '{self.parsed_options.name}'"
|
|
245
|
+
raise SalesforceDXException(msg)
|
|
246
|
+
|
|
247
|
+
self.logger.info(
|
|
248
|
+
f"Successfully updated named credential '{self.parsed_options.name}'"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def _get_named_credential_id(self) -> Optional[str]:
|
|
252
|
+
"""Get the named credential ID from the named credential name"""
|
|
253
|
+
query = f"SELECT Id FROM NamedCredential WHERE DeveloperName='{self.parsed_options.name}'"
|
|
254
|
+
|
|
255
|
+
if self.parsed_options.namespace:
|
|
256
|
+
query += f" AND NamespacePrefix='{self.parsed_options.namespace}'"
|
|
257
|
+
|
|
258
|
+
query += " LIMIT 1"
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
res = self.tooling.query(query)
|
|
262
|
+
if res["size"] == 0:
|
|
263
|
+
return None
|
|
264
|
+
return res["records"][0]["Id"]
|
|
265
|
+
except Exception as e:
|
|
266
|
+
self.logger.error(f"Error querying named credential: {str(e)}")
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
def _get_named_credential_object(
|
|
270
|
+
self, named_credential_id: str
|
|
271
|
+
) -> Optional[Dict[str, Any]]:
|
|
272
|
+
"""Get the named credential object using Metadata API"""
|
|
273
|
+
try:
|
|
274
|
+
# Use Tooling API to get the named credential metadata
|
|
275
|
+
result = self.tooling._call_salesforce(
|
|
276
|
+
method="GET",
|
|
277
|
+
url=f"{self.tooling.base_url}sobjects/NamedCredential/{named_credential_id}",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if result.status_code != 200:
|
|
281
|
+
self.logger.error(
|
|
282
|
+
f"Error retrieving named credential object: {result.json()}"
|
|
283
|
+
)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
return result.json().get("Metadata", None)
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
self.logger.error(f"Error retrieving named credential object: {str(e)}")
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
def _update_named_credential_object(
|
|
293
|
+
self, named_credential_id: str, named_credential: Dict[str, Any]
|
|
294
|
+
) -> Optional[bool]:
|
|
295
|
+
"""Update the named credential object"""
|
|
296
|
+
try:
|
|
297
|
+
resultUpdate = self.tooling._call_salesforce(
|
|
298
|
+
method="PATCH",
|
|
299
|
+
url=f"{self.tooling.base_url}sobjects/NamedCredential/{named_credential_id}",
|
|
300
|
+
json={"Metadata": named_credential},
|
|
301
|
+
)
|
|
302
|
+
if not resultUpdate.ok:
|
|
303
|
+
self.logger.error(
|
|
304
|
+
f"Error updating named credential object: {resultUpdate.json()}"
|
|
305
|
+
)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
return resultUpdate.ok
|
|
309
|
+
except Exception as e:
|
|
310
|
+
raise SalesforceDXException(
|
|
311
|
+
f"Failed to update named credential object: {str(e)}"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _update_named_credential_parameters(self, named_credential: Dict[str, Any]):
|
|
315
|
+
"""Update the named credential parameters"""
|
|
316
|
+
try:
|
|
317
|
+
template_param = self._get_named_credential_template_parameter(
|
|
318
|
+
named_credential
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
self._update_callout_options(named_credential)
|
|
322
|
+
self._update_parameters(
|
|
323
|
+
named_credential, self.parsed_options.parameters, template_param
|
|
324
|
+
)
|
|
325
|
+
self._update_parameters(
|
|
326
|
+
named_credential,
|
|
327
|
+
self.parsed_options.transform_parameters,
|
|
328
|
+
template_param,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
raise SalesforceDXException(f"Failed to update parameters: {str(e)}")
|
|
333
|
+
|
|
334
|
+
def _update_callout_options(self, named_credential: Dict[str, Any]):
|
|
335
|
+
"""Update the callout options"""
|
|
336
|
+
if not self.parsed_options.callout_options:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
if self.parsed_options.callout_options.allow_merge_fields_in_body:
|
|
340
|
+
named_credential[
|
|
341
|
+
"allowMergeFieldsInBody"
|
|
342
|
+
] = self.parsed_options.callout_options.allow_merge_fields_in_body
|
|
343
|
+
if self.parsed_options.callout_options.allow_merge_fields_in_header:
|
|
344
|
+
named_credential[
|
|
345
|
+
"allowMergeFieldsInHeader"
|
|
346
|
+
] = self.parsed_options.callout_options.allow_merge_fields_in_header
|
|
347
|
+
if self.parsed_options.callout_options.generate_authorization_header:
|
|
348
|
+
named_credential[
|
|
349
|
+
"generateAuthorizationHeader"
|
|
350
|
+
] = self.parsed_options.callout_options.generate_authorization_header
|
|
351
|
+
|
|
352
|
+
def _update_parameters(
|
|
353
|
+
self,
|
|
354
|
+
named_credential: Dict[str, Any],
|
|
355
|
+
named_credential_parameters: List[NamedCredentialParameter],
|
|
356
|
+
template_param: Dict[str, Any],
|
|
357
|
+
):
|
|
358
|
+
"""Update the parameters"""
|
|
359
|
+
for param_input in named_credential_parameters:
|
|
360
|
+
params_to_update = param_input.get_parameter_to_update()
|
|
361
|
+
|
|
362
|
+
for param_to_update in params_to_update:
|
|
363
|
+
secret = param_to_update.pop("secret", False)
|
|
364
|
+
|
|
365
|
+
param_to_update_copy = param_to_update.copy()
|
|
366
|
+
param_to_update_copy.pop("parameterValue", None)
|
|
367
|
+
param_to_update_copy.pop("certificate", None)
|
|
368
|
+
param_to_update_copy.pop("externalCredential", None)
|
|
369
|
+
|
|
370
|
+
cred_param = next(
|
|
371
|
+
(
|
|
372
|
+
param
|
|
373
|
+
for param in named_credential.get(
|
|
374
|
+
"namedCredentialParameters", []
|
|
375
|
+
)
|
|
376
|
+
if param_to_update_copy.items() <= param.items()
|
|
377
|
+
),
|
|
378
|
+
None,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if cred_param:
|
|
382
|
+
cred_param.update(param_to_update)
|
|
383
|
+
self.logger.info(
|
|
384
|
+
f"Updated parameter {cred_param['parameterType']}-{cred_param['parameterName']} with new value {param_to_update['parameterValue'] if not secret else '********'}"
|
|
385
|
+
)
|
|
386
|
+
else:
|
|
387
|
+
copy_template_param = template_param.copy()
|
|
388
|
+
copy_template_param.update(param_to_update)
|
|
389
|
+
named_credential["namedCredentialParameters"].append(
|
|
390
|
+
copy_template_param
|
|
391
|
+
)
|
|
392
|
+
self.logger.info(
|
|
393
|
+
f"Added parameter {copy_template_param['parameterType']}-{copy_template_param['parameterName']} with new value {param_to_update['parameterValue'] if not secret else '********'}"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def _update_parameter_value(
|
|
397
|
+
self, named_credential_parameter: Dict[str, Any], new_value: str
|
|
398
|
+
):
|
|
399
|
+
"""Update an existing parameter's value"""
|
|
400
|
+
update_data = {"parameterValue": new_value}
|
|
401
|
+
named_credential_parameter.update(update_data)
|
|
402
|
+
|
|
403
|
+
def _get_named_credential_template_parameter(
|
|
404
|
+
self, named_credential: Dict[str, Any]
|
|
405
|
+
) -> Dict[str, Any]:
|
|
406
|
+
"""Get the named credential template parameter"""
|
|
407
|
+
template_param = [
|
|
408
|
+
param
|
|
409
|
+
for param in named_credential.get("namedCredentialParameters", [])
|
|
410
|
+
if param.get("parameterType") == "Url"
|
|
411
|
+
]
|
|
412
|
+
if len(template_param) == 0:
|
|
413
|
+
self.logger.warning(
|
|
414
|
+
f"No template parameter found for named credential '{self.parsed_options.name}', using default template parameter."
|
|
415
|
+
)
|
|
416
|
+
return {
|
|
417
|
+
"certificate": None,
|
|
418
|
+
"description": None,
|
|
419
|
+
"externalCredential": None,
|
|
420
|
+
"globalNamedPrincipalCredential": None,
|
|
421
|
+
"managedFeatureEnabledCallout": None,
|
|
422
|
+
"outboundNetworkConnection": None,
|
|
423
|
+
"parameterName": None,
|
|
424
|
+
"parameterType": None,
|
|
425
|
+
"parameterValue": None,
|
|
426
|
+
"readOnlyNamedCredential": None,
|
|
427
|
+
"sequenceNumber": None,
|
|
428
|
+
"systemUserNamedCredential": None,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
ret = template_param[0].copy()
|
|
432
|
+
ret.update(
|
|
433
|
+
{
|
|
434
|
+
"parameterName": None,
|
|
435
|
+
"parameterType": None,
|
|
436
|
+
"parameterValue": None,
|
|
437
|
+
"sequenceNumber": None,
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
return ret
|
|
@@ -2,18 +2,27 @@ import json
|
|
|
2
2
|
|
|
3
3
|
from cumulusci.cli.ui import CliTable
|
|
4
4
|
from cumulusci.core.exceptions import CumulusCIException
|
|
5
|
-
from cumulusci.core.utils import process_list_arg
|
|
5
|
+
from cumulusci.core.utils import process_list_arg, process_bool_arg, determine_managed_mode
|
|
6
6
|
from cumulusci.tasks.salesforce import BaseSalesforceApiTask
|
|
7
|
+
from cumulusci.utils import inject_namespace
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class AssignPermissionSets(BaseSalesforceApiTask):
|
|
10
11
|
task_docs = """
|
|
11
12
|
Assigns Permission Sets whose Names are in ``api_names`` to either the default org user or the user whose Alias is ``user_alias``. This task skips assigning Permission Sets that are already assigned.
|
|
13
|
+
|
|
14
|
+
Permission Set names can include namespace tokens that will be replaced based on the context:
|
|
15
|
+
- ``%%%NAMESPACE%%%`` is replaced with the package's namespace in managed contexts (e.g., when the package is installed)
|
|
16
|
+
- ``%%%NAMESPACED_ORG%%%`` is replaced with the package's namespace in namespaced orgs only (e.g., packaging orgs)
|
|
17
|
+
- ``%%%NAMESPACE_OR_C%%%`` is replaced with the namespace in managed contexts, or 'c' otherwise
|
|
18
|
+
- ``%%%NAMESPACED_ORG_OR_C%%%`` is replaced with the namespace in namespaced orgs, or 'c' otherwise
|
|
19
|
+
|
|
20
|
+
The managed mode and namespaced org detection is automatic based on the org context.
|
|
12
21
|
"""
|
|
13
22
|
|
|
14
23
|
task_options = {
|
|
15
24
|
"api_names": {
|
|
16
|
-
"description": "API Names of desired Permission Sets, separated by commas.",
|
|
25
|
+
"description": "API Names of desired Permission Sets, separated by commas. Can include namespace tokens like %%%NAMESPACE%%%.",
|
|
17
26
|
"required": True,
|
|
18
27
|
},
|
|
19
28
|
"user_alias": {
|
|
@@ -36,7 +45,51 @@ Assigns Permission Sets whose Names are in ``api_names`` to either the default o
|
|
|
36
45
|
self.options.get("user_alias") or []
|
|
37
46
|
)
|
|
38
47
|
|
|
48
|
+
def _init_namespace_injection(self):
|
|
49
|
+
"""Initialize namespace injection options for processing permission set names.
|
|
50
|
+
|
|
51
|
+
This automatically determines managed mode and namespaced org context based on:
|
|
52
|
+
- Whether the package is installed in the org (managed mode)
|
|
53
|
+
- Whether we're in a packaging org (namespaced org)
|
|
54
|
+
"""
|
|
55
|
+
namespace = self.project_config.project__package__namespace
|
|
56
|
+
|
|
57
|
+
# Automatically determine managed mode based on org context
|
|
58
|
+
managed = determine_managed_mode(
|
|
59
|
+
self.options, self.project_config, self.org_config
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Automatically determine if we're in a namespaced org (e.g., packaging org)
|
|
63
|
+
namespaced_org = (
|
|
64
|
+
bool(namespace) and namespace == getattr(self.org_config, "namespace", None)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Store in options for use by inject_namespace
|
|
68
|
+
self.options["namespace_inject"] = namespace
|
|
69
|
+
self.options["managed"] = managed
|
|
70
|
+
self.options["namespaced_org"] = namespaced_org
|
|
71
|
+
|
|
72
|
+
def _inject_namespace(self, text):
|
|
73
|
+
"""Inject the namespace into the given text if running in managed mode."""
|
|
74
|
+
if self.org_config is None:
|
|
75
|
+
return text
|
|
76
|
+
return inject_namespace(
|
|
77
|
+
"",
|
|
78
|
+
text,
|
|
79
|
+
namespace=self.options.get("namespace_inject"),
|
|
80
|
+
managed=self.options.get("managed") or False,
|
|
81
|
+
namespaced_org=self.options.get("namespaced_org"),
|
|
82
|
+
)[1]
|
|
83
|
+
|
|
39
84
|
def _run_task(self):
|
|
85
|
+
# Initialize namespace injection only if tokens are present or options are set
|
|
86
|
+
if self.org_config and self._needs_namespace_injection():
|
|
87
|
+
self._init_namespace_injection()
|
|
88
|
+
# Process namespace tokens in api_names
|
|
89
|
+
self.options["api_names"] = [
|
|
90
|
+
self._inject_namespace(api_name) for api_name in self.options["api_names"]
|
|
91
|
+
]
|
|
92
|
+
|
|
40
93
|
users = self._query_existing_assignments()
|
|
41
94
|
users_assigned_perms = {
|
|
42
95
|
user["Id"]: self._get_assigned_perms(user) for user in users
|
|
@@ -51,6 +104,14 @@ Assigns Permission Sets whose Names are in ``api_names`` to either the default o
|
|
|
51
104
|
|
|
52
105
|
self._insert_assignments(records_to_insert)
|
|
53
106
|
|
|
107
|
+
def _needs_namespace_injection(self):
|
|
108
|
+
"""Check if namespace injection is needed based on presence of tokens in api_names."""
|
|
109
|
+
namespace_tokens = ["%%%NAMESPACE%%%", "%%%NAMESPACED_ORG%%%", "%%%NAMESPACE_OR_C%%%", "%%%NAMESPACED_ORG_OR_C%%%", "%%%NAMESPACE_DOT%%%"]
|
|
110
|
+
return any(
|
|
111
|
+
any(token in api_name for token in namespace_tokens)
|
|
112
|
+
for api_name in self.options["api_names"]
|
|
113
|
+
)
|
|
114
|
+
|
|
54
115
|
def _query_existing_assignments(self):
|
|
55
116
|
if not self.options["user_alias"]:
|
|
56
117
|
query = (
|