diff --git a/docs/getting-started/concepts/permission.md b/docs/getting-started/concepts/permission.md index 5bca1bd568..a635357968 100644 --- a/docs/getting-started/concepts/permission.md +++ b/docs/getting-started/concepts/permission.md @@ -36,7 +36,7 @@ The permission model is based on the following components: The `Permission` class identifies a single permission configured on the feature store and is identified by these attributes: - `name`: The permission name. - `types`: The list of protected resource types. Defaults to all managed types, e.g. the `ALL_RESOURCE_TYPES` alias. All sub-classes are included in the resource match. -- `name_pattern`: A regex to match the resource name. Defaults to `None`, meaning that no name filtering is applied +- `name_patterns`: A list of regex patterns to match resource names. If any regex matches, the `Permission` policy is applied. Defaults to `[]`, meaning no name filtering is applied. - `required_tags`: Dictionary of key-value pairs that must match the resource tags. Defaults to `None`, meaning that no tags filtering is applied. - `actions`: The actions authorized by this permission. Defaults to `ALL_VALUES`, an alias defined in the `action` module. - `policy`: The policy to be applied to validate a client request. @@ -95,7 +95,7 @@ The following permission grants authorization to read the offline store of all t Permission( name="reader", types=[FeatureView], - name_pattern=".*risky.*", + name_patterns=".*risky.*", # Accepts both `str` or `list[str]` types policy=RoleBasedPolicy(roles=["trusted"]), actions=[AuthzedAction.READ_OFFLINE], ) diff --git a/docs/reference/feast-cli-commands.md b/docs/reference/feast-cli-commands.md index b32db3215a..8f1a7c302e 100644 --- a/docs/reference/feast-cli-commands.md +++ b/docs/reference/feast-cli-commands.md @@ -172,9 +172,10 @@ Options: ```text +-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ -| NAME | TYPES | NAME_PATTERN | ACTIONS | ROLES | REQUIRED_TAGS | +| NAME | TYPES | NAME_PATTERNS | ACTIONS | ROLES | REQUIRED_TAGS | +=======================+=============+=======================+===========+================+================+========+ | reader_permission1234 | FeatureView | transformed_conv_rate | DESCRIBE | reader | - | +| | | driver_hourly_stats | DESCRIBE | reader | - | +-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ | writer_permission1234 | FeatureView | transformed_conv_rate | CREATE | writer | - | +-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ diff --git a/protos/feast/core/Permission.proto b/protos/feast/core/Permission.proto index 400f70a11b..8a876a0dc7 100644 --- a/protos/feast/core/Permission.proto +++ b/protos/feast/core/Permission.proto @@ -50,7 +50,7 @@ message PermissionSpec { repeated Type types = 3; - string name_pattern = 4; + repeated string name_patterns = 4; map required_tags = 5; diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index 499788101e..010493f01c 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -1211,7 +1211,7 @@ def feast_permissions_list_command(ctx: click.Context, verbose: bool, tags: list headers=[ "NAME", "TYPES", - "NAME_PATTERN", + "NAME_PATTERNS", "ACTIONS", "ROLES", "REQUIRED_TAGS", diff --git a/sdk/python/feast/cli_utils.py b/sdk/python/feast/cli_utils.py index 4152eb219b..38ebf91570 100644 --- a/sdk/python/feast/cli_utils.py +++ b/sdk/python/feast/cli_utils.py @@ -196,7 +196,7 @@ def handle_not_verbose_permissions_command( [ p.name, _to_multi_line([t.__name__ for t in p.types]), # type: ignore[union-attr, attr-defined] - p.name_pattern, + _to_multi_line(p.name_patterns), _to_multi_line([a.value.upper() for a in p.actions]), _to_multi_line(sorted(roles)), _dict_to_multi_line(p.required_tags), diff --git a/sdk/python/feast/permissions/matcher.py b/sdk/python/feast/permissions/matcher.py index 337bfd5c57..5cb0de85e3 100644 --- a/sdk/python/feast/permissions/matcher.py +++ b/sdk/python/feast/permissions/matcher.py @@ -44,7 +44,7 @@ def _get_type(resource: "FeastObject") -> Any: def resource_match_config( resource: "FeastObject", expected_types: list["FeastObject"], - name_pattern: Optional[str] = None, + name_patterns: list[str], required_tags: Optional[dict[str, str]] = None, ) -> bool: """ @@ -53,7 +53,7 @@ def resource_match_config( Args: resource: A FeastObject instance to match agains the permission. expected_types: The list of object types configured in the permission. Type match also includes all the sub-classes. - name_pattern: The optional name pattern filter configured in the permission. + name_patterns: The possibly empty list of name pattern filters configured in the permission. required_tags: The optional dictionary of required tags configured in the permission. Returns: @@ -75,21 +75,8 @@ def resource_match_config( ) return False - if name_pattern is not None: - if hasattr(resource, "name"): - if isinstance(resource.name, str): - match = bool(re.fullmatch(name_pattern, resource.name)) - if not match: - logger.info( - f"Resource name {resource.name} does not match pattern {name_pattern}" - ) - return False - else: - logger.warning( - f"Resource {resource} has no `name` attribute of unexpected type {type(resource.name)}" - ) - else: - logger.warning(f"Resource {resource} has no `name` attribute") + if not _resource_name_matches_name_patterns(resource, name_patterns): + return False if required_tags: if hasattr(resource, "required_tags"): @@ -112,6 +99,39 @@ def resource_match_config( return True +def _resource_name_matches_name_patterns( + resource: "FeastObject", + name_patterns: list[str], +) -> bool: + if not hasattr(resource, "name"): + logger.warning(f"Resource {resource} has no `name` attribute") + return True + + if not name_patterns: + return True + + if resource.name is None: + return True + + if not isinstance(resource.name, str): + logger.warning( + f"Resource {resource} has `name` attribute of unexpected type {type(resource.name)}" + ) + return True + + for name_pattern in name_patterns: + match = bool(re.fullmatch(name_pattern, resource.name)) + if not match: + logger.info( + f"Resource name {resource.name} does not match pattern {name_pattern}" + ) + else: + logger.info(f"Resource name {resource.name} matched pattern {name_pattern}") + return True + + return False + + def actions_match_config( requested_actions: list[AuthzedAction], allowed_actions: list[AuthzedAction], diff --git a/sdk/python/feast/permissions/permission.py b/sdk/python/feast/permissions/permission.py index 9046abbfa9..964ca743e7 100644 --- a/sdk/python/feast/permissions/permission.py +++ b/sdk/python/feast/permissions/permission.py @@ -33,7 +33,7 @@ class Permission(ABC): name: The permission name (can be duplicated, used for logging troubleshooting). types: The list of protected resource types as defined by the `FeastObject` type. The match includes all the sub-classes of the given types. Defaults to all managed types (e.g. the `ALL_RESOURCE_TYPES` constant) - name_pattern: A regex to match the resource name. Defaults to None, meaning that no name filtering is applied + name_patterns: A possibly empty list of regex patterns to match the resource name. Defaults to empty list, e.g. no name filtering is applied 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. @@ -43,7 +43,7 @@ class Permission(ABC): _name: str _types: list["FeastObject"] - _name_pattern: Optional[str] + _name_patterns: list[str] _actions: list[AuthzedAction] _policy: Policy _tags: Dict[str, str] @@ -54,8 +54,8 @@ class Permission(ABC): def __init__( self, name: str, - types: Optional[Union[list["FeastObject"], "FeastObject"]] = None, - name_pattern: Optional[str] = None, + types: Optional[Union[list["FeastObject"], "FeastObject"]] = [], + name_patterns: Optional[Union[str, list[str]]] = [], actions: Union[list[AuthzedAction], AuthzedAction] = ALL_ACTIONS, policy: Policy = AllowAll, tags: Optional[dict[str, str]] = None, @@ -74,7 +74,7 @@ def __init__( raise ValueError("The list 'policy' must be non-empty.") self._name = name self._types = types if isinstance(types, list) else [types] - self._name_pattern = _normalize_name_pattern(name_pattern) + self._name_patterns = _normalize_name_patterns(name_patterns) self._actions = actions if isinstance(actions, list) else [actions] self._policy = policy self._tags = _normalize_tags(tags) @@ -88,7 +88,7 @@ def __eq__(self, other): if ( self.name != other.name - or self.name_pattern != other.name_pattern + or self.name_patterns != other.name_patterns or self.tags != other.tags or self.policy != other.policy or self.actions != other.actions @@ -116,8 +116,8 @@ def types(self) -> list["FeastObject"]: return self._types @property - def name_pattern(self) -> Optional[str]: - return self._name_pattern + def name_patterns(self) -> list[str]: + return self._name_patterns @property def actions(self) -> list[AuthzedAction]: @@ -143,7 +143,7 @@ def match_resource(self, resource: "FeastObject") -> bool: return resource_match_config( resource=resource, expected_types=self.types, - name_pattern=self.name_pattern, + name_patterns=self.name_patterns, required_tags=self.required_tags, ) @@ -175,6 +175,9 @@ def from_proto(permission_proto: PermissionProto) -> Any: ) for t in permission_proto.spec.types ] + name_patterns = [ + name_pattern for name_pattern in permission_proto.spec.name_patterns + ] actions = [ AuthzedAction[PermissionSpecProto.AuthzedAction.Name(action)] for action in permission_proto.spec.actions @@ -183,7 +186,7 @@ def from_proto(permission_proto: PermissionProto) -> Any: permission = Permission( permission_proto.spec.name, types, - permission_proto.spec.name_pattern or None, + name_patterns, actions, Policy.from_proto(permission_proto.spec.policy), dict(permission_proto.spec.tags) or None, @@ -220,7 +223,7 @@ def to_proto(self) -> PermissionProto: permission_spec = PermissionSpecProto( name=self.name, types=types, - name_pattern=self.name_pattern if self.name_pattern is not None else "", + name_patterns=self.name_patterns, actions=actions, policy=self.policy.to_proto(), tags=self.tags, @@ -236,10 +239,17 @@ def to_proto(self) -> PermissionProto: return PermissionProto(spec=permission_spec, meta=meta) -def _normalize_name_pattern(name_pattern: Optional[str]): - if name_pattern is not None: - return name_pattern.strip() - return None +def _normalize_name_patterns( + name_patterns: Optional[Union[str, list[str]]], +) -> list[str]: + if name_patterns is None: + return [] + if isinstance(name_patterns, str): + return _normalize_name_patterns([name_patterns]) + normalized_name_patterns = [] + for name_pattern in name_patterns: + normalized_name_patterns.append(name_pattern.strip()) + return normalized_name_patterns def _normalize_tags(tags: Optional[dict[str, str]]): diff --git a/sdk/python/feast/protos/feast/core/Permission_pb2.py b/sdk/python/feast/protos/feast/core/Permission_pb2.py index 822ad0261b..706fd2eec4 100644 --- a/sdk/python/feast/protos/feast/core/Permission_pb2.py +++ b/sdk/python/feast/protos/feast/core/Permission_pb2.py @@ -16,7 +16,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/Permission.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Policy.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\nPermission\x12(\n\x04spec\x18\x01 \x01(\x0b\x32\x1a.feast.core.PermissionSpec\x12(\n\x04meta\x18\x02 \x01(\x0b\x32\x1a.feast.core.PermissionMeta\"\x9f\x06\n\x0ePermissionSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\x05types\x18\x03 \x03(\x0e\x32\x1f.feast.core.PermissionSpec.Type\x12\x14\n\x0cname_pattern\x18\x04 \x01(\t\x12\x43\n\rrequired_tags\x18\x05 \x03(\x0b\x32,.feast.core.PermissionSpec.RequiredTagsEntry\x12\x39\n\x07\x61\x63tions\x18\x06 \x03(\x0e\x32(.feast.core.PermissionSpec.AuthzedAction\x12\"\n\x06policy\x18\x07 \x01(\x0b\x32\x12.feast.core.Policy\x12\x32\n\x04tags\x18\x08 \x03(\x0b\x32$.feast.core.PermissionSpec.TagsEntry\x1a\x33\n\x11RequiredTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x89\x01\n\rAuthzedAction\x12\n\n\x06\x43REATE\x10\x00\x12\x0c\n\x08\x44\x45SCRIBE\x10\x01\x12\n\n\x06UPDATE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03\x12\x0f\n\x0bREAD_ONLINE\x10\x04\x12\x10\n\x0cREAD_OFFLINE\x10\x05\x12\x10\n\x0cWRITE_ONLINE\x10\x06\x12\x11\n\rWRITE_OFFLINE\x10\x07\"\xe1\x01\n\x04Type\x12\x10\n\x0c\x46\x45\x41TURE_VIEW\x10\x00\x12\x1a\n\x16ON_DEMAND_FEATURE_VIEW\x10\x01\x12\x16\n\x12\x42\x41TCH_FEATURE_VIEW\x10\x02\x12\x17\n\x13STREAM_FEATURE_VIEW\x10\x03\x12\n\n\x06\x45NTITY\x10\x04\x12\x13\n\x0f\x46\x45\x41TURE_SERVICE\x10\x05\x12\x0f\n\x0b\x44\x41TA_SOURCE\x10\x06\x12\x18\n\x14VALIDATION_REFERENCE\x10\x07\x12\x11\n\rSAVED_DATASET\x10\x08\x12\x0e\n\nPERMISSION\x10\t\x12\x0b\n\x07PROJECT\x10\n\"\x83\x01\n\x0ePermissionMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBT\n\x10\x66\x65\x61st.proto.coreB\x0fPermissionProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/Permission.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Policy.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\nPermission\x12(\n\x04spec\x18\x01 \x01(\x0b\x32\x1a.feast.core.PermissionSpec\x12(\n\x04meta\x18\x02 \x01(\x0b\x32\x1a.feast.core.PermissionMeta\"\xa0\x06\n\x0ePermissionSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\x05types\x18\x03 \x03(\x0e\x32\x1f.feast.core.PermissionSpec.Type\x12\x15\n\rname_patterns\x18\x04 \x03(\t\x12\x43\n\rrequired_tags\x18\x05 \x03(\x0b\x32,.feast.core.PermissionSpec.RequiredTagsEntry\x12\x39\n\x07\x61\x63tions\x18\x06 \x03(\x0e\x32(.feast.core.PermissionSpec.AuthzedAction\x12\"\n\x06policy\x18\x07 \x01(\x0b\x32\x12.feast.core.Policy\x12\x32\n\x04tags\x18\x08 \x03(\x0b\x32$.feast.core.PermissionSpec.TagsEntry\x1a\x33\n\x11RequiredTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x89\x01\n\rAuthzedAction\x12\n\n\x06\x43REATE\x10\x00\x12\x0c\n\x08\x44\x45SCRIBE\x10\x01\x12\n\n\x06UPDATE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03\x12\x0f\n\x0bREAD_ONLINE\x10\x04\x12\x10\n\x0cREAD_OFFLINE\x10\x05\x12\x10\n\x0cWRITE_ONLINE\x10\x06\x12\x11\n\rWRITE_OFFLINE\x10\x07\"\xe1\x01\n\x04Type\x12\x10\n\x0c\x46\x45\x41TURE_VIEW\x10\x00\x12\x1a\n\x16ON_DEMAND_FEATURE_VIEW\x10\x01\x12\x16\n\x12\x42\x41TCH_FEATURE_VIEW\x10\x02\x12\x17\n\x13STREAM_FEATURE_VIEW\x10\x03\x12\n\n\x06\x45NTITY\x10\x04\x12\x13\n\x0f\x46\x45\x41TURE_SERVICE\x10\x05\x12\x0f\n\x0b\x44\x41TA_SOURCE\x10\x06\x12\x18\n\x14VALIDATION_REFERENCE\x10\x07\x12\x11\n\rSAVED_DATASET\x10\x08\x12\x0e\n\nPERMISSION\x10\t\x12\x0b\n\x07PROJECT\x10\n\"\x83\x01\n\x0ePermissionMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBT\n\x10\x66\x65\x61st.proto.coreB\x0fPermissionProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -31,15 +31,15 @@ _globals['_PERMISSION']._serialized_start=101 _globals['_PERMISSION']._serialized_end=197 _globals['_PERMISSIONSPEC']._serialized_start=200 - _globals['_PERMISSIONSPEC']._serialized_end=999 - _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_start=535 - _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_end=586 - _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_start=588 - _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_end=631 - _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_start=634 - _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_end=771 - _globals['_PERMISSIONSPEC_TYPE']._serialized_start=774 - _globals['_PERMISSIONSPEC_TYPE']._serialized_end=999 - _globals['_PERMISSIONMETA']._serialized_start=1002 - _globals['_PERMISSIONMETA']._serialized_end=1133 + _globals['_PERMISSIONSPEC']._serialized_end=1000 + _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_start=536 + _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_end=587 + _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_start=589 + _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_end=632 + _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_start=635 + _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_end=772 + _globals['_PERMISSIONSPEC_TYPE']._serialized_start=775 + _globals['_PERMISSIONSPEC_TYPE']._serialized_end=1000 + _globals['_PERMISSIONMETA']._serialized_start=1003 + _globals['_PERMISSIONMETA']._serialized_end=1134 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Permission_pb2.pyi b/sdk/python/feast/protos/feast/core/Permission_pb2.pyi index 1155c13188..b2387d2946 100644 --- a/sdk/python/feast/protos/feast/core/Permission_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/Permission_pb2.pyi @@ -134,7 +134,7 @@ class PermissionSpec(google.protobuf.message.Message): NAME_FIELD_NUMBER: builtins.int PROJECT_FIELD_NUMBER: builtins.int TYPES_FIELD_NUMBER: builtins.int - NAME_PATTERN_FIELD_NUMBER: builtins.int + NAME_PATTERNS_FIELD_NUMBER: builtins.int REQUIRED_TAGS_FIELD_NUMBER: builtins.int ACTIONS_FIELD_NUMBER: builtins.int POLICY_FIELD_NUMBER: builtins.int @@ -145,7 +145,8 @@ class PermissionSpec(google.protobuf.message.Message): """Name of Feast project.""" @property def types(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___PermissionSpec.Type.ValueType]: ... - name_pattern: builtins.str + @property + def name_patterns(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... @property def required_tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... @property @@ -163,14 +164,14 @@ class PermissionSpec(google.protobuf.message.Message): name: builtins.str = ..., project: builtins.str = ..., types: collections.abc.Iterable[global___PermissionSpec.Type.ValueType] | None = ..., - name_pattern: builtins.str = ..., + name_patterns: collections.abc.Iterable[builtins.str] | None = ..., required_tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., actions: collections.abc.Iterable[global___PermissionSpec.AuthzedAction.ValueType] | None = ..., policy: feast.core.Policy_pb2.Policy | None = ..., tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["policy", b"policy"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["actions", b"actions", "name", b"name", "name_pattern", b"name_pattern", "policy", b"policy", "project", b"project", "required_tags", b"required_tags", "tags", b"tags", "types", b"types"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["actions", b"actions", "name", b"name", "name_patterns", b"name_patterns", "policy", b"policy", "project", b"project", "required_tags", b"required_tags", "tags", b"tags", "types", b"types"]) -> None: ... global___PermissionSpec = PermissionSpec diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index 0bed89ca16..a194b8ae26 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -1463,7 +1463,7 @@ def test_apply_permission_success(test_registry): and isinstance(permission.policy, RoleBasedPolicy) and len(permission.policy.roles) == 1 and permission.policy.roles[0] == "reader" - and permission.name_pattern is None + and permission.name_patterns == [] and permission.tags is None and permission.required_tags is None ) @@ -1481,7 +1481,7 @@ def test_apply_permission_success(test_registry): and isinstance(permission.policy, RoleBasedPolicy) and len(permission.policy.roles) == 1 and permission.policy.roles[0] == "reader" - and permission.name_pattern is None + and permission.name_patterns == [] and permission.tags is None and permission.required_tags is None ) @@ -1511,7 +1511,7 @@ def test_apply_permission_success(test_registry): and len(updated_permission.policy.roles) == 2 and "reader" in updated_permission.policy.roles and "writer" in updated_permission.policy.roles - and updated_permission.name_pattern is None + and updated_permission.name_patterns == [] and updated_permission.tags is None and updated_permission.required_tags is None ) @@ -1527,7 +1527,7 @@ def test_apply_permission_success(test_registry): actions=[AuthzedAction.DESCRIBE, AuthzedAction.WRITE_ONLINE], policy=RoleBasedPolicy(roles=["reader", "writer"]), types=FeatureView, - name_pattern="aaa", + name_patterns="aaa", tags={"team": "matchmaking"}, required_tags={"tag1": "tag1-value"}, ) @@ -1549,7 +1549,7 @@ def test_apply_permission_success(test_registry): and len(updated_permission.policy.roles) == 2 and "reader" in updated_permission.policy.roles and "writer" in updated_permission.policy.roles - and updated_permission.name_pattern == "aaa" + and updated_permission.name_patterns == ["aaa"] and "team" in updated_permission.tags and updated_permission.tags["team"] == "matchmaking" and updated_permission.required_tags["tag1"] == "tag1-value" diff --git a/sdk/python/tests/unit/permissions/conftest.py b/sdk/python/tests/unit/permissions/conftest.py index 6adbc6ec54..ba277d13b4 100644 --- a/sdk/python/tests/unit/permissions/conftest.py +++ b/sdk/python/tests/unit/permissions/conftest.py @@ -79,7 +79,7 @@ def security_manager() -> SecurityManager: Permission( name="special", types=FeatureView, - name_pattern="special.*", + name_patterns="special.*", policy=RoleBasedPolicy(roles=["special-reader"]), actions=[AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], ) diff --git a/sdk/python/tests/unit/permissions/test_permission.py b/sdk/python/tests/unit/permissions/test_permission.py index 606d750d81..8f1f2c46ba 100644 --- a/sdk/python/tests/unit/permissions/test_permission.py +++ b/sdk/python/tests/unit/permissions/test_permission.py @@ -11,9 +11,8 @@ from feast.feature_view import FeatureView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.action import ALL_ACTIONS, AuthzedAction -from feast.permissions.permission import ( - Permission, -) +from feast.permissions.matcher import _resource_name_matches_name_patterns +from feast.permissions.permission import Permission, _normalize_name_patterns from feast.permissions.policy import AllowAll, Policy from feast.saved_dataset import ValidationReference from feast.stream_feature_view import StreamFeatureView @@ -23,7 +22,7 @@ def test_defaults(): p = Permission(name="test") assertpy.assert_that(type(p.types)).is_equal_to(list) assertpy.assert_that(p.types).is_equal_to(ALL_RESOURCE_TYPES) - assertpy.assert_that(p.name_pattern).is_none() + assertpy.assert_that(p.name_patterns).is_equal_to([]) 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) @@ -66,6 +65,53 @@ def test_normalized_args(): assertpy.assert_that(type(p.actions)).is_equal_to(list) assertpy.assert_that(p.actions).is_equal_to([AuthzedAction.CREATE]) + p = Permission(name="test", name_patterns=None) + assertpy.assert_that(type(p.name_patterns)).is_equal_to(list) + assertpy.assert_that(p.name_patterns).is_equal_to([]) + + p = Permission(name="test", name_patterns="a_pattern") + assertpy.assert_that(type(p.name_patterns)).is_equal_to(list) + assertpy.assert_that(p.name_patterns).is_equal_to(["a_pattern"]) + + p = Permission(name="test", name_patterns=["pattern1", "pattern2"]) + assertpy.assert_that(type(p.name_patterns)).is_equal_to(list) + assertpy.assert_that(p.name_patterns).is_equal_to(["pattern1", "pattern2"]) + + p = Permission( + name="test", name_patterns=[" pattern1 ", " pattern2", "pattern3 "] + ) + assertpy.assert_that(type(p.name_patterns)).is_equal_to(list) + assertpy.assert_that(p.name_patterns).is_equal_to( + ["pattern1", "pattern2", "pattern3"] + ) + + +@pytest.mark.parametrize( + "name, patterns, result", + [ + (None, None, True), + (None, "", True), + (None, [], True), + (None, [""], True), + ("name", "name", True), + ("name", "another", False), + ("name", ".*me", True), + ("name", "^na.*", True), + ("123_must_start_by_number", r"^[\d].*", True), + ("name", ["invalid", "another_invalid"], False), + ("name", ["invalid", "name"], True), + ("name", ["name", "invalid"], True), + ("name", ["invalid", "another_invalid", "name"], True), + ("name", ["invalid", "name", "name"], True), + ], +) +def test_match_name_patterns(name, patterns, result): + assertpy.assert_that( + _resource_name_matches_name_patterns( + Permission(name=name), _normalize_name_patterns(patterns) + ) + ).is_equal_to(result) + @pytest.mark.parametrize( "resource, types, result", @@ -152,7 +198,7 @@ def test_match_resource_with_subclasses(resource, types, result): ], ) def test_resource_match_with_name_filter(pattern, name, match): - p = Permission(name="test", name_pattern=pattern) + p = Permission(name="test", name_patterns=pattern) for t in ALL_RESOURCE_TYPES: resource = Mock(spec=t) resource.name = name