Skip to content

Commit 8a27ee0

Browse files
jjdauroraclaude
andcommitted
Address self-review findings on AWS IAM auth strategy
- Add raise after _handle_credential_error to fix implicit None return - Add logger.warning on silent us-east-1 region fallback - Add logger.debug for partial cached credential detection - Add comment explaining lazy boto3 import - Fix em-dash in NoCredentialsError user message - Fix fragile test assertion to match current error message wording - Remove dead required_fields variable from test_missing_fields - Add test_optional_param_not_required for optional ConnectorParam path - Remove unused ExternalDatasetReference import from test file Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b9b99b3 commit 8a27ee0

5 files changed

Lines changed: 40 additions & 35 deletions

File tree

src/fides/api/schemas/connection_configuration/connection_secrets_saas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def get_saas_schema(self) -> Type[SaaSSchema]:
162162
json_schema_extra=extra,
163163
),
164164
)
165-
if connector_param.default_value
165+
if connector_param.default_value or connector_param.optional
166166
else (
167167
param_type,
168168
FieldInfo(

src/fides/api/schemas/saas/saas_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ class ConnectorParam(BaseModel):
334334
multiselect: Optional[bool] = False
335335
description: Optional[str] = None
336336
sensitive: Optional[bool] = False
337+
optional: bool = False
337338
type: Optional[str] = None
338339
allowed_values: Optional[List[str]] = None
339340
# type="endpoint" marks this param as a URL endpoint/domain param.

src/fides/api/service/authentication/authentication_strategy_aws_iam.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ def _get_assumed_role_credentials(
9999
if cached_key and cached_secret and cached_token and cached_expiry:
100100
if not self._is_close_to_expiration(cached_expiry):
101101
return Credentials(cached_key, cached_secret, cached_token)
102+
elif any([cached_key, cached_secret, cached_token, cached_expiry]):
103+
logger.debug(
104+
"Partial cached credentials found for {}; refreshing.",
105+
connection_config.key,
106+
)
102107

103108
return self._refresh_assumed_role_credentials(secrets, connection_config)
104109

@@ -107,6 +112,7 @@ def _refresh_assumed_role_credentials(
107112
secrets: Dict[str, Any],
108113
connection_config: ConnectionConfig,
109114
) -> Credentials:
115+
# Imported lazily to avoid a hard dependency on boto3 at module load time.
110116
import boto3
111117

112118
assume_role_arn = secrets["aws_assume_role_arn"]
@@ -155,6 +161,7 @@ def _refresh_assumed_role_credentials(
155161

156162
except (ClientError, NoCredentialsError) as exc:
157163
self._handle_credential_error(exc, connection_config)
164+
raise # unreachable; _handle_credential_error is NoReturn
158165

159166
def _resolve_region(
160167
self, url: Optional[str], connection_config: ConnectionConfig
@@ -174,6 +181,12 @@ def _resolve_region(
174181
if len(parts) >= 4 and parts[-2] == "amazonaws" and parts[-1] == "com":
175182
return parts[-3]
176183

184+
logger.warning(
185+
"Could not infer AWS region from URL or secrets for connector {}; "
186+
"defaulting to us-east-1. Set aws_region in the connector secrets or "
187+
"authentication configuration to avoid this.",
188+
connection_config.key,
189+
)
177190
return "us-east-1"
178191

179192
def _is_close_to_expiration(self, expires_at: int) -> bool:
@@ -224,9 +237,12 @@ def _handle_credential_error(
224237

225238
if isinstance(exc, NoCredentialsError):
226239
user_message = (
227-
"No AWS credentials found. Provide either aws_access_key_id "
228-
"and aws_secret_access_key, or ensure the Fides environment "
229-
"has AWS credentials configured (e.g. via instance profile)."
240+
"No base AWS credentials found to authenticate the STS AssumeRole call. "
241+
"Providing an IAM Role ARN alone is not sufficient. AWS requires credentials "
242+
"to call sts:AssumeRole. Either provide aws_access_key_id and "
243+
"aws_secret_access_key alongside the ARN, or ensure the Fides environment "
244+
"has ambient AWS credentials configured (e.g. instance profile, environment "
245+
"variables, or ~/.aws/credentials)."
230246
)
231247
elif isinstance(exc, ClientError):
232248
error_code = exc.response.get("Error", {}).get("Code", "")

tests/fides/ops/schemas/connection_configuration/test_connection_secrets_saas.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
)
1111
from fides.api.schemas.saas.saas_config import (
1212
ConnectorParam,
13-
ExternalDatasetReference,
1413
SaaSConfig,
1514
)
1615
from fides.config.security_settings import DomainValidationMode
@@ -44,23 +43,24 @@ def test_missing_fields(self, saas_config: SaaSConfig):
4443
with pytest.raises(ValidationError) as exc:
4544
schema.model_validate(config)
4645

47-
required_fields = [
48-
connector_param.name
49-
for connector_param in (
50-
saas_config.connector_params + saas_config.external_references
51-
)
52-
if isinstance(
53-
connector_param, ExternalDatasetReference
54-
) # external refs are required
55-
or not connector_param.default_value
56-
]
57-
5846
errors = exc._excinfo[1].errors()
5947
assert (
6048
errors[0]["msg"]
6149
== "Value error, custom_schema must be supplied all of: [username, api_key, api_version, page_size, account_types, customer_id]."
6250
)
6351

52+
def test_optional_param_not_required(self, saas_config: SaaSConfig):
53+
saas_config.connector_params = [
54+
ConnectorParam(name="required_key"),
55+
ConnectorParam(name="optional_key", optional=True),
56+
]
57+
saas_config.external_references = []
58+
schema = SaaSSchemaFactory(saas_config).get_saas_schema()
59+
# optional_key absent — should not raise
60+
schema.model_validate({"required_key": "value"})
61+
# optional_key present — should also work
62+
schema.model_validate({"required_key": "value", "optional_key": "val"})
63+
6464
def test_extra_fields(
6565
self, saas_config: SaaSConfig, saas_example_secrets: Dict[str, Any]
6666
):

tests/fides/ops/service/authentication/test_authentication_strategy_aws_iam.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ def test_default_service(self):
5959
assert strategy.service == "execute-api"
6060

6161
def test_custom_service(self):
62-
strategy = AuthenticationStrategy.get_strategy(
63-
"aws_iam", {"service": "lambda"}
64-
)
62+
strategy = AuthenticationStrategy.get_strategy("aws_iam", {"service": "lambda"})
6563
assert strategy.service == "lambda"
6664

6765
def test_custom_region(self):
@@ -142,9 +140,7 @@ def test_static_keys_with_session_token(self):
142140

143141
class TestAssumeRole:
144142
@patch("fides.api.service.authentication.authentication_strategy_aws_iam.boto3")
145-
def test_assume_role_signs_request(
146-
self, mock_boto3, assume_role_connection_config
147-
):
143+
def test_assume_role_signs_request(self, mock_boto3, assume_role_connection_config):
148144
future_expiry = datetime.now(timezone.utc) + timedelta(hours=1)
149145
mock_sts = MagicMock()
150146
mock_sts.assume_role.return_value = {
@@ -253,7 +249,7 @@ def test_assume_role_no_credentials(
253249

254250
with pytest.raises(FidesopsException) as exc:
255251
strategy.add_authentication(req, assume_role_connection_config)
256-
assert "No AWS credentials found" in str(exc.value)
252+
assert "No base AWS credentials found" in str(exc.value)
257253

258254

259255
class TestCredentialCaching:
@@ -291,9 +287,7 @@ def test_refreshes_credentials_when_close_to_expiration(
291287
):
292288
from botocore.credentials import Credentials
293289

294-
mock_refresh.return_value = Credentials(
295-
"ASIA_NEW", "new_secret", "new_token"
296-
)
290+
mock_refresh.return_value = Credentials("ASIA_NEW", "new_secret", "new_token")
297291

298292
close_expiry = int(
299293
(
@@ -325,9 +319,7 @@ def test_refreshes_credentials_when_close_to_expiration(
325319

326320
class TestRegionResolution:
327321
def test_region_from_configuration(self, static_key_secrets):
328-
connection_config = ConnectionConfig(
329-
key="test", secrets=static_key_secrets
330-
)
322+
connection_config = ConnectionConfig(key="test", secrets=static_key_secrets)
331323
req = Request(
332324
method="GET",
333325
url="https://abc123.execute-api.us-east-1.amazonaws.com/prod/users",
@@ -342,9 +334,7 @@ def test_region_from_configuration(self, static_key_secrets):
342334
assert "ap-southeast-1" in auth_header
343335

344336
def test_region_from_secrets(self, static_key_secrets):
345-
connection_config = ConnectionConfig(
346-
key="test", secrets=static_key_secrets
347-
)
337+
connection_config = ConnectionConfig(key="test", secrets=static_key_secrets)
348338
req = Request(
349339
method="GET",
350340
url="https://custom-domain.example.com/users",
@@ -447,9 +437,7 @@ def test_custom_service_name(self, static_key_connection_config):
447437
url="https://abc123.execute-api.us-west-2.amazonaws.com/prod/users",
448438
).prepare()
449439

450-
strategy = AuthenticationStrategy.get_strategy(
451-
"aws_iam", {"service": "lambda"}
452-
)
440+
strategy = AuthenticationStrategy.get_strategy("aws_iam", {"service": "lambda"})
453441
authenticated_request = strategy.add_authentication(
454442
req, static_key_connection_config
455443
)

0 commit comments

Comments
 (0)