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