Skip to content

Commit f05e928

Browse files
authored
fix: Added support for multiple name patterns to Permissions (feast-dev#4633)
* added support for multiple name patterns to Permissions Signed-off-by: Daniele Martinoli <dmartino@redhat.com> * fixed lint issues Signed-off-by: Daniele Martinoli <dmartino@redhat.com> --------- Signed-off-by: Daniele Martinoli <dmartino@redhat.com>
1 parent 95fe8c2 commit f05e928

File tree

12 files changed

+143
-65
lines changed

12 files changed

+143
-65
lines changed

docs/getting-started/concepts/permission.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ The permission model is based on the following components:
3636
The `Permission` class identifies a single permission configured on the feature store and is identified by these attributes:
3737
- `name`: The permission name.
3838
- `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.
39-
- `name_pattern`: A regex to match the resource name. Defaults to `None`, meaning that no name filtering is applied
39+
- `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.
4040
- `required_tags`: Dictionary of key-value pairs that must match the resource tags. Defaults to `None`, meaning that no tags filtering is applied.
4141
- `actions`: The actions authorized by this permission. Defaults to `ALL_VALUES`, an alias defined in the `action` module.
4242
- `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
9595
Permission(
9696
name="reader",
9797
types=[FeatureView],
98-
name_pattern=".*risky.*",
98+
name_patterns=".*risky.*", # Accepts both `str` or `list[str]` types
9999
policy=RoleBasedPolicy(roles=["trusted"]),
100100
actions=[AuthzedAction.READ_OFFLINE],
101101
)

docs/reference/feast-cli-commands.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,10 @@ Options:
172172

173173
```text
174174
+-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+
175-
| NAME | TYPES | NAME_PATTERN | ACTIONS | ROLES | REQUIRED_TAGS |
175+
| NAME | TYPES | NAME_PATTERNS | ACTIONS | ROLES | REQUIRED_TAGS |
176176
+=======================+=============+=======================+===========+================+================+========+
177177
| reader_permission1234 | FeatureView | transformed_conv_rate | DESCRIBE | reader | - |
178+
| | | driver_hourly_stats | DESCRIBE | reader | - |
178179
+-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+
179180
| writer_permission1234 | FeatureView | transformed_conv_rate | CREATE | writer | - |
180181
+-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+

protos/feast/core/Permission.proto

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ message PermissionSpec {
5050

5151
repeated Type types = 3;
5252

53-
string name_pattern = 4;
53+
repeated string name_patterns = 4;
5454

5555
map<string, string> required_tags = 5;
5656

sdk/python/feast/cli.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1211,7 +1211,7 @@ def feast_permissions_list_command(ctx: click.Context, verbose: bool, tags: list
12111211
headers=[
12121212
"NAME",
12131213
"TYPES",
1214-
"NAME_PATTERN",
1214+
"NAME_PATTERNS",
12151215
"ACTIONS",
12161216
"ROLES",
12171217
"REQUIRED_TAGS",

sdk/python/feast/cli_utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def handle_not_verbose_permissions_command(
196196
[
197197
p.name,
198198
_to_multi_line([t.__name__ for t in p.types]), # type: ignore[union-attr, attr-defined]
199-
p.name_pattern,
199+
_to_multi_line(p.name_patterns),
200200
_to_multi_line([a.value.upper() for a in p.actions]),
201201
_to_multi_line(sorted(roles)),
202202
_dict_to_multi_line(p.required_tags),

sdk/python/feast/permissions/matcher.py

+37-17
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def _get_type(resource: "FeastObject") -> Any:
4444
def resource_match_config(
4545
resource: "FeastObject",
4646
expected_types: list["FeastObject"],
47-
name_pattern: Optional[str] = None,
47+
name_patterns: list[str],
4848
required_tags: Optional[dict[str, str]] = None,
4949
) -> bool:
5050
"""
@@ -53,7 +53,7 @@ def resource_match_config(
5353
Args:
5454
resource: A FeastObject instance to match agains the permission.
5555
expected_types: The list of object types configured in the permission. Type match also includes all the sub-classes.
56-
name_pattern: The optional name pattern filter configured in the permission.
56+
name_patterns: The possibly empty list of name pattern filters configured in the permission.
5757
required_tags: The optional dictionary of required tags configured in the permission.
5858
5959
Returns:
@@ -75,21 +75,8 @@ def resource_match_config(
7575
)
7676
return False
7777

78-
if name_pattern is not None:
79-
if hasattr(resource, "name"):
80-
if isinstance(resource.name, str):
81-
match = bool(re.fullmatch(name_pattern, resource.name))
82-
if not match:
83-
logger.info(
84-
f"Resource name {resource.name} does not match pattern {name_pattern}"
85-
)
86-
return False
87-
else:
88-
logger.warning(
89-
f"Resource {resource} has no `name` attribute of unexpected type {type(resource.name)}"
90-
)
91-
else:
92-
logger.warning(f"Resource {resource} has no `name` attribute")
78+
if not _resource_name_matches_name_patterns(resource, name_patterns):
79+
return False
9380

9481
if required_tags:
9582
if hasattr(resource, "required_tags"):
@@ -112,6 +99,39 @@ def resource_match_config(
11299
return True
113100

114101

102+
def _resource_name_matches_name_patterns(
103+
resource: "FeastObject",
104+
name_patterns: list[str],
105+
) -> bool:
106+
if not hasattr(resource, "name"):
107+
logger.warning(f"Resource {resource} has no `name` attribute")
108+
return True
109+
110+
if not name_patterns:
111+
return True
112+
113+
if resource.name is None:
114+
return True
115+
116+
if not isinstance(resource.name, str):
117+
logger.warning(
118+
f"Resource {resource} has `name` attribute of unexpected type {type(resource.name)}"
119+
)
120+
return True
121+
122+
for name_pattern in name_patterns:
123+
match = bool(re.fullmatch(name_pattern, resource.name))
124+
if not match:
125+
logger.info(
126+
f"Resource name {resource.name} does not match pattern {name_pattern}"
127+
)
128+
else:
129+
logger.info(f"Resource name {resource.name} matched pattern {name_pattern}")
130+
return True
131+
132+
return False
133+
134+
115135
def actions_match_config(
116136
requested_actions: list[AuthzedAction],
117137
allowed_actions: list[AuthzedAction],

sdk/python/feast/permissions/permission.py

+25-15
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Permission(ABC):
3333
name: The permission name (can be duplicated, used for logging troubleshooting).
3434
types: The list of protected resource types as defined by the `FeastObject` type. The match includes all the sub-classes of the given types.
3535
Defaults to all managed types (e.g. the `ALL_RESOURCE_TYPES` constant)
36-
name_pattern: A regex to match the resource name. Defaults to None, meaning that no name filtering is applied
36+
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
3737
be present in a resource tags with the given value. Defaults to None, meaning that no tags filtering is applied.
3838
actions: The actions authorized by this permission. Defaults to `ALL_ACTIONS`.
3939
policy: The policy to be applied to validate a client request.
@@ -43,7 +43,7 @@ class Permission(ABC):
4343

4444
_name: str
4545
_types: list["FeastObject"]
46-
_name_pattern: Optional[str]
46+
_name_patterns: list[str]
4747
_actions: list[AuthzedAction]
4848
_policy: Policy
4949
_tags: Dict[str, str]
@@ -54,8 +54,8 @@ class Permission(ABC):
5454
def __init__(
5555
self,
5656
name: str,
57-
types: Optional[Union[list["FeastObject"], "FeastObject"]] = None,
58-
name_pattern: Optional[str] = None,
57+
types: Optional[Union[list["FeastObject"], "FeastObject"]] = [],
58+
name_patterns: Optional[Union[str, list[str]]] = [],
5959
actions: Union[list[AuthzedAction], AuthzedAction] = ALL_ACTIONS,
6060
policy: Policy = AllowAll,
6161
tags: Optional[dict[str, str]] = None,
@@ -74,7 +74,7 @@ def __init__(
7474
raise ValueError("The list 'policy' must be non-empty.")
7575
self._name = name
7676
self._types = types if isinstance(types, list) else [types]
77-
self._name_pattern = _normalize_name_pattern(name_pattern)
77+
self._name_patterns = _normalize_name_patterns(name_patterns)
7878
self._actions = actions if isinstance(actions, list) else [actions]
7979
self._policy = policy
8080
self._tags = _normalize_tags(tags)
@@ -88,7 +88,7 @@ def __eq__(self, other):
8888

8989
if (
9090
self.name != other.name
91-
or self.name_pattern != other.name_pattern
91+
or self.name_patterns != other.name_patterns
9292
or self.tags != other.tags
9393
or self.policy != other.policy
9494
or self.actions != other.actions
@@ -116,8 +116,8 @@ def types(self) -> list["FeastObject"]:
116116
return self._types
117117

118118
@property
119-
def name_pattern(self) -> Optional[str]:
120-
return self._name_pattern
119+
def name_patterns(self) -> list[str]:
120+
return self._name_patterns
121121

122122
@property
123123
def actions(self) -> list[AuthzedAction]:
@@ -143,7 +143,7 @@ def match_resource(self, resource: "FeastObject") -> bool:
143143
return resource_match_config(
144144
resource=resource,
145145
expected_types=self.types,
146-
name_pattern=self.name_pattern,
146+
name_patterns=self.name_patterns,
147147
required_tags=self.required_tags,
148148
)
149149

@@ -175,6 +175,9 @@ def from_proto(permission_proto: PermissionProto) -> Any:
175175
)
176176
for t in permission_proto.spec.types
177177
]
178+
name_patterns = [
179+
name_pattern for name_pattern in permission_proto.spec.name_patterns
180+
]
178181
actions = [
179182
AuthzedAction[PermissionSpecProto.AuthzedAction.Name(action)]
180183
for action in permission_proto.spec.actions
@@ -183,7 +186,7 @@ def from_proto(permission_proto: PermissionProto) -> Any:
183186
permission = Permission(
184187
permission_proto.spec.name,
185188
types,
186-
permission_proto.spec.name_pattern or None,
189+
name_patterns,
187190
actions,
188191
Policy.from_proto(permission_proto.spec.policy),
189192
dict(permission_proto.spec.tags) or None,
@@ -220,7 +223,7 @@ def to_proto(self) -> PermissionProto:
220223
permission_spec = PermissionSpecProto(
221224
name=self.name,
222225
types=types,
223-
name_pattern=self.name_pattern if self.name_pattern is not None else "",
226+
name_patterns=self.name_patterns,
224227
actions=actions,
225228
policy=self.policy.to_proto(),
226229
tags=self.tags,
@@ -236,10 +239,17 @@ def to_proto(self) -> PermissionProto:
236239
return PermissionProto(spec=permission_spec, meta=meta)
237240

238241

239-
def _normalize_name_pattern(name_pattern: Optional[str]):
240-
if name_pattern is not None:
241-
return name_pattern.strip()
242-
return None
242+
def _normalize_name_patterns(
243+
name_patterns: Optional[Union[str, list[str]]],
244+
) -> list[str]:
245+
if name_patterns is None:
246+
return []
247+
if isinstance(name_patterns, str):
248+
return _normalize_name_patterns([name_patterns])
249+
normalized_name_patterns = []
250+
for name_pattern in name_patterns:
251+
normalized_name_patterns.append(name_pattern.strip())
252+
return normalized_name_patterns
243253

244254

245255
def _normalize_tags(tags: Optional[dict[str, str]]):

sdk/python/feast/protos/feast/core/Permission_pb2.py

+12-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/python/feast/protos/feast/core/Permission_pb2.pyi

+5-4
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class PermissionSpec(google.protobuf.message.Message):
134134
NAME_FIELD_NUMBER: builtins.int
135135
PROJECT_FIELD_NUMBER: builtins.int
136136
TYPES_FIELD_NUMBER: builtins.int
137-
NAME_PATTERN_FIELD_NUMBER: builtins.int
137+
NAME_PATTERNS_FIELD_NUMBER: builtins.int
138138
REQUIRED_TAGS_FIELD_NUMBER: builtins.int
139139
ACTIONS_FIELD_NUMBER: builtins.int
140140
POLICY_FIELD_NUMBER: builtins.int
@@ -145,7 +145,8 @@ class PermissionSpec(google.protobuf.message.Message):
145145
"""Name of Feast project."""
146146
@property
147147
def types(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___PermissionSpec.Type.ValueType]: ...
148-
name_pattern: builtins.str
148+
@property
149+
def name_patterns(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ...
149150
@property
150151
def required_tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ...
151152
@property
@@ -163,14 +164,14 @@ class PermissionSpec(google.protobuf.message.Message):
163164
name: builtins.str = ...,
164165
project: builtins.str = ...,
165166
types: collections.abc.Iterable[global___PermissionSpec.Type.ValueType] | None = ...,
166-
name_pattern: builtins.str = ...,
167+
name_patterns: collections.abc.Iterable[builtins.str] | None = ...,
167168
required_tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ...,
168169
actions: collections.abc.Iterable[global___PermissionSpec.AuthzedAction.ValueType] | None = ...,
169170
policy: feast.core.Policy_pb2.Policy | None = ...,
170171
tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ...,
171172
) -> None: ...
172173
def HasField(self, field_name: typing_extensions.Literal["policy", b"policy"]) -> builtins.bool: ...
173-
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: ...
174+
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: ...
174175

175176
global___PermissionSpec = PermissionSpec
176177

0 commit comments

Comments
 (0)