diff --git a/sdk/python/feast/api/registry/rest/metrics.py b/sdk/python/feast/api/registry/rest/metrics.py index 87cb30ee4e2..e2249c7a420 100644 --- a/sdk/python/feast/api/registry/rest/metrics.py +++ b/sdk/python/feast/api/registry/rest/metrics.py @@ -11,6 +11,9 @@ grpc_call, paginate_and_sort, ) +from feast.errors import FeastObjectNotFoundException +from feast.permissions.action import AuthzedAction +from feast.permissions.security_manager import assert_permissions from feast.protos.feast.registry import RegistryServer_pb2 @@ -433,15 +436,21 @@ async def recently_visited( key = f"recently_visited_{user}" visits = [] if project: - try: - visits_json = ( - server.registry.get_project_metadata(project, key) - if server - else None - ) - visits = json.loads(visits_json) if visits_json else [] - except Exception: - visits = [] + if server: + try: + project_obj = server.registry.get_project( + name=project, allow_cache=True + ) + assert_permissions( + resource=project_obj, actions=[AuthzedAction.DESCRIBE] + ) + except FeastObjectNotFoundException: + pass + try: + visits_json = server.registry.get_project_metadata(project, key) + visits = json.loads(visits_json) if visits_json else [] + except Exception: + visits = [] else: try: if server: diff --git a/sdk/python/feast/api/registry/rest/rest_registry_server.py b/sdk/python/feast/api/registry/rest/rest_registry_server.py index 0238776dbc0..02454b263ff 100644 --- a/sdk/python/feast/api/registry/rest/rest_registry_server.py +++ b/sdk/python/feast/api/registry/rest/rest_registry_server.py @@ -235,28 +235,35 @@ async def dispatch(self, request: Request, call_next): else: object_type = None object_name = None - visit = { - "path": path, - "timestamp": _utc_now().isoformat(), - "project": project, - "user": user, - "object": object_type, - "object_name": object_name, - "method": method, - } - try: - visits_json = self.registry.get_project_metadata(project, key) - visits = json.loads(visits_json) if visits_json else [] - except Exception: - visits = [] - visits.append(visit) - visits = visits[-self.recent_visits_limit :] - try: - self.registry.set_project_metadata( - project, key, json.dumps(visits) - ) - except Exception as e: - logger.warning(f"Failed to persist recent visits: {e}") + + response = await call_next(request) + + if response.status_code < 400: + visit = { + "path": path, + "timestamp": _utc_now().isoformat(), + "project": project, + "user": user, + "object": object_type, + "object_name": object_name, + "method": method, + } + try: + visits_json = self.registry.get_project_metadata( + project, key + ) + visits = json.loads(visits_json) if visits_json else [] + except Exception: + visits = [] + visits.append(visit) + visits = visits[-self.recent_visits_limit :] + try: + self.registry.set_project_metadata( + project, key, json.dumps(visits) + ) + except Exception as e: + logger.warning(f"Failed to persist recent visits: {e}") + return response response = await call_next(request) return response diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index bca34e918e2..37d634936e4 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -584,12 +584,22 @@ async def chat_ui(): @app.post("/materialize", dependencies=[Depends(inject_user_details)]) async def materialize(request: MaterializeRequest) -> None: with feast_metrics.track_request_latency("/materialize"): - for feature_view in request.feature_views or []: - resource = await _get_feast_object(feature_view, True) - assert_permissions( - resource=resource, - actions=[AuthzedAction.WRITE_ONLINE], + if request.feature_views: + for feature_view in request.feature_views: + resource = await _get_feast_object(feature_view, True) + assert_permissions( + resource=resource, + actions=[AuthzedAction.WRITE_ONLINE], + ) + else: + feature_views_to_materialize = store._get_feature_views_to_materialize( + None ) + for fv in feature_views_to_materialize: + assert_permissions( + resource=fv, + actions=[AuthzedAction.WRITE_ONLINE], + ) if request.disable_event_timestamp: now = datetime.now() @@ -615,12 +625,22 @@ async def materialize(request: MaterializeRequest) -> None: @app.post("/materialize-incremental", dependencies=[Depends(inject_user_details)]) async def materialize_incremental(request: MaterializeIncrementalRequest) -> None: with feast_metrics.track_request_latency("/materialize-incremental"): - for feature_view in request.feature_views or []: - resource = await _get_feast_object(feature_view, True) - assert_permissions( - resource=resource, - actions=[AuthzedAction.WRITE_ONLINE], + if request.feature_views: + for feature_view in request.feature_views: + resource = await _get_feast_object(feature_view, True) + assert_permissions( + resource=resource, + actions=[AuthzedAction.WRITE_ONLINE], + ) + else: + feature_views_to_materialize = store._get_feature_views_to_materialize( + None ) + for fv in feature_views_to_materialize: + assert_permissions( + resource=fv, + actions=[AuthzedAction.WRITE_ONLINE], + ) await run_in_threadpool( store.materialize_incremental, utils.make_tzaware(parser.parse(request.end_ts)), diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index c55cd1cc3b2..2da25a317bc 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -891,6 +891,13 @@ def DeleteValidationReference( def ListProjectMetadata( self, request: RegistryServer_pb2.ListProjectMetadataRequest, context ): + try: + project = self.proxied_registry.get_project( + name=request.project, allow_cache=True + ) + assert_permissions(resource=project, actions=[AuthzedAction.DESCRIBE]) + except FeastObjectNotFoundException: + pass return RegistryServer_pb2.ListProjectMetadataResponse( project_metadata=[ project_metadata.to_proto() @@ -923,6 +930,10 @@ def ApplyMaterialization( return Empty() def UpdateInfra(self, request: RegistryServer_pb2.UpdateInfraRequest, context): + project = self.proxied_registry.get_project( + name=request.project, allow_cache=True + ) + assert_permissions(resource=project, actions=[AuthzedAction.UPDATE]) self.proxied_registry.update_infra( infra=Infra.from_proto(request.infra), project=request.project, @@ -931,6 +942,10 @@ def UpdateInfra(self, request: RegistryServer_pb2.UpdateInfraRequest, context): return Empty() def GetInfra(self, request: RegistryServer_pb2.GetInfraRequest, context): + project = self.proxied_registry.get_project( + name=request.project, allow_cache=True + ) + assert_permissions(resource=project, actions=[AuthzedAction.DESCRIBE]) return self.proxied_registry.get_infra( project=request.project, allow_cache=request.allow_cache ).to_proto() @@ -1063,6 +1078,13 @@ def DeleteProject(self, request: RegistryServer_pb2.DeleteProjectRequest, contex def GetRegistryLineage( self, request: RegistryServer_pb2.GetRegistryLineageRequest, context ): + try: + project = self.proxied_registry.get_project( + name=request.project, allow_cache=True + ) + assert_permissions(resource=project, actions=[AuthzedAction.DESCRIBE]) + except FeastObjectNotFoundException: + pass direct_relationships, indirect_relationships = ( self.proxied_registry.get_registry_lineage( project=request.project, @@ -1101,6 +1123,13 @@ def GetObjectRelationships( self, request: RegistryServer_pb2.GetObjectRelationshipsRequest, context ): """Get relationships for a specific object.""" + try: + project = self.proxied_registry.get_project( + name=request.project, allow_cache=True + ) + assert_permissions(resource=project, actions=[AuthzedAction.DESCRIBE]) + except FeastObjectNotFoundException: + pass relationships = self.proxied_registry.get_object_relationships( project=request.project, object_type=request.object_type,