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,551 @@
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.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 ExtParameter(CCIOptions):
13
+ """External Auth Identity Provider Parameter options"""
14
+
15
+ name: str = Field(
16
+ None,
17
+ description="Parameter name. [default to None]",
18
+ )
19
+ value: str = Field(
20
+ None,
21
+ description="Parameter value. [default to None]",
22
+ )
23
+ sequence_number: int = Field(
24
+ None,
25
+ description="Sequence number. [default to None]",
26
+ )
27
+
28
+
29
+ class ExternalAuthIdentityProviderCredential(CCIOptions):
30
+ """External Auth Identity Provider Credential options"""
31
+
32
+ name: str = Field(
33
+ None,
34
+ description="Credential name. [default to None]",
35
+ )
36
+ client_id: str = Field(
37
+ None,
38
+ description="Client ID. [default to None]",
39
+ )
40
+ client_secret: str = Field(
41
+ None,
42
+ description="Client secret. [default to None]",
43
+ )
44
+ auth_protocol: str = Field(
45
+ "OAuth",
46
+ description="Authentication protocol. [default to OAuth]",
47
+ )
48
+
49
+
50
+ class ExternalAuthIdentityProviderParameter(CCIOptions):
51
+ """External Auth Identity Provider Parameter options"""
52
+
53
+ authorize_url: str = Field(
54
+ None,
55
+ description="Authorize URL. [default to None]",
56
+ )
57
+ token_url: str = Field(
58
+ None,
59
+ description="Token URL. [default to None]",
60
+ )
61
+ user_info_url: str = Field(
62
+ None,
63
+ description="User info URL. [default to None]",
64
+ )
65
+ jwks_url: str = Field(
66
+ None,
67
+ description="JWKS URL (for OpenID Connect). [default to None]",
68
+ )
69
+ issuer_url: str = Field(
70
+ None,
71
+ description="Issuer URL (for OpenID Connect). [default to None]",
72
+ )
73
+ client_authentication: str = Field(
74
+ None,
75
+ description="Client authentication method (e.g., ClientSecretBasic, ClientSecretPost, ClientSecretJwt). [default to None]",
76
+ )
77
+ custom_parameter: ExtParameter = Field(
78
+ None,
79
+ description="Custom parameter. [default to None]",
80
+ )
81
+ identity_provider_option: ExtParameter = Field(
82
+ None,
83
+ description="Identity provider option (e.g., PkceEnabled, UserinfoEnabled). [default to None]",
84
+ )
85
+ credential: ExternalAuthIdentityProviderCredential = Field(
86
+ None,
87
+ description="Credential to update. [default to None]",
88
+ )
89
+ secret: bool = Field(
90
+ False,
91
+ description="Is the value a secret. [default to False]",
92
+ )
93
+
94
+ @root_validator
95
+ def check_parameters(cls, values):
96
+ """Check if at least one parameter is provided"""
97
+ param_fields = [
98
+ "authorize_url",
99
+ "token_url",
100
+ "user_info_url",
101
+ "jwks_url",
102
+ "issuer_url",
103
+ "client_authentication",
104
+ "custom_parameter",
105
+ "identity_provider_option",
106
+ "credential",
107
+ ]
108
+
109
+ provided_params = [
110
+ field for field in param_fields if values.get(field) is not None
111
+ ]
112
+
113
+ if len(provided_params) == 0:
114
+ raise ValueError("At least and only one parameter must be provided.")
115
+
116
+ if len(provided_params) > 1:
117
+ raise ValueError("At least and only one parameter must be provided.")
118
+
119
+ return values
120
+
121
+ def get_external_auth_identity_provider_parameter(self):
122
+ """Get the external auth identity provider parameter based on which field is set"""
123
+ ext_auth_param = {}
124
+
125
+ if self.authorize_url is not None:
126
+ ext_auth_param["parameterType"] = "AuthorizeUrl"
127
+ ext_auth_param["parameterName"] = "AuthorizeUrl"
128
+ ext_auth_param["parameterValue"] = self.authorize_url
129
+
130
+ if self.token_url is not None:
131
+ ext_auth_param["parameterType"] = "TokenUrl"
132
+ ext_auth_param["parameterName"] = "TokenUrl"
133
+ ext_auth_param["parameterValue"] = self.token_url
134
+
135
+ if self.user_info_url is not None:
136
+ ext_auth_param["parameterType"] = "UserInfoUrl"
137
+ ext_auth_param["parameterName"] = "UserInfoUrl"
138
+ ext_auth_param["parameterValue"] = self.user_info_url
139
+
140
+ if self.jwks_url is not None:
141
+ ext_auth_param["parameterType"] = "JwksUrl"
142
+ ext_auth_param["parameterName"] = "JwksUrl"
143
+ ext_auth_param["parameterValue"] = self.jwks_url
144
+
145
+ if self.issuer_url is not None:
146
+ ext_auth_param["parameterType"] = "IssuerUrl"
147
+ ext_auth_param["parameterName"] = "IssuerUrl"
148
+ ext_auth_param["parameterValue"] = self.issuer_url
149
+
150
+ if self.client_authentication is not None:
151
+ ext_auth_param["parameterType"] = "ClientAuthentication"
152
+ ext_auth_param["parameterName"] = "ClientAuthentication"
153
+ ext_auth_param["parameterValue"] = self.client_authentication
154
+
155
+ if self.custom_parameter is not None:
156
+ ext_auth_param["parameterType"] = "CustomParameter"
157
+ ext_auth_param["parameterName"] = self.custom_parameter.name
158
+ ext_auth_param["parameterValue"] = self.custom_parameter.value
159
+ if self.custom_parameter.sequence_number is not None:
160
+ ext_auth_param["sequenceNumber"] = self.custom_parameter.sequence_number
161
+
162
+ if self.identity_provider_option is not None:
163
+ ext_auth_param["parameterType"] = "IdentityProviderOptions"
164
+ ext_auth_param["parameterName"] = self.identity_provider_option.name
165
+ ext_auth_param["parameterValue"] = self.identity_provider_option.value
166
+
167
+ return ext_auth_param
168
+
169
+ def get_credential(self, ext_auth_identity_provider_full_name: str):
170
+ """Get the credential to update"""
171
+ if self.credential is None:
172
+ return None
173
+
174
+ return {
175
+ "credentials": [
176
+ {
177
+ "credentialName": "clientId",
178
+ "credentialValue": self.credential.client_id,
179
+ },
180
+ {
181
+ "credentialName": "clientSecret",
182
+ "credentialValue": self.credential.client_secret,
183
+ },
184
+ ]
185
+ }
186
+
187
+
188
+ class TransformExternalAuthIdentityProviderParameter(
189
+ ExternalAuthIdentityProviderParameter
190
+ ):
191
+ """Transform External Auth Identity Provider Parameter with environment variable support"""
192
+
193
+ def get_external_auth_identity_provider_parameter(self):
194
+ ret = super().get_external_auth_identity_provider_parameter()
195
+ if ret.get("parameterValue", None) is not None:
196
+ ret["parameterValue"] = os.getenv(
197
+ ret.get("parameterValue"), ret.get("parameterValue")
198
+ )
199
+ return ret
200
+
201
+ def get_credential(self, ext_auth_identity_provider_full_name: str):
202
+ """Get the credential with values from environment variables"""
203
+ value = super().get_credential(ext_auth_identity_provider_full_name)
204
+
205
+ if value is None:
206
+ return None
207
+
208
+ for credential in value["credentials"]:
209
+ if credential["credentialValue"]:
210
+ credential["credentialValue"] = os.getenv(
211
+ credential["credentialValue"], credential["credentialValue"]
212
+ )
213
+
214
+ return value
215
+
216
+
217
+ ExternalAuthIdentityProviderParameter.update_forward_refs()
218
+ TransformExternalAuthIdentityProviderParameter.update_forward_refs()
219
+
220
+
221
+ class UpdateExternalAuthIdentityProvider(BaseSalesforceApiTask):
222
+ """Custom task to update external auth identity provider parameters.
223
+ This task updates External Auth Identity Provider parameters and credentials using
224
+ the Tooling API and Connect API.
225
+
226
+ Reference:
227
+ - https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_externalauthidentityprovider.htm
228
+ - https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/connect_resources_named_credentials_external_auth_identity_provider_credentials.htm
229
+ """
230
+
231
+ class Options(CCIOptions):
232
+ name: str = Field(
233
+ ..., description="Name of the external auth identity provider to update."
234
+ )
235
+ namespace: str = Field(
236
+ "",
237
+ description="Namespace of the external auth identity provider to update. [default to empty string]",
238
+ )
239
+ # External auth identity provider parameters
240
+ parameters: List[ExternalAuthIdentityProviderParameter] = Field(
241
+ [],
242
+ description="Parameters to update. [default to empty list]",
243
+ )
244
+ # Transform parameters (from environment variables)
245
+ transform_parameters: List[
246
+ TransformExternalAuthIdentityProviderParameter
247
+ ] = Field(
248
+ [],
249
+ description="Parameters to transform from environment variables. [default to empty list]",
250
+ )
251
+
252
+ parsed_options: Options
253
+
254
+ def _init_task(self):
255
+ self.tooling = get_simple_salesforce_connection(
256
+ self.project_config,
257
+ self.org_config,
258
+ api_version=self.project_config.project__package__api_version,
259
+ base_url="tooling",
260
+ )
261
+ self.connect = get_simple_salesforce_connection(
262
+ self.project_config,
263
+ self.org_config,
264
+ api_version=self.project_config.project__package__api_version,
265
+ )
266
+
267
+ def _run_task(self):
268
+ # Step 1: Get the external auth identity provider id from the name
269
+ ext_auth_id = self._get_external_auth_identity_provider_id()
270
+
271
+ if not ext_auth_id:
272
+ msg = f"External auth identity provider '{self.parsed_options.name}' not found"
273
+ raise SalesforceDXException(msg)
274
+
275
+ # Step 2: Get the external auth identity provider object
276
+ ext_auth_provider = self._get_external_auth_identity_provider_object(
277
+ ext_auth_id
278
+ )
279
+
280
+ if not ext_auth_provider:
281
+ msg = f"Failed to retrieve external auth identity provider object for '{self.parsed_options.name}'"
282
+ raise SalesforceDXException(msg)
283
+
284
+ # Step 3: Update the external auth identity provider parameters
285
+ self._update_external_auth_identity_provider_parameters(ext_auth_provider)
286
+
287
+ updated_ext_auth_provider = self._update_external_auth_identity_provider_object(
288
+ ext_auth_id, ext_auth_provider
289
+ )
290
+
291
+ if not updated_ext_auth_provider:
292
+ msg = f"Failed to update external auth identity provider object for '{self.parsed_options.name}'"
293
+ raise SalesforceDXException(msg)
294
+
295
+ # Step 4: Update credentials if specified
296
+ response = self._update_credential()
297
+
298
+ if not response:
299
+ raise SalesforceDXException(
300
+ f"Failed to update credentials for external auth identity provider '{self.parsed_options.name}'"
301
+ )
302
+
303
+ self.logger.info(
304
+ f"Successfully updated external auth identity provider '{self.parsed_options.name}'"
305
+ )
306
+
307
+ def _get_external_auth_identity_provider_id(self) -> Optional[str]:
308
+ """Get the external auth identity provider ID from the name"""
309
+ query = f"SELECT Id FROM ExternalAuthIdentityProvider WHERE DeveloperName='{self.parsed_options.name}'"
310
+
311
+ if self.parsed_options.namespace:
312
+ query += f" AND NamespacePrefix='{self.parsed_options.namespace}'"
313
+
314
+ query += " LIMIT 1"
315
+
316
+ try:
317
+ res = self.tooling.query(query)
318
+ if res["size"] == 0:
319
+ return None
320
+ return res["records"][0]["Id"]
321
+ except Exception as e:
322
+ self.logger.error(
323
+ f"Error querying external auth identity provider: {str(e)}"
324
+ )
325
+ return None
326
+
327
+ def _get_external_auth_identity_provider_object(
328
+ self, ext_auth_id: str
329
+ ) -> Optional[Dict[str, Any]]:
330
+ """Get the external auth identity provider object using Tooling API"""
331
+ try:
332
+ # Use Tooling API to get the external auth identity provider metadata
333
+ result = self.tooling._call_salesforce(
334
+ method="GET",
335
+ url=f"{self.tooling.base_url}sobjects/ExternalAuthIdentityProvider/{ext_auth_id}",
336
+ )
337
+
338
+ if result.status_code != 200:
339
+ self.logger.error(
340
+ f"Error retrieving external auth identity provider object: {result.json()}"
341
+ )
342
+ return None
343
+
344
+ return result.json().get("Metadata", None)
345
+
346
+ except Exception as e:
347
+ self.logger.error(
348
+ f"Error retrieving external auth identity provider object: {str(e)}"
349
+ )
350
+ return None
351
+
352
+ def _update_external_auth_identity_provider_object(
353
+ self, ext_auth_id: str, ext_auth_provider: Dict[str, Any]
354
+ ) -> Optional[bool]:
355
+ """Update the external auth identity provider object"""
356
+ try:
357
+ result_update = self.tooling._call_salesforce(
358
+ method="PATCH",
359
+ url=f"{self.tooling.base_url}sobjects/ExternalAuthIdentityProvider/{ext_auth_id}",
360
+ json={"Metadata": ext_auth_provider},
361
+ )
362
+ if not result_update.ok:
363
+ self.logger.error(
364
+ f"Error updating external auth identity provider object: {result_update.json()}"
365
+ )
366
+ return None
367
+
368
+ return result_update.ok
369
+ except Exception as e:
370
+ self.logger.error(
371
+ f"Error updating external auth identity provider object: {str(e)}"
372
+ )
373
+ return None
374
+
375
+ def _update_external_auth_identity_provider_parameters(
376
+ self, ext_auth_provider: Dict[str, Any]
377
+ ):
378
+ """Update the external auth identity provider parameters"""
379
+ try:
380
+ # Get template parameter for new parameters
381
+ template_param = (
382
+ self._get_external_auth_identity_provider_template_parameter()
383
+ )
384
+
385
+ # Update regular parameters
386
+ self._update_parameters(
387
+ ext_auth_provider, self.parsed_options.parameters, template_param
388
+ )
389
+
390
+ # Update transform parameters (from environment variables)
391
+ self._update_parameters(
392
+ ext_auth_provider,
393
+ self.parsed_options.transform_parameters,
394
+ template_param,
395
+ )
396
+
397
+ except Exception as e:
398
+ raise SalesforceDXException(f"Failed to update parameters: {str(e)}")
399
+
400
+ def _update_parameters(
401
+ self,
402
+ ext_auth_provider: Dict[str, Any],
403
+ ext_auth_parameters: List[ExternalAuthIdentityProviderParameter],
404
+ template_param: Dict[str, Any],
405
+ ):
406
+ """Update the parameters"""
407
+ for param_input in ext_auth_parameters:
408
+ # Skip credential-only updates
409
+ if param_input.credential is not None and all(
410
+ getattr(param_input, field) is None
411
+ for field in [
412
+ "authorize_url",
413
+ "token_url",
414
+ "user_info_url",
415
+ "jwks_url",
416
+ "issuer_url",
417
+ "client_authentication",
418
+ "custom_parameter",
419
+ "identity_provider_option",
420
+ ]
421
+ ):
422
+ continue
423
+
424
+ param_to_update = (
425
+ param_input.get_external_auth_identity_provider_parameter()
426
+ )
427
+ secret = (
428
+ param_to_update.pop("secret", False)
429
+ if "secret" in param_to_update
430
+ else False
431
+ )
432
+
433
+ # Create a copy for matching (without parameterValue and parameterName)
434
+ param_to_match = {
435
+ k: v
436
+ for k, v in param_to_update.items()
437
+ if k != "parameterValue" and k != "parameterName"
438
+ }
439
+
440
+ # Find existing parameter
441
+ auth_param = next(
442
+ (
443
+ param
444
+ for param in ext_auth_provider.get(
445
+ "externalAuthIdentityProviderParameters", []
446
+ )
447
+ if param_to_match.items() <= param.items()
448
+ ),
449
+ None,
450
+ )
451
+
452
+ if auth_param:
453
+ # Update existing parameter
454
+ auth_param.update(param_to_update)
455
+ self.logger.info(
456
+ f"Updated parameter {auth_param['parameterType']}"
457
+ + (
458
+ f"-{auth_param.get('parameterName', 'N/A')}"
459
+ if auth_param.get("parameterName")
460
+ else ""
461
+ )
462
+ + f" with new value {param_to_update['parameterValue'] if not secret else '********'}"
463
+ )
464
+ else:
465
+ # Add new parameter
466
+ copy_template_param = template_param.copy()
467
+ copy_template_param.update(param_to_update)
468
+ if "externalAuthIdentityProviderParameters" not in ext_auth_provider:
469
+ ext_auth_provider["externalAuthIdentityProviderParameters"] = []
470
+ ext_auth_provider["externalAuthIdentityProviderParameters"].append(
471
+ copy_template_param
472
+ )
473
+ self.logger.info(
474
+ f"Added parameter {copy_template_param['parameterType']}"
475
+ + (
476
+ f"-{copy_template_param.get('parameterName', 'N/A')}"
477
+ if copy_template_param.get("parameterName")
478
+ else ""
479
+ )
480
+ + f" with new value {param_to_update['parameterValue'] if not secret else '********'}"
481
+ )
482
+
483
+ def _get_external_auth_identity_provider_template_parameter(
484
+ self,
485
+ ) -> Dict[str, Any]:
486
+ """Get the external auth identity provider template parameter"""
487
+ return {
488
+ "description": None,
489
+ "parameterName": None,
490
+ "parameterType": None,
491
+ "parameterValue": None,
492
+ "sequenceNumber": None,
493
+ }
494
+
495
+ def _update_credential(self):
496
+ """Update the credential using Connect API"""
497
+ for param in (
498
+ self.parsed_options.parameters + self.parsed_options.transform_parameters
499
+ ):
500
+ if param.credential is None or (
501
+ param.credential.client_secret is None
502
+ and param.credential.client_id is None
503
+ ):
504
+ continue
505
+
506
+ namespace = (
507
+ f"{self.parsed_options.namespace}__"
508
+ if self.parsed_options.namespace
509
+ else ""
510
+ )
511
+ ext_auth_full_name = f"{namespace}{self.parsed_options.name}"
512
+
513
+ self.logger.info(
514
+ f"Managing credential for external auth identity provider {self.parsed_options.name}..."
515
+ )
516
+
517
+ # Get current credential
518
+ credential_response = self.connect._call_salesforce(
519
+ method="GET",
520
+ url=f"{self.connect.base_url}named-credentials/external-auth-identity-provider-credentials/{ext_auth_full_name}",
521
+ )
522
+
523
+ if not credential_response.ok:
524
+ msg = f"Failed to retrieve credential for {self.parsed_options.name}: {credential_response.json()}"
525
+ raise SalesforceDXException(msg)
526
+
527
+ credential = credential_response.json()
528
+ http_verb = "PUT" if credential.get("credentials") else "POST"
529
+
530
+ # Update credential data
531
+ credential_data = param.get_credential(ext_auth_full_name)
532
+ credential["credentials"] = credential_data["credentials"]
533
+
534
+ # Update credential via Connect API
535
+ try:
536
+ response = self.connect._call_salesforce(
537
+ method=http_verb,
538
+ url=f"{self.connect.base_url}named-credentials/external-auth-identity-provider-credentials/{ext_auth_full_name}",
539
+ json=credential,
540
+ )
541
+
542
+ return response.ok
543
+
544
+ except Exception as e:
545
+ self.logger.error(
546
+ f"Error updating credential for {self.parsed_options.name}: {str(e)}"
547
+ )
548
+ return False
549
+
550
+ # Return True if no credentials to update or all updates succeeded
551
+ return True