diff --git a/protos/feast/registry/RegistryServer.proto b/protos/feast/registry/RegistryServer.proto index e441a71077f..fc2ab122178 100644 --- a/protos/feast/registry/RegistryServer.proto +++ b/protos/feast/registry/RegistryServer.proto @@ -342,6 +342,7 @@ message GetPermissionRequest { message ListPermissionsRequest { string project = 1; bool allow_cache = 2; + map tags = 3; } message ListPermissionsResponse { diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index cc27dd3d56d..eb6e5ba5bb3 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -2093,7 +2093,9 @@ def get_validation_reference( ref._dataset = self.get_saved_dataset(ref.dataset_name) return ref - def list_permissions(self, allow_cache: bool = False) -> List[Permission]: + def list_permissions( + self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + ) -> List[Permission]: """ Retrieves the list of permissions from the registry. @@ -2103,7 +2105,9 @@ def list_permissions(self, allow_cache: bool = False) -> List[Permission]: Returns: A list of data sources. """ - return self._registry.list_permissions(self.project, allow_cache=allow_cache) + return self._registry.list_permissions( + self.project, allow_cache=allow_cache, tags=tags + ) def _print_materialization_log( diff --git a/sdk/python/feast/infra/registry/base_registry.py b/sdk/python/feast/infra/registry/base_registry.py index 7283feaec83..a5af1d85dac 100644 --- a/sdk/python/feast/infra/registry/base_registry.py +++ b/sdk/python/feast/infra/registry/base_registry.py @@ -640,6 +640,7 @@ def list_permissions( self, project: str, allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[Permission]: """ Retrieve a list of permissions from the registry @@ -779,6 +780,13 @@ def to_dict(self, project: str) -> Dict[str, List[Any]]: registry_dict["infra"].append( self._message_to_sorted_dict(infra_object.to_proto()) ) + for permission in sorted( + self.list_permissions(project=project), key=lambda ds: ds.name + ): + registry_dict["permissions"].append( + self._message_to_sorted_dict(permission.to_proto()) + ) + return registry_dict @staticmethod diff --git a/sdk/python/feast/infra/registry/caching_registry.py b/sdk/python/feast/infra/registry/caching_registry.py index 45c4e349c2f..09604a19877 100644 --- a/sdk/python/feast/infra/registry/caching_registry.py +++ b/sdk/python/feast/infra/registry/caching_registry.py @@ -324,20 +324,23 @@ def get_permission( return self._get_permission(name, project) @abstractmethod - def _list_permissions(self, project: str) -> List[Permission]: + def _list_permissions( + self, project: str, tags: Optional[dict[str, str]] + ) -> List[Permission]: pass def list_permissions( self, project: str, allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[Permission]: if allow_cache: self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_permissions( - self.cached_registry_proto, project + self.cached_registry_proto, project, tags ) - return self._list_permissions(project) + return self._list_permissions(project, tags) def refresh(self, project: Optional[str] = None): if project: diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py index 4dc972f325b..bb4eb4ee40c 100644 --- a/sdk/python/feast/infra/registry/proto_registry_utils.py +++ b/sdk/python/feast/infra/registry/proto_registry_utils.py @@ -289,11 +289,15 @@ def list_project_metadata( ] -@registry_proto_cache -def list_permissions(registry_proto: RegistryProto, project: str) -> List[Permission]: +@registry_proto_cache_with_tags +def list_permissions( + registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] +) -> List[Permission]: permissions = [] for permission_proto in registry_proto.permissions: - if permission_proto.project == project: + if permission_proto.project == project and utils.has_all_tags( + permission_proto.tags, tags + ): permissions.append(Permission.from_proto(permission_proto)) return permissions diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index a0ee9645f9a..2dec58acf54 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -916,12 +916,15 @@ def get_permission( return proto_registry_utils.get_permission(registry_proto, name, project) def list_permissions( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[Permission]: registry_proto = self._get_registry_proto( project=project, allow_cache=allow_cache ) - return proto_registry_utils.list_permissions(registry_proto, project) + return proto_registry_utils.list_permissions(registry_proto, project, tags) def apply_permission( self, permission: Permission, project: str, commit: bool = True diff --git a/sdk/python/feast/infra/registry/remote.py b/sdk/python/feast/infra/registry/remote.py index 4ef2359cbe5..2b1e40b50dd 100644 --- a/sdk/python/feast/infra/registry/remote.py +++ b/sdk/python/feast/infra/registry/remote.py @@ -472,9 +472,10 @@ def list_permissions( self, project: str, allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[Permission]: request = RegistryServer_pb2.ListPermissionsRequest( - project=project, allow_cache=allow_cache + project=project, allow_cache=allow_cache, tags=tags ) response = self.stub.ListPermissions(request) diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index 5e48824adaa..4303d3667f6 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -838,6 +838,7 @@ def list_permissions( self, project: str, allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[Permission]: if allow_cache: self._refresh_cached_registry_if_necessary() @@ -850,6 +851,7 @@ def list_permissions( PermissionProto, Permission, "PERMISSION_PROTO", + tags, ) def apply_materialization( diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index 0bc2afc6ec1..21e0b2db77f 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -949,13 +949,16 @@ def _get_permission(self, name: str, project: str) -> Permission: not_found_exception=PermissionNotFoundException, ) - def _list_permissions(self, project: str) -> List[Permission]: + def _list_permissions( + self, project: str, tags: Optional[dict[str, str]] + ) -> List[Permission]: return self._list_objects( permissions, project, PermissionProto, Permission, "permission_proto", + tags=tags, ) def apply_permission( diff --git a/sdk/python/feast/permissions/permission.py b/sdk/python/feast/permissions/permission.py index be8abacdd40..2f8f8207659 100644 --- a/sdk/python/feast/permissions/permission.py +++ b/sdk/python/feast/permissions/permission.py @@ -32,17 +32,16 @@ class Permission(ABC): with_subclasses: If `True`, it includes sub-classes of the given types in the match, otherwise only exact type match is applied. Defaults to `True`. name_pattern: A regex to match the resource name. Defaults to None, meaning that no name filtering is applied - required_tags: Dictionary of key-value pairs that must match the resource tags. All these required_tags must be present in a resource tags with the given value. Defaults to None, meaning that no tags filtering is applied. actions: The actions authorized by this permission. Defaults to `ALL_ACTIONS`. policy: The policy to be applied to validate a client request. + tags: Dictionary of key-value pairs that must match the resource tags. All these tags must """ _name: str _types: list["FeastObject"] _with_subclasses: bool _name_pattern: Optional[str] - _required_tags: Optional[dict[str, str]] _actions: list[AuthzedAction] _policy: Policy _tags: Dict[str, str] @@ -53,10 +52,9 @@ def __init__( types: Optional[Union[list["FeastObject"], "FeastObject"]] = None, with_subclasses: bool = True, name_pattern: Optional[str] = None, - required_tags: Optional[dict[str, str]] = None, actions: Union[list[AuthzedAction], AuthzedAction] = ALL_ACTIONS, policy: Policy = AllowAll, - tags: Optional[Dict[str, str]] = None, + tags: Optional[dict[str, str]] = None, ): from feast.feast_object import ALL_RESOURCE_TYPES @@ -73,10 +71,9 @@ def __init__( self._types = types if isinstance(types, list) else [types] self._with_subclasses = with_subclasses self._name_pattern = _normalize_name_pattern(name_pattern) - self._required_tags = _normalize_required_tags(required_tags) self._actions = actions if isinstance(actions, list) else [actions] self._policy = policy - self._tags = tags or {} + self._tags = _normalize_tags(tags) def __eq__(self, other): if not isinstance(other, Permission): @@ -86,7 +83,7 @@ def __eq__(self, other): self.name != other.name or self.with_subclasses != other.with_subclasses or self.name_pattern != other.name_pattern - or self.required_tags != other.required_tags + or self.tags != other.tags or self.policy != other.policy or self.actions != other.actions ): @@ -129,10 +126,6 @@ def with_subclasses(self) -> bool: def name_pattern(self) -> Optional[str]: return self._name_pattern - @property - def required_tags(self) -> Optional[dict[str, str]]: - return self._required_tags - @property def actions(self) -> list[AuthzedAction]: return self._actions @@ -155,7 +148,7 @@ def match_resource(self, resource: "FeastObject") -> bool: expected_types=self.types, with_subclasses=self.with_subclasses, name_pattern=self.name_pattern, - required_tags=self.required_tags, + required_tags=self.tags, ) def match_actions(self, requested_actions: list[AuthzedAction]) -> bool: @@ -196,7 +189,6 @@ def from_proto(permission_proto: PermissionProto) -> Any: types, permission_proto.with_subclasses, permission_proto.name_pattern or None, - dict(permission_proto.required_tags), actions, Policy.from_proto(permission_proto.policy), dict(permission_proto.tags) or None, @@ -224,7 +216,6 @@ def to_proto(self) -> PermissionProto: types=types, with_subclasses=self.with_subclasses, name_pattern=self.name_pattern if self.name_pattern is not None else None, - required_tags=self.required_tags, actions=actions, policy=self.policy.to_proto(), tags=self._tags if self._tags is not None else None, @@ -239,11 +230,10 @@ def _normalize_name_pattern(name_pattern: Optional[str]): return None -def _normalize_required_tags(required_tags: Optional[dict[str, str]]): - if required_tags: +def _normalize_tags(tags: Optional[dict[str, str]]): + if tags: return { - k.strip(): v.strip() if isinstance(v, str) else v - for k, v in required_tags.items() + k.strip(): v.strip() if isinstance(v, str) else v for k, v in tags.items() } return None diff --git a/sdk/python/tests/unit/permissions/test_permission.py b/sdk/python/tests/unit/permissions/test_permission.py index 86659d4ff99..8bb473952e8 100644 --- a/sdk/python/tests/unit/permissions/test_permission.py +++ b/sdk/python/tests/unit/permissions/test_permission.py @@ -38,7 +38,7 @@ def test_defaults(): assertpy.assert_that(p.types).is_equal_to(ALL_RESOURCE_TYPES) assertpy.assert_that(p.with_subclasses).is_true() assertpy.assert_that(p.name_pattern).is_none() - assertpy.assert_that(p.required_tags).is_none() + assertpy.assert_that(p.tags).is_none() assertpy.assert_that(type(p.actions)).is_equal_to(list) assertpy.assert_that(p.actions).is_equal_to(ALL_ACTIONS) assertpy.assert_that(type(p.actions)).is_equal_to(list) @@ -246,7 +246,7 @@ def test_resource_match_with_name_filter(pattern, name, match): ) def test_resource_match_with_tags(required_tags, tags, result): # Missing tags - p = Permission(name="test", required_tags=required_tags) + p = Permission(name="test", tags=required_tags) for t in ALL_RESOURCE_TYPES: resource = Mock(spec=t) resource.name = "test"