Skip to content

Commit 27e1d91

Browse files
committed
Changed error logging for Lake Formation/IAM permissions issues due to
api structure. Changed testing to match. --- X-AI-Prompt: Refactor Lake Formation error handling in FeatureGroupManager to remove _has_lake_formation_config() which won't work because our API has no way to record this and replace separate LF/IAM error messages with a single combined _ICEBERG_PERMISSIONS_ERROR_MESSAGE constant covering both governance models (SELECT/DESCRIBE/ALTER for LF, glue:GetTable/glue:UpdateTable for IAM). Apply to both _get_iceberg_properties and _update_iceberg_properties, preserving the more expansive logger.error() calls in the update path. Update tests accordingly. X-AI-Tool: Kiro CLI sisyphus
1 parent 0c37d84 commit 27e1d91

3 files changed

Lines changed: 24 additions & 106 deletions

File tree

sagemaker-mlops/src/sagemaker/mlops/feature_store/feature_group_manager.py

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
from botocore.exceptions import ClientError
2929
from pyiceberg.catalog import load_catalog
3030
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
31-
from sagemaker.mlops.feature_store.feature_utils import _APPROVED_ICEBERG_PROPERTIES
31+
from sagemaker.mlops.feature_store.feature_utils import (
32+
_APPROVED_ICEBERG_PROPERTIES,
33+
_ICEBERG_PERMISSIONS_ERROR_MESSAGE,
34+
)
3235

3336

3437
logger = logging.getLogger(__name__)
@@ -77,16 +80,6 @@ class FeatureGroupManager(FeatureGroup):
7780
if FeatureGroup.__doc__ and __doc__:
7881
__doc__ = FeatureGroup.__doc__
7982

80-
def _has_lake_formation_config(self) -> bool:
81-
"""Check if this feature group was created with Lake Formation governance.
82-
83-
Note: Returns False if the object was not hydrated via get()/describe
84-
(i.e., constructed directly with just a name). In that case, the caller
85-
falls back to the generic IAM error message, which is still valid advice.
86-
"""
87-
lf_config = getattr(self, "lake_formation_config", None)
88-
return lf_config is not None and lf_config != Unassigned()
89-
9083
def _validate_table_ownership(self, table, database_name: str, table_name: str):
9184
"""Validate that the Iceberg table belongs to this feature group by checking S3 location."""
9285
table_location = table.metadata.location if table.metadata else None
@@ -187,17 +180,9 @@ def _get_iceberg_properties(
187180

188181
except ClientError as e:
189182
if e.response["Error"]["Code"] == "AccessDeniedException":
190-
if self._has_lake_formation_config():
191-
raise PermissionError(
192-
f"Access denied reading Iceberg properties for '{self.feature_group_name}'. "
193-
f"This feature group uses Lake Formation governance — ensure you have "
194-
f"SELECT and DESCRIBE permissions on the table in Lake Formation, "
195-
f"in addition to IAM permissions."
196-
) from e
197183
raise PermissionError(
198184
f"Access denied reading Iceberg properties for '{self.feature_group_name}'. "
199-
f"Ensure your role has glue:GetTable permission "
200-
f"on the feature group's Glue table."
185+
f"{_ICEBERG_PERMISSIONS_ERROR_MESSAGE}"
201186
) from e
202187
raise RuntimeError(
203188
f"Failed to get Iceberg properties for '{self.feature_group_name}': {e}"
@@ -298,17 +283,9 @@ def _update_iceberg_properties(
298283

299284
except ClientError as e:
300285
if e.response["Error"]["Code"] == "AccessDeniedException":
301-
if self._has_lake_formation_config():
302-
raise PermissionError(
303-
f"Access denied updating Iceberg properties for '{self.feature_group_name}'. "
304-
f"This feature group uses Lake Formation governance — ensure you have "
305-
f"ALTER permission on the table in Lake Formation, "
306-
f"in addition to IAM permissions."
307-
) from e
308286
raise PermissionError(
309287
f"Access denied updating Iceberg properties for '{self.feature_group_name}'. "
310-
f"Ensure your role has glue:UpdateTable permission "
311-
f"on the feature group's Glue table."
288+
f"{_ICEBERG_PERMISSIONS_ERROR_MESSAGE}"
312289
) from e
313290
logger.error(
314291
f"Failed to update Iceberg properties for feature group "

sagemaker-mlops/src/sagemaker/mlops/feature_store/feature_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@
6666
"write.target-file-size-bytes"
6767
}
6868

69+
_ICEBERG_PERMISSIONS_ERROR_MESSAGE = (
70+
"If this feature group uses Lake Formation governance, ensure you have "
71+
"SELECT, DESCRIBE, and ALTER permissions on the table in Lake Formation, "
72+
"in addition to IAM permissions.\n"
73+
"If this feature group uses IAM governance, ensure your role has "
74+
"glue:GetTable and glue:UpdateTable permissions on the feature group's Glue table."
75+
)
6976

7077
def _get_athena_client(session: Session):
7178
"""Get Athena client from session."""

sagemaker-mlops/tests/unit/sagemaker/mlops/feature_store/test_iceberg_properties.py

Lines changed: 11 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -995,41 +995,6 @@ def test_passes_session_and_region_to_get_iceberg_properties(self, mock_get_clie
995995
mock_get_iceberg.assert_called_once_with(session=mock_session, region="us-east-1")
996996

997997

998-
class TestHasLakeFormationConfig:
999-
"""Tests for _has_lake_formation_config method."""
1000-
1001-
def test_returns_false_when_attribute_missing(self):
1002-
"""Test returns False when lake_formation_config attribute does not exist."""
1003-
fg = MagicMock(spec=FeatureGroupManager)
1004-
fg._has_lake_formation_config = FeatureGroupManager._has_lake_formation_config.__get__(fg)
1005-
# MagicMock(spec=...) won't have lake_formation_config since it's not on the class
1006-
del fg.lake_formation_config
1007-
assert fg._has_lake_formation_config() is False
1008-
1009-
def test_returns_false_when_none(self):
1010-
"""Test returns False when lake_formation_config is None."""
1011-
fg = MagicMock(spec=FeatureGroupManager)
1012-
fg._has_lake_formation_config = FeatureGroupManager._has_lake_formation_config.__get__(fg)
1013-
fg.lake_formation_config = None
1014-
assert fg._has_lake_formation_config() is False
1015-
1016-
def test_returns_false_when_unassigned(self):
1017-
"""Test returns False when lake_formation_config is Unassigned()."""
1018-
from sagemaker.core.shapes import Unassigned
1019-
1020-
fg = MagicMock(spec=FeatureGroupManager)
1021-
fg._has_lake_formation_config = FeatureGroupManager._has_lake_formation_config.__get__(fg)
1022-
fg.lake_formation_config = Unassigned()
1023-
assert fg._has_lake_formation_config() is False
1024-
1025-
def test_returns_true_when_config_present(self):
1026-
"""Test returns True when lake_formation_config has a real value."""
1027-
fg = MagicMock(spec=FeatureGroupManager)
1028-
fg._has_lake_formation_config = FeatureGroupManager._has_lake_formation_config.__get__(fg)
1029-
fg.lake_formation_config = MagicMock()
1030-
assert fg._has_lake_formation_config() is True
1031-
1032-
1033998
class TestGetIcebergPropertiesAccessDenied:
1034999
"""Tests for AccessDeniedException handling in _get_iceberg_properties."""
10351000

@@ -1052,26 +1017,16 @@ def _make_client_error(self, code):
10521017
return ClientError({"Error": {"Code": code, "Message": "denied"}}, "GetTable")
10531018

10541019
@patch("sagemaker.mlops.feature_store.feature_group_manager.load_catalog")
1055-
def test_access_denied_with_lake_formation_raises_permission_error(self, mock_load_catalog):
1056-
"""Test PermissionError with LF message when AccessDenied and LF config present."""
1057-
mock_catalog = MagicMock()
1058-
mock_catalog.load_table.side_effect = self._make_client_error("AccessDeniedException")
1059-
mock_load_catalog.return_value = mock_catalog
1060-
self.fg._has_lake_formation_config.return_value = True
1061-
1062-
with pytest.raises(PermissionError, match="Lake Formation governance"):
1063-
self.fg._get_iceberg_properties(session=MagicMock())
1064-
1065-
@patch("sagemaker.mlops.feature_store.feature_group_manager.load_catalog")
1066-
def test_access_denied_without_lake_formation_raises_permission_error(self, mock_load_catalog):
1067-
"""Test PermissionError with IAM message when AccessDenied and no LF config."""
1020+
def test_access_denied_raises_permission_error_with_combined_message(self, mock_load_catalog):
1021+
"""Test PermissionError with combined LF/IAM message on AccessDenied."""
10681022
mock_catalog = MagicMock()
10691023
mock_catalog.load_table.side_effect = self._make_client_error("AccessDeniedException")
10701024
mock_load_catalog.return_value = mock_catalog
1071-
self.fg._has_lake_formation_config.return_value = False
10721025

1073-
with pytest.raises(PermissionError, match="glue:GetTable"):
1026+
with pytest.raises(PermissionError, match="Lake Formation governance") as exc_info:
10741027
self.fg._get_iceberg_properties(session=MagicMock())
1028+
assert "glue:GetTable" in str(exc_info.value)
1029+
assert "glue:UpdateTable" in str(exc_info.value)
10751030

10761031
@patch("sagemaker.mlops.feature_store.feature_group_manager.load_catalog")
10771032
def test_non_access_denied_client_error_raises_runtime_error(self, mock_load_catalog):
@@ -1090,7 +1045,6 @@ def test_access_denied_permission_error_chains_original(self, mock_load_catalog)
10901045
mock_catalog = MagicMock()
10911046
mock_catalog.load_table.side_effect = original
10921047
mock_load_catalog.return_value = mock_catalog
1093-
self.fg._has_lake_formation_config.return_value = False
10941048

10951049
with pytest.raises(PermissionError) as exc_info:
10961050
self.fg._get_iceberg_properties(session=MagicMock())
@@ -1121,31 +1075,22 @@ def _setup_get_result(self):
11211075
}
11221076
return mock_table
11231077

1124-
def test_access_denied_with_lake_formation_raises_permission_error(self):
1125-
"""Test PermissionError with LF message when AccessDenied and LF config present."""
1078+
def test_access_denied_raises_permission_error_with_combined_message(self):
1079+
"""Test PermissionError with combined LF/IAM message on AccessDenied."""
11261080
mock_table = self._setup_get_result()
11271081
mock_table.transaction().__enter__().set_properties.side_effect = self._make_client_error("AccessDeniedException")
1128-
self.fg._has_lake_formation_config.return_value = True
11291082

11301083
props = IcebergProperties(properties={"write.target-file-size-bytes": "536870912"})
1131-
with pytest.raises(PermissionError, match="Lake Formation governance"):
1132-
self.fg._update_iceberg_properties(iceberg_properties=props)
1133-
1134-
def test_access_denied_without_lake_formation_raises_permission_error(self):
1135-
"""Test PermissionError with IAM message when AccessDenied and no LF config."""
1136-
mock_table = self._setup_get_result()
1137-
mock_table.transaction().__enter__().set_properties.side_effect = self._make_client_error("AccessDeniedException")
1138-
self.fg._has_lake_formation_config.return_value = False
1139-
1140-
props = IcebergProperties(properties={"write.target-file-size-bytes": "536870912"})
1141-
with pytest.raises(PermissionError, match="glue:UpdateTable"):
1084+
with pytest.raises(PermissionError, match="Lake Formation governance") as exc_info:
11421085
self.fg._update_iceberg_properties(iceberg_properties=props)
1086+
assert "glue:GetTable" in str(exc_info.value)
1087+
assert "glue:UpdateTable" in str(exc_info.value)
1088+
assert "ALTER" in str(exc_info.value)
11431089

11441090
def test_non_access_denied_client_error_raises_runtime_error(self):
11451091
"""Test RuntimeError for non-AccessDenied ClientError."""
11461092
mock_table = self._setup_get_result()
11471093
mock_table.transaction().__enter__().set_properties.side_effect = self._make_client_error("InternalServiceException")
1148-
self.fg._has_lake_formation_config.return_value = False
11491094

11501095
props = IcebergProperties(properties={"write.target-file-size-bytes": "536870912"})
11511096
with pytest.raises(RuntimeError, match="Failed to update Iceberg properties"):
@@ -1156,19 +1101,8 @@ def test_access_denied_permission_error_chains_original(self):
11561101
mock_table = self._setup_get_result()
11571102
original = self._make_client_error("AccessDeniedException")
11581103
mock_table.transaction().__enter__().set_properties.side_effect = original
1159-
self.fg._has_lake_formation_config.return_value = True
11601104

11611105
props = IcebergProperties(properties={"write.target-file-size-bytes": "536870912"})
11621106
with pytest.raises(PermissionError) as exc_info:
11631107
self.fg._update_iceberg_properties(iceberg_properties=props)
11641108
assert exc_info.value.__cause__ is original
1165-
1166-
def test_access_denied_lf_message_mentions_alter(self):
1167-
"""Test that LF error message mentions ALTER permission."""
1168-
mock_table = self._setup_get_result()
1169-
mock_table.transaction().__enter__().set_properties.side_effect = self._make_client_error("AccessDeniedException")
1170-
self.fg._has_lake_formation_config.return_value = True
1171-
1172-
props = IcebergProperties(properties={"write.target-file-size-bytes": "536870912"})
1173-
with pytest.raises(PermissionError, match="ALTER permission"):
1174-
self.fg._update_iceberg_properties(iceberg_properties=props)

0 commit comments

Comments
 (0)