cumulusci-plus 5.0.21__py3-none-any.whl → 5.0.43__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.
Files changed (135) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/logger.py +2 -2
  3. cumulusci/cli/service.py +20 -0
  4. cumulusci/cli/task.py +19 -3
  5. cumulusci/cli/tests/test_error.py +3 -1
  6. cumulusci/cli/tests/test_flow.py +279 -2
  7. cumulusci/cli/tests/test_org.py +5 -0
  8. cumulusci/cli/tests/test_service.py +15 -12
  9. cumulusci/cli/tests/test_task.py +122 -2
  10. cumulusci/cli/tests/utils.py +1 -4
  11. cumulusci/core/config/__init__.py +1 -0
  12. cumulusci/core/config/base_task_flow_config.py +26 -1
  13. cumulusci/core/config/org_config.py +2 -1
  14. cumulusci/core/config/project_config.py +14 -20
  15. cumulusci/core/config/scratch_org_config.py +12 -0
  16. cumulusci/core/config/tests/test_config.py +1 -0
  17. cumulusci/core/config/tests/test_config_expensive.py +9 -3
  18. cumulusci/core/config/universal_config.py +3 -4
  19. cumulusci/core/dependencies/base.py +5 -1
  20. cumulusci/core/dependencies/dependencies.py +1 -1
  21. cumulusci/core/dependencies/github.py +1 -2
  22. cumulusci/core/dependencies/resolvers.py +1 -1
  23. cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
  24. cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
  25. cumulusci/core/flowrunner.py +90 -6
  26. cumulusci/core/github.py +1 -1
  27. cumulusci/core/sfdx.py +3 -1
  28. cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
  29. cumulusci/core/source_transforms/transforms.py +1 -1
  30. cumulusci/core/tasks.py +13 -2
  31. cumulusci/core/tests/test_flowrunner.py +100 -0
  32. cumulusci/core/tests/test_tasks.py +65 -0
  33. cumulusci/core/utils.py +3 -1
  34. cumulusci/core/versions.py +1 -1
  35. cumulusci/cumulusci.yml +73 -1
  36. cumulusci/oauth/client.py +1 -1
  37. cumulusci/plugins/plugin_base.py +5 -3
  38. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  39. cumulusci/salesforce_api/rest_deploy.py +1 -1
  40. cumulusci/schema/cumulusci.jsonschema.json +69 -0
  41. cumulusci/tasks/apex/anon.py +1 -1
  42. cumulusci/tasks/apex/testrunner.py +421 -144
  43. cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
  44. cumulusci/tasks/bulkdata/extract.py +0 -1
  45. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
  46. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
  47. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
  48. cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
  49. cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
  50. cumulusci/tasks/bulkdata/select_utils.py +1 -1
  51. cumulusci/tasks/bulkdata/snowfakery.py +100 -25
  52. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
  53. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  54. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
  55. cumulusci/tasks/bulkdata/tests/test_select_utils.py +46 -0
  56. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
  57. cumulusci/tasks/create_package_version.py +190 -16
  58. cumulusci/tasks/datadictionary.py +1 -1
  59. cumulusci/tasks/metadata_etl/__init__.py +2 -0
  60. cumulusci/tasks/metadata_etl/applications.py +256 -0
  61. cumulusci/tasks/metadata_etl/base.py +7 -3
  62. cumulusci/tasks/metadata_etl/layouts.py +1 -1
  63. cumulusci/tasks/metadata_etl/permissions.py +1 -1
  64. cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
  65. cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
  66. cumulusci/tasks/push/README.md +15 -17
  67. cumulusci/tasks/release_notes/README.md +13 -13
  68. cumulusci/tasks/release_notes/generator.py +13 -8
  69. cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
  70. cumulusci/tasks/salesforce/Deploy.py +53 -2
  71. cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
  72. cumulusci/tasks/salesforce/__init__.py +1 -0
  73. cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
  74. cumulusci/tasks/salesforce/composite.py +1 -1
  75. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  76. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  77. cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
  78. cumulusci/tasks/salesforce/insert_record.py +18 -19
  79. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  80. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  81. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  82. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  83. cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
  84. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  85. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  86. cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
  87. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +1427 -0
  88. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  89. cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
  90. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  91. cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
  92. cumulusci/tasks/salesforce/update_external_credential.py +647 -0
  93. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  94. cumulusci/tasks/salesforce/update_profile.py +17 -13
  95. cumulusci/tasks/salesforce/update_record.py +217 -0
  96. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  97. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  98. cumulusci/tasks/sfdmu/__init__.py +0 -0
  99. cumulusci/tasks/sfdmu/sfdmu.py +376 -0
  100. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  101. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  102. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  103. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  104. cumulusci/tasks/tests/test_util.py +42 -0
  105. cumulusci/tasks/util.py +37 -1
  106. cumulusci/tasks/utility/copyContents.py +402 -0
  107. cumulusci/tasks/utility/credentialManager.py +302 -0
  108. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  109. cumulusci/tasks/utility/env_management.py +1 -1
  110. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  111. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  112. cumulusci/tasks/utility/tests/test_credentialManager.py +1150 -0
  113. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  114. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1118 -0
  115. cumulusci/tests/test_integration_infrastructure.py +3 -1
  116. cumulusci/tests/test_utils.py +70 -6
  117. cumulusci/utils/__init__.py +54 -9
  118. cumulusci/utils/classutils.py +5 -2
  119. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  120. cumulusci/utils/options.py +23 -1
  121. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  122. cumulusci/utils/yaml/cumulusci_yml.py +8 -3
  123. cumulusci/utils/yaml/model_parser.py +2 -2
  124. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  125. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  126. cumulusci/vcs/base.py +23 -15
  127. cumulusci/vcs/bootstrap.py +5 -4
  128. cumulusci/vcs/utils/list_modified_files.py +189 -0
  129. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  130. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +11 -10
  131. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +135 -104
  132. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
  133. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
  134. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
  135. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,647 @@
1
+ import os
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from pydantic.v1 import root_validator
5
+
6
+ from cumulusci.core.exceptions import SalesforceDXException
7
+ from cumulusci.core.utils import determine_managed_mode
8
+ from cumulusci.salesforce_api.utils import get_simple_salesforce_connection
9
+ from cumulusci.tasks.salesforce import BaseSalesforceApiTask
10
+ from cumulusci.utils import inject_namespace
11
+ from cumulusci.utils.options import CCIOptions, Field
12
+
13
+
14
+ class ExtParameter(CCIOptions):
15
+ """Http Header options"""
16
+
17
+ name: str = Field(
18
+ None,
19
+ description="Parameter name. [default to None]",
20
+ )
21
+ value: str = Field(
22
+ None,
23
+ description="Parameter value. [default to None]",
24
+ )
25
+ group: str = Field(
26
+ None,
27
+ description="Parameter group. [default to None]",
28
+ )
29
+ sequence_number: int = Field(
30
+ None,
31
+ description="Sequence number. [default to None]",
32
+ )
33
+
34
+
35
+ class HttpHeader(ExtParameter):
36
+ """Http Header options"""
37
+
38
+ sequence_number: int = Field(
39
+ None,
40
+ description="Sequence number. [default to None]",
41
+ )
42
+ secret: bool = Field(
43
+ False,
44
+ description="Is the value a secret. [default to False]",
45
+ )
46
+
47
+
48
+ class ExternalCredential(HttpHeader):
49
+ client_secret: str = Field(
50
+ None,
51
+ description="Client secret. [default to None]",
52
+ )
53
+ client_id: str = Field(
54
+ None,
55
+ description="Client id. [default to None]",
56
+ )
57
+ auth_protocol: str = Field(
58
+ "OAuth",
59
+ description="Authentication protocol. [default to OAuth]",
60
+ )
61
+
62
+
63
+ class ExternalCredentialParameter(CCIOptions):
64
+ """External Credential Parameter options"""
65
+
66
+ auth_header: HttpHeader = Field(
67
+ None,
68
+ description="Auth header value. [default to None]",
69
+ )
70
+ auth_provider: str = Field(
71
+ None,
72
+ description="Auth provider name (only subscriber editable in 2GP). [default to None]",
73
+ )
74
+ auth_provider_url: str = Field(
75
+ None,
76
+ description="Auth provider URL. [default to None]",
77
+ )
78
+ auth_provider_url_query_parameter: ExtParameter = Field(
79
+ None,
80
+ description="Auth provider URL query parameter, The allowed AuthProviderUrlQueryParameter values are AwsExternalId and AwsDuration, used with AWS STS. [default to None]",
81
+ )
82
+ auth_parameter: ExtParameter = Field(
83
+ None,
84
+ description="Auth parameter. Allows the user to add additional authentication settings. parameterName defines the parameter to set. [default to None]",
85
+ )
86
+ # aws_sts_principal: str = Field(
87
+ # None,
88
+ # description="AWS STS Principal (only for external credentials that use AWS Signature v4 authentication with STS). [default to None]",
89
+ # )
90
+ jwt_body_claim: ExtParameter = Field(
91
+ None,
92
+ description="Specifies a JWT (JSON Web Token) body claim. [default to None]",
93
+ )
94
+ jwt_header_claim: ExtParameter = Field(
95
+ None,
96
+ description="Specifies a JWT header claim. [default to None]",
97
+ )
98
+ named_principal: ExternalCredential = Field(
99
+ None,
100
+ description="Named principal. [default to None]",
101
+ )
102
+ per_user_principal: str = Field(
103
+ None,
104
+ description="Per user principal. [default to None]",
105
+ )
106
+ signing_certificate: str = Field(
107
+ None,
108
+ description="Signing certificate (only subscriber editable in 2GP). [default to None]",
109
+ )
110
+ secret: bool = Field(
111
+ False,
112
+ description="Is the value a secret. [default to False]",
113
+ )
114
+ external_auth_identity_provider: str = Field(
115
+ None,
116
+ description="External auth identity provider name. [default to None]",
117
+ )
118
+
119
+ @root_validator
120
+ def check_parameters(cls, values):
121
+ """Check if at least one parameter is provided"""
122
+ param_fields = [
123
+ "auth_header",
124
+ "auth_provider",
125
+ "auth_provider_url",
126
+ "auth_provider_url_query_parameter",
127
+ "auth_parameter",
128
+ # "aws_sts_principal",
129
+ "jwt_body_claim",
130
+ "jwt_header_claim",
131
+ "named_principal",
132
+ "per_user_principal",
133
+ "signing_certificate",
134
+ "external_auth_identity_provider",
135
+ ]
136
+
137
+ provided_params = [
138
+ field for field in param_fields if values.get(field) is not None
139
+ ]
140
+
141
+ if len(provided_params) == 0:
142
+ raise ValueError("At least and only one parameter must be provided.")
143
+
144
+ if len(provided_params) > 1:
145
+ raise ValueError("At least and only one parameter must be provided.")
146
+
147
+ return values
148
+
149
+ def get_external_credential_parameter(self):
150
+ ext_cred_param = {"parameterGroup": "DefaultGroup"}
151
+
152
+ """Get the external credential parameter based on which field is set"""
153
+ if self.auth_header is not None:
154
+ ext_cred_param["parameterType"] = "AuthHeader"
155
+ ext_cred_param["parameterValue"] = self.auth_header.value
156
+ ext_cred_param["parameterName"] = self.auth_header.name
157
+
158
+ if self.auth_header.sequence_number is not None:
159
+ ext_cred_param["sequenceNumber"] = self.auth_header.sequence_number
160
+ if self.auth_header.secret is not None:
161
+ ext_cred_param["secret"] = self.auth_header.secret
162
+ if self.auth_header.group is not None:
163
+ ext_cred_param["parameterGroup"] = self.auth_header.group
164
+
165
+ if self.auth_provider is not None:
166
+ ext_cred_param["parameterType"] = "AuthProvider"
167
+ ext_cred_param["parameterName"] = "AuthProvider"
168
+ ext_cred_param["authProvider"] = self.auth_provider
169
+
170
+ if self.external_auth_identity_provider is not None:
171
+ ext_cred_param["parameterType"] = "ExternalAuthIdentityProvider"
172
+ ext_cred_param["parameterName"] = "ExternalAuthIdentityProvider"
173
+ ext_cred_param[
174
+ "externalAuthIdentityProvider"
175
+ ] = self.external_auth_identity_provider
176
+
177
+ if self.auth_provider_url is not None:
178
+ ext_cred_param["parameterType"] = "AuthProviderUrl"
179
+ ext_cred_param["parameterValue"] = self.auth_provider_url
180
+
181
+ if self.auth_provider_url_query_parameter is not None:
182
+ ext_cred_param["parameterType"] = "AuthProviderUrlQueryParameter"
183
+ ext_cred_param[
184
+ "parameterValue"
185
+ ] = self.auth_provider_url_query_parameter.value
186
+ ext_cred_param[
187
+ "parameterName"
188
+ ] = self.auth_provider_url_query_parameter.name
189
+
190
+ if self.auth_parameter is not None:
191
+ ext_cred_param["parameterType"] = "AuthParameter"
192
+ ext_cred_param["parameterValue"] = self.auth_parameter.value
193
+ ext_cred_param["parameterName"] = self.auth_parameter.name
194
+ if self.auth_parameter.group is not None:
195
+ ext_cred_param["parameterGroup"] = self.auth_parameter.group
196
+
197
+ # if self.aws_sts_principal is not None:
198
+ # ext_cred_param["parameterType"] = "AwsStsPrincipal"
199
+ # ext_cred_param["parameterName"] = 'AwsStsPrincipal'
200
+ # ext_cred_param["parameterValue"] = self.aws_sts_principal
201
+
202
+ if self.jwt_body_claim is not None:
203
+ ext_cred_param["parameterType"] = "JwtBodyClaim"
204
+ ext_cred_param["parameterName"] = self.jwt_body_claim.name
205
+ ext_cred_param["parameterValue"] = self.jwt_body_claim.value
206
+
207
+ if self.jwt_header_claim is not None:
208
+ ext_cred_param["parameterType"] = "JwtHeaderClaim"
209
+ ext_cred_param["parameterName"] = self.jwt_header_claim.name
210
+ ext_cred_param["parameterValue"] = self.jwt_header_claim.value
211
+
212
+ if self.named_principal is not None:
213
+ ext_cred_param["parameterType"] = "NamedPrincipal"
214
+ ext_cred_param["parameterName"] = self.named_principal.name
215
+ ext_cred_param["parameterValue"] = None
216
+ if self.named_principal.sequence_number is not None:
217
+ ext_cred_param["sequenceNumber"] = self.named_principal.sequence_number
218
+ ext_cred_param["parameterGroup"] = (
219
+ self.named_principal.group
220
+ if self.named_principal.group is not None
221
+ else self.named_principal.name
222
+ )
223
+
224
+ if self.per_user_principal is not None:
225
+ ext_cred_param["parameterType"] = "PerUserPrincipal"
226
+ ext_cred_param["parameterName"] = "PerUserPrincipal"
227
+ ext_cred_param["parameterGroup"] = "PerUser"
228
+ ext_cred_param["parameterValue"] = self.per_user_principal
229
+
230
+ if self.signing_certificate is not None:
231
+ ext_cred_param["parameterType"] = "SigningCertificate"
232
+ ext_cred_param["parameterName"] = "SigningCertificate"
233
+ ext_cred_param["certificate"] = self.signing_certificate
234
+ ext_cred_param["parameterValue"] = self.signing_certificate
235
+
236
+ return ext_cred_param
237
+
238
+ def get_principal_credential(self, ext_cred_full_name: str):
239
+ if self.named_principal is None:
240
+ return None
241
+
242
+ return {
243
+ "principalType": "NamedPrincipal",
244
+ "principalName": self.named_principal.name,
245
+ "externalCredential": ext_cred_full_name,
246
+ }
247
+
248
+ def get_credential_parameter(self):
249
+ if self.named_principal is None or (
250
+ self.named_principal.client_secret is None
251
+ and self.named_principal.client_id is None
252
+ ):
253
+ return None
254
+
255
+ return {
256
+ "clientId": {"encrypted": False, "value": self.named_principal.client_id},
257
+ "clientSecret": {
258
+ "encrypted": True,
259
+ "value": self.named_principal.client_secret,
260
+ },
261
+ }
262
+
263
+ def get_credential(self, ext_cred_full_name: str):
264
+ return {
265
+ "authenticationProtocol": self.named_principal.auth_protocol,
266
+ "credentials": self.get_credential_parameter(),
267
+ "externalCredential": ext_cred_full_name,
268
+ "principalName": self.named_principal.name,
269
+ "principalType": "NamedPrincipal",
270
+ }
271
+
272
+
273
+ class TransformExternalCredentialParameter(ExternalCredentialParameter):
274
+ """Transform External Credential Parameter with environment variable support"""
275
+
276
+ def get_external_credential_parameter(self):
277
+ ret = super().get_external_credential_parameter()
278
+ if ret.get("parameterValue", None) is not None:
279
+ ret["parameterValue"] = os.getenv(ret.get("parameterValue"))
280
+ return ret
281
+
282
+ def get_credential_parameter(self):
283
+
284
+ value = super().get_credential_parameter()
285
+
286
+ if value is None:
287
+ return None
288
+
289
+ value["clientSecret"]["value"] = os.getenv(value["clientSecret"]["value"])
290
+ value["clientId"]["value"] = os.getenv(
291
+ value["clientId"]["value"], value["clientId"]["value"]
292
+ )
293
+
294
+ return value
295
+
296
+
297
+ ExternalCredentialParameter.update_forward_refs()
298
+ TransformExternalCredentialParameter.update_forward_refs()
299
+
300
+
301
+ class UpdateExternalCredential(BaseSalesforceApiTask):
302
+ """Custom task to update external credential parameters.
303
+ This task is based on the manageability rules of external credentials in 2GP.
304
+ https://developer.salesforce.com/docs/atlas.en-us.pkg2_dev.meta/pkg2_dev/packaging_packageable_components.htm#mdc_external_credential
305
+ """
306
+
307
+ class Options(CCIOptions):
308
+ name: str = Field(..., description="Name of the external credential to update.")
309
+ namespace: str = Field(
310
+ "",
311
+ description="Namespace of the external credential to update. [default to empty string]",
312
+ )
313
+ # External credential parameters
314
+ parameters: List[ExternalCredentialParameter] = Field(
315
+ [],
316
+ description="Parameters to update. [default to empty list]",
317
+ )
318
+ # Transform parameters (from environment variables)
319
+ transform_parameters: List[TransformExternalCredentialParameter] = Field(
320
+ [],
321
+ description="Parameters to transform from environment variables. [default to empty list]",
322
+ )
323
+
324
+ parsed_options: Options
325
+
326
+ def _init_task(self):
327
+ self.tooling = get_simple_salesforce_connection(
328
+ self.project_config,
329
+ self.org_config,
330
+ api_version=self.project_config.project__package__api_version,
331
+ base_url="tooling",
332
+ )
333
+ self.connect = get_simple_salesforce_connection(
334
+ self.project_config,
335
+ self.org_config,
336
+ api_version=self.project_config.project__package__api_version,
337
+ )
338
+
339
+ def _run_task(self):
340
+ # Step 1: Get the external credential id from the external credential name
341
+ external_credential_id = self._get_external_credential_id()
342
+
343
+ if not external_credential_id:
344
+ msg = f"External credential '{self.parsed_options.name}' not found"
345
+ raise SalesforceDXException(msg)
346
+
347
+ # Step 2: Get the external credential object
348
+ external_credential = self._get_external_credential_object(
349
+ external_credential_id
350
+ )
351
+
352
+ if not external_credential:
353
+ msg = f"Failed to retrieve external credential object for '{self.parsed_options.name}'"
354
+ raise SalesforceDXException(msg)
355
+
356
+ # Step 3: Update the external credential parameters
357
+ self._update_external_credential_parameters(external_credential)
358
+
359
+ updated_external_credential = self._update_external_credential_object(
360
+ external_credential_id, external_credential
361
+ )
362
+
363
+ if not updated_external_credential:
364
+ msg = f"Failed to update external credential object for '{self.parsed_options.name}'"
365
+ raise SalesforceDXException(msg)
366
+
367
+ self._update_credential()
368
+
369
+ self.logger.info(
370
+ f"Successfully updated external credential '{self.parsed_options.name}'"
371
+ )
372
+
373
+ def _get_external_credential_id(self) -> Optional[str]:
374
+ """Get the external credential ID from the external credential name"""
375
+ query = f"SELECT Id FROM ExternalCredential WHERE DeveloperName='{self.parsed_options.name}'"
376
+
377
+ if self.parsed_options.namespace:
378
+ query += f" AND NamespacePrefix='{self.parsed_options.namespace}'"
379
+
380
+ query += " LIMIT 1"
381
+
382
+ try:
383
+ res = self.tooling.query(query)
384
+ if res["size"] == 0:
385
+ return None
386
+ return res["records"][0]["Id"]
387
+ except Exception as e:
388
+ self.logger.error(f"Error querying external credential: {str(e)}")
389
+ return None
390
+
391
+ def _get_external_credential_object(
392
+ self, external_credential_id: str
393
+ ) -> Optional[Dict[str, Any]]:
394
+ """Get the external credential object using Tooling API"""
395
+ try:
396
+ # Use Tooling API to get the external credential metadata
397
+ result = self.tooling._call_salesforce(
398
+ method="GET",
399
+ url=f"{self.tooling.base_url}sobjects/ExternalCredential/{external_credential_id}",
400
+ )
401
+
402
+ if result.status_code != 200:
403
+ self.logger.error(
404
+ f"Error retrieving external credential object: {result.json()}"
405
+ )
406
+ return None
407
+
408
+ return result.json().get("Metadata", None)
409
+
410
+ except Exception as e:
411
+ self.logger.error(f"Error retrieving external credential object: {str(e)}")
412
+ return None
413
+
414
+ def _update_external_credential_object(
415
+ self, external_credential_id: str, external_credential: Dict[str, Any]
416
+ ) -> Optional[bool]:
417
+ """Update the external credential object"""
418
+ try:
419
+ result_update = self.tooling._call_salesforce(
420
+ method="PATCH",
421
+ url=f"{self.tooling.base_url}sobjects/ExternalCredential/{external_credential_id}",
422
+ json={"Metadata": external_credential},
423
+ )
424
+ if not result_update.ok:
425
+ self.logger.error(
426
+ f"Error updating external credential object: {result_update.json()}"
427
+ )
428
+ return None
429
+
430
+ return result_update.ok
431
+ except Exception as e:
432
+ raise SalesforceDXException(
433
+ f"Failed to update external credential object: {str(e)}"
434
+ )
435
+
436
+ def _update_external_credential_parameters(
437
+ self, external_credential: Dict[str, Any]
438
+ ):
439
+ """Update the external credential parameters"""
440
+ try:
441
+ # Get template parameter for new parameters
442
+ template_param = self._get_external_credential_template_parameter()
443
+
444
+ # Update regular parameters
445
+ self._update_parameters(
446
+ external_credential, self.parsed_options.parameters, template_param
447
+ )
448
+
449
+ # Update transform parameters (from environment variables)
450
+ self._update_parameters(
451
+ external_credential,
452
+ self.parsed_options.transform_parameters,
453
+ template_param,
454
+ )
455
+
456
+ except Exception as e:
457
+ raise SalesforceDXException(f"Failed to update parameters: {str(e)}")
458
+
459
+ def _update_parameters(
460
+ self,
461
+ external_credential: Dict[str, Any],
462
+ external_credential_parameters: List[ExternalCredentialParameter],
463
+ template_param: Dict[str, Any],
464
+ ):
465
+ """Update the parameters"""
466
+ for param_input in external_credential_parameters:
467
+
468
+ if param_input.external_auth_identity_provider is not None:
469
+ param_input.external_auth_identity_provider = self._inject_namespace(
470
+ param_input.external_auth_identity_provider
471
+ )
472
+ if param_input.auth_provider is not None:
473
+ param_input.auth_provider = self._inject_namespace(
474
+ param_input.auth_provider
475
+ )
476
+
477
+ param_to_update = param_input.get_external_credential_parameter()
478
+ secret = param_to_update.pop("secret", False)
479
+
480
+ # Create a copy for matching (without parameterValue)
481
+ param_to_match = {
482
+ k: v for k, v in param_to_update.items() if k != "parameterValue"
483
+ }
484
+
485
+ if param_to_update.get("parameterType") == "ExternalAuthIdentityProvider":
486
+ self._remove_conflicting_parameters(
487
+ external_credential, "ExternalAuthIdentityProvider", log=False
488
+ )
489
+ elif param_to_update.get("parameterType") == "AuthProvider":
490
+ self._remove_conflicting_parameters(
491
+ external_credential, "AuthProvider", log=False
492
+ )
493
+
494
+ # Find existing parameter
495
+ cred_param = next(
496
+ (
497
+ param
498
+ for param in external_credential.get(
499
+ "externalCredentialParameters", []
500
+ )
501
+ if param_to_match.items() <= param.items()
502
+ ),
503
+ None,
504
+ )
505
+
506
+ if cred_param:
507
+ # Update existing parameter
508
+ cred_param.update(param_to_update)
509
+ self.logger.info(
510
+ f"Updated parameter {cred_param['parameterType']}"
511
+ + (
512
+ f"-{cred_param.get('parameterName', 'N/A')}"
513
+ if cred_param.get("parameterName")
514
+ else ""
515
+ )
516
+ + f" with new value {param_to_update.get('parameterValue', '') if not secret else '********'}"
517
+ )
518
+ else:
519
+ # Add new parameter
520
+ copy_template_param = template_param.copy()
521
+ copy_template_param.update(param_to_update)
522
+ if "externalCredentialParameters" not in external_credential:
523
+ external_credential["externalCredentialParameters"] = []
524
+ external_credential["externalCredentialParameters"].append(
525
+ copy_template_param
526
+ )
527
+ self.logger.info(
528
+ f"Added parameter {copy_template_param['parameterType']}"
529
+ + (
530
+ f"-{copy_template_param.get('parameterName', 'N/A')}"
531
+ if copy_template_param.get("parameterName")
532
+ else ""
533
+ )
534
+ + f" with new value {param_to_update.get('parameterValue', '') if not secret else '********'}"
535
+ )
536
+
537
+ # Enforce mutual exclusivity between AuthProvider and ExternalAuthIdentityProvider
538
+ # If auth_provider is provided, remove any ExternalAuthIdentityProvider parameters
539
+ # If external_auth_identity_provider is provided, remove any AuthProvider parameters
540
+
541
+ if param_to_update.get("parameterType") == "AuthProvider":
542
+ self._remove_conflicting_parameters(
543
+ external_credential, "ExternalAuthIdentityProvider"
544
+ )
545
+ elif param_to_update.get("parameterType") == "ExternalAuthIdentityProvider":
546
+ self._remove_conflicting_parameters(external_credential, "AuthProvider")
547
+
548
+ def _remove_conflicting_parameters(
549
+ self, external_credential: Dict[str, Any], parameter_type: str, log: bool = True
550
+ ):
551
+ """Remove conflicting parameter types from external credential.
552
+
553
+ Args:
554
+ external_credential: The external credential object
555
+ parameter_type: The parameter type to remove (e.g., 'AuthProvider' or 'ExternalAuthIdentityProvider')
556
+ """
557
+ if "externalCredentialParameters" not in external_credential:
558
+ return
559
+
560
+ original_count = len(external_credential["externalCredentialParameters"])
561
+
562
+ # Filter out parameters with the conflicting type
563
+ external_credential["externalCredentialParameters"] = [
564
+ param
565
+ for param in external_credential["externalCredentialParameters"]
566
+ if param.get("parameterType") != parameter_type
567
+ ]
568
+
569
+ removed_count = original_count - len(
570
+ external_credential["externalCredentialParameters"]
571
+ )
572
+
573
+ if removed_count > 0 and log:
574
+ self.logger.info(
575
+ f"Removed {removed_count} conflicting parameter(s) of type '{parameter_type}'"
576
+ )
577
+
578
+ def _get_external_credential_template_parameter(self) -> Dict[str, Any]:
579
+ """Get the external credential template parameter"""
580
+ return {
581
+ "authProvider": None,
582
+ "certificate": None,
583
+ "description": None,
584
+ "externalAuthIdentityProvider": None,
585
+ "parameterGroup": None,
586
+ "parameterName": None,
587
+ "parameterType": None,
588
+ "parameterValue": None,
589
+ "sequenceNumber": None,
590
+ }
591
+
592
+ def _update_credential(self):
593
+ """Update the credential"""
594
+ for param in (
595
+ self.parsed_options.parameters + self.parsed_options.transform_parameters
596
+ ):
597
+ if param.named_principal is None or (
598
+ param.named_principal.client_secret is None
599
+ and param.named_principal.client_id is None
600
+ ):
601
+ continue
602
+
603
+ namespace = (
604
+ f"{self.parsed_options.namespace}__"
605
+ if self.parsed_options.namespace
606
+ else ""
607
+ )
608
+ credential_param = param.get_principal_credential(
609
+ f"{namespace}{self.parsed_options.name}"
610
+ )
611
+
612
+ self.logger.info(f"Managing credential for {param.named_principal.name}...")
613
+
614
+ credential_response = self.connect._call_salesforce(
615
+ method="GET",
616
+ url=f"{self.connect.base_url}named-credentials/credential",
617
+ params=credential_param,
618
+ )
619
+
620
+ credential = credential_response.json()
621
+ credential.pop("authenticationStatus")
622
+ http_verb = "PUT" if credential["credentials"] else "POST"
623
+ credential["credentials"] = param.get_credential_parameter()
624
+
625
+ response = self.connect._call_salesforce(
626
+ method=http_verb,
627
+ url=f"{self.connect.base_url}named-credentials/credential",
628
+ json=credential,
629
+ )
630
+
631
+ if not response.ok:
632
+ msg = f"Failed to update credential {param.named_principal.name}: {response.json()}"
633
+ raise SalesforceDXException(msg)
634
+
635
+ self.logger.info(f"Updated credential {param.named_principal.name}")
636
+
637
+ def _inject_namespace(self, value: str) -> str:
638
+ _, value = inject_namespace(
639
+ "",
640
+ value,
641
+ namespace=self.parsed_options.namespace,
642
+ managed=determine_managed_mode(
643
+ self.options, self.project_config, self.org_config
644
+ ),
645
+ namespaced_org=self.org_config.namespaced or False,
646
+ )
647
+ return value