← 返回首页
fix: Added support for multiple name patterns to Permissions (#4633) · lokeshrangineni/feast@f05e928 · GitHub
Skip to content

Navigation Menu

Toggle navigation
Sign in
Appearance settings
Search or jump to...

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Include my email address so I can be contacted

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Resetting focus

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

12 files changed

Lines changed: 143 additions & 65 deletions

File tree

‎docs/getting-started/concepts/permission.md‎

Lines changed: 2 additions & 2 deletions
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‎

Lines changed: 2 additions & 1 deletion
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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 37 additions & 17 deletions
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‎

Lines changed: 25 additions & 15 deletions
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‎

Lines changed: 12 additions & 12 deletions
Load diffSome 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‎

Lines changed: 5 additions & 4 deletions
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)

Footer

© 2026 GitHub, Inc.