Skip to content

Commit d9706ce

Browse files
committed
fix: Fix missing error handling for resource_counts endpoint
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent 8b5a526 commit d9706ce

File tree

2 files changed

+98
-12
lines changed

2 files changed

+98
-12
lines changed

sdk/python/feast/api/registry/rest/metrics.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,24 @@ async def resource_counts(
6666
allow_cache: bool = Query(True),
6767
):
6868
def count_resources_for_project(project_name: str):
69-
entities = grpc_call(
70-
grpc_handler.ListEntities,
71-
RegistryServer_pb2.ListEntitiesRequest(
72-
project=project_name, allow_cache=allow_cache
73-
),
74-
)
75-
data_sources = grpc_call(
76-
grpc_handler.ListDataSources,
77-
RegistryServer_pb2.ListDataSourcesRequest(
78-
project=project_name, allow_cache=allow_cache
79-
),
80-
)
69+
try:
70+
entities = grpc_call(
71+
grpc_handler.ListEntities,
72+
RegistryServer_pb2.ListEntitiesRequest(
73+
project=project_name, allow_cache=allow_cache
74+
),
75+
)
76+
except Exception:
77+
entities = {"entities": []}
78+
try:
79+
data_sources = grpc_call(
80+
grpc_handler.ListDataSources,
81+
RegistryServer_pb2.ListDataSourcesRequest(
82+
project=project_name, allow_cache=allow_cache
83+
),
84+
)
85+
except Exception:
86+
data_sources = {"dataSources": []}
8187
try:
8288
saved_datasets = grpc_call(
8389
grpc_handler.ListSavedDatasets,

sdk/python/tests/unit/api/test_api_rest_registry.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,86 @@ def test_metrics_resource_counts_via_rest(fastapi_test_app):
15281528
assert isinstance(per_project["demo_project"], dict)
15291529

15301530

1531+
def test_metrics_resource_counts_with_permission_errors(fastapi_test_app):
1532+
"""
1533+
Test that /metrics/resource_counts returns 200 with zero counts for
1534+
resource types that raise FeastPermissionError, instead of failing
1535+
the entire request. This simulates the scenario where access is
1536+
restricted for some resource types via GroupBasedPolicy,
1537+
NamespaceBasedPolicy, or CombinedGroupNamespacePolicy.
1538+
"""
1539+
from unittest.mock import patch
1540+
1541+
from feast.errors import FeastPermissionError
1542+
1543+
original_get = fastapi_test_app.get
1544+
1545+
# First, get the baseline counts to know which resources have data
1546+
baseline = original_get("/metrics/resource_counts?project=demo_project").json()
1547+
baseline_counts = baseline["counts"]
1548+
1549+
# Patch grpc_call to raise FeastPermissionError for entities and data sources
1550+
# while allowing other resource types to succeed
1551+
original_grpc_call = None
1552+
1553+
def grpc_call_entities_denied(handler_fn, request):
1554+
handler_name = getattr(handler_fn, "__name__", "")
1555+
if handler_name in ("ListEntities", "ListDataSources"):
1556+
raise FeastPermissionError(f"Permission denied for {handler_name}")
1557+
return original_grpc_call(handler_fn, request)
1558+
1559+
with patch("feast.api.registry.rest.metrics.grpc_call") as mock_grpc_call:
1560+
import feast.api.registry.rest.metrics as metrics_module
1561+
1562+
original_grpc_call = metrics_module.__dict__.get("grpc_call")
1563+
# Restore actual import since patch replaces it
1564+
from feast.api.registry.rest.rest_utils import grpc_call as real_grpc_call
1565+
1566+
original_grpc_call = real_grpc_call
1567+
mock_grpc_call.side_effect = grpc_call_entities_denied
1568+
1569+
response = fastapi_test_app.get("/metrics/resource_counts?project=demo_project")
1570+
1571+
assert response.status_code == 200, (
1572+
f"Expected 200 but got {response.status_code}: {response.text}"
1573+
)
1574+
data = response.json()
1575+
assert "counts" in data
1576+
counts = data["counts"]
1577+
1578+
# Denied resource types should have 0 counts
1579+
assert counts["entities"] == 0
1580+
assert counts["dataSources"] == 0
1581+
1582+
# Permitted resource types should still have their original counts
1583+
assert counts["featureViews"] == baseline_counts["featureViews"]
1584+
assert counts["featureServices"] == baseline_counts["featureServices"]
1585+
assert counts["features"] == baseline_counts["features"]
1586+
assert counts["savedDatasets"] == baseline_counts["savedDatasets"]
1587+
1588+
# Now test with ALL resource types denied
1589+
def grpc_call_all_denied(handler_fn, request):
1590+
handler_name = getattr(handler_fn, "__name__", "")
1591+
if handler_name.startswith("List") and handler_name != "ListProjects":
1592+
raise FeastPermissionError(f"Permission denied for {handler_name}")
1593+
return real_grpc_call(handler_fn, request)
1594+
1595+
with patch("feast.api.registry.rest.metrics.grpc_call") as mock_grpc_call:
1596+
mock_grpc_call.side_effect = grpc_call_all_denied
1597+
1598+
response = fastapi_test_app.get("/metrics/resource_counts?project=demo_project")
1599+
1600+
assert response.status_code == 200
1601+
data = response.json()
1602+
counts = data["counts"]
1603+
1604+
# All counts should be 0 when all resource types are denied
1605+
for resource_type, count in counts.items():
1606+
assert count == 0, (
1607+
f"Expected 0 for {resource_type} when permission denied, got {count}"
1608+
)
1609+
1610+
15311611
def test_feature_views_all_types_and_resource_counts_match(fastapi_test_app):
15321612
"""
15331613
Test that verifies:

0 commit comments

Comments
 (0)