12 files changed
@@ -36,7 +36,7 @@ The permission model is based on the following components: | |||
| 36 | 36 | The `Permission` class identifies a single permission configured on the feature store and is identified by these attributes: | |
| 37 | 37 | - `name`: The permission name. | |
| 38 | 38 | - `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. | ||
| 40 | 40 | - `required_tags`: Dictionary of key-value pairs that must match the resource tags. Defaults to `None`, meaning that no tags filtering is applied. | |
| 41 | 41 | - `actions`: The actions authorized by this permission. Defaults to `ALL_VALUES`, an alias defined in the `action` module. | |
| 42 | 42 | - `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 | |||
| 95 | 95 | Permission( | |
| 96 | 96 | name="reader", | |
| 97 | 97 | types=[FeatureView], | |
| 98 | - name_pattern=".*risky.*", | ||
| 98 | + name_patterns=".*risky.*", # Accepts both `str` or `list[str]` types | ||
| 99 | 99 | policy=RoleBasedPolicy(roles=["trusted"]), | |
| 100 | 100 | actions=[AuthzedAction.READ_OFFLINE], | |
| 101 | 101 | ) | |
@@ -172,9 +172,10 @@ Options: | |||
| 172 | 172 | ||
| 173 | 173 | ```text | |
| 174 | 174 | +-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ | |
| 175 | - | NAME | TYPES | NAME_PATTERN | ACTIONS | ROLES | REQUIRED_TAGS | | ||
| 175 | + | NAME | TYPES | NAME_PATTERNS | ACTIONS | ROLES | REQUIRED_TAGS | | ||
| 176 | 176 | +=======================+=============+=======================+===========+================+================+========+ | |
| 177 | 177 | | reader_permission1234 | FeatureView | transformed_conv_rate | DESCRIBE | reader | - | | |
| 178 | + | | | driver_hourly_stats | DESCRIBE | reader | - | | ||
| 178 | 179 | +-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ | |
| 179 | 180 | | writer_permission1234 | FeatureView | transformed_conv_rate | CREATE | writer | - | | |
| 180 | 181 | +-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ | |
@@ -50,7 +50,7 @@ message PermissionSpec { | |||
| 50 | 50 | ||
| 51 | 51 | repeated Type types = 3; | |
| 52 | 52 | ||
| 53 | - string name_pattern = 4; | ||
| 53 | + repeated string name_patterns = 4; | ||
| 54 | 54 | ||
| 55 | 55 | map<string, string> required_tags = 5; | |
| 56 | 56 | ||
@@ -1211,7 +1211,7 @@ def feast_permissions_list_command(ctx: click.Context, verbose: bool, tags: list | |||
| 1211 | 1211 | headers=[ | |
| 1212 | 1212 | "NAME", | |
| 1213 | 1213 | "TYPES", | |
| 1214 | - "NAME_PATTERN", | ||
| 1214 | + "NAME_PATTERNS", | ||
| 1215 | 1215 | "ACTIONS", | |
| 1216 | 1216 | "ROLES", | |
| 1217 | 1217 | "REQUIRED_TAGS", | |
@@ -196,7 +196,7 @@ def handle_not_verbose_permissions_command( | |||
| 196 | 196 | [ | |
| 197 | 197 | p.name, | |
| 198 | 198 | _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), | ||
| 200 | 200 | _to_multi_line([a.value.upper() for a in p.actions]), | |
| 201 | 201 | _to_multi_line(sorted(roles)), | |
| 202 | 202 | _dict_to_multi_line(p.required_tags), | |
@@ -44,7 +44,7 @@ def _get_type(resource: "FeastObject") -> Any: | |||
| 44 | 44 | def resource_match_config( | |
| 45 | 45 | resource: "FeastObject", | |
| 46 | 46 | expected_types: list["FeastObject"], | |
| 47 | - name_pattern: Optional[str] = None, | ||
| 47 | + name_patterns: list[str], | ||
| 48 | 48 | required_tags: Optional[dict[str, str]] = None, | |
| 49 | 49 | ) -> bool: | |
| 50 | 50 | """ | |
@@ -53,7 +53,7 @@ def resource_match_config( | |||
| 53 | 53 | Args: | |
| 54 | 54 | resource: A FeastObject instance to match agains the permission. | |
| 55 | 55 | 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. | ||
| 57 | 57 | required_tags: The optional dictionary of required tags configured in the permission. | |
| 58 | 58 | ||
| 59 | 59 | Returns: | |
@@ -75,21 +75,8 @@ def resource_match_config( | |||
| 75 | 75 | ) | |
| 76 | 76 | return False | |
| 77 | 77 | ||
| 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 | ||
| 93 | 80 | ||
| 94 | 81 | if required_tags: | |
| 95 | 82 | if hasattr(resource, "required_tags"): | |
@@ -112,6 +99,39 @@ def resource_match_config( | |||
| 112 | 99 | return True | |
| 113 | 100 | ||
| 114 | 101 | ||
| 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 | + | ||
| 115 | 135 | def actions_match_config( | |
| 116 | 136 | requested_actions: list[AuthzedAction], | |
| 117 | 137 | allowed_actions: list[AuthzedAction], | |
@@ -33,7 +33,7 @@ class Permission(ABC): | |||
| 33 | 33 | name: The permission name (can be duplicated, used for logging troubleshooting). | |
| 34 | 34 | types: The list of protected resource types as defined by the `FeastObject` type. The match includes all the sub-classes of the given types. | |
| 35 | 35 | 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 | ||
| 37 | 37 | be present in a resource tags with the given value. Defaults to None, meaning that no tags filtering is applied. | |
| 38 | 38 | actions: The actions authorized by this permission. Defaults to `ALL_ACTIONS`. | |
| 39 | 39 | policy: The policy to be applied to validate a client request. | |
@@ -43,7 +43,7 @@ class Permission(ABC): | |||
| 43 | 43 | ||
| 44 | 44 | _name: str | |
| 45 | 45 | _types: list["FeastObject"] | |
| 46 | - _name_pattern: Optional[str] | ||
| 46 | + _name_patterns: list[str] | ||
| 47 | 47 | _actions: list[AuthzedAction] | |
| 48 | 48 | _policy: Policy | |
| 49 | 49 | _tags: Dict[str, str] | |
@@ -54,8 +54,8 @@ class Permission(ABC): | |||
| 54 | 54 | def __init__( | |
| 55 | 55 | self, | |
| 56 | 56 | 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]]] = [], | ||
| 59 | 59 | actions: Union[list[AuthzedAction], AuthzedAction] = ALL_ACTIONS, | |
| 60 | 60 | policy: Policy = AllowAll, | |
| 61 | 61 | tags: Optional[dict[str, str]] = None, | |
@@ -74,7 +74,7 @@ def __init__( | |||
| 74 | 74 | raise ValueError("The list 'policy' must be non-empty.") | |
| 75 | 75 | self._name = name | |
| 76 | 76 | 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) | ||
| 78 | 78 | self._actions = actions if isinstance(actions, list) else [actions] | |
| 79 | 79 | self._policy = policy | |
| 80 | 80 | self._tags = _normalize_tags(tags) | |
@@ -88,7 +88,7 @@ def __eq__(self, other): | |||
| 88 | 88 | ||
| 89 | 89 | if ( | |
| 90 | 90 | self.name != other.name | |
| 91 | - or self.name_pattern != other.name_pattern | ||
| 91 | + or self.name_patterns != other.name_patterns | ||
| 92 | 92 | or self.tags != other.tags | |
| 93 | 93 | or self.policy != other.policy | |
| 94 | 94 | or self.actions != other.actions | |
@@ -116,8 +116,8 @@ def types(self) -> list["FeastObject"]: | |||
| 116 | 116 | return self._types | |
| 117 | 117 | ||
| 118 | 118 | @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 | ||
| 121 | 121 | ||
| 122 | 122 | @property | |
| 123 | 123 | def actions(self) -> list[AuthzedAction]: | |
@@ -143,7 +143,7 @@ def match_resource(self, resource: "FeastObject") -> bool: | |||
| 143 | 143 | return resource_match_config( | |
| 144 | 144 | resource=resource, | |
| 145 | 145 | expected_types=self.types, | |
| 146 | - name_pattern=self.name_pattern, | ||
| 146 | + name_patterns=self.name_patterns, | ||
| 147 | 147 | required_tags=self.required_tags, | |
| 148 | 148 | ) | |
| 149 | 149 | ||
@@ -175,6 +175,9 @@ def from_proto(permission_proto: PermissionProto) -> Any: | |||
| 175 | 175 | ) | |
| 176 | 176 | for t in permission_proto.spec.types | |
| 177 | 177 | ] | |
| 178 | + name_patterns = [ | ||
| 179 | + name_pattern for name_pattern in permission_proto.spec.name_patterns | ||
| 180 | + ] | ||
| 178 | 181 | actions = [ | |
| 179 | 182 | AuthzedAction[PermissionSpecProto.AuthzedAction.Name(action)] | |
| 180 | 183 | for action in permission_proto.spec.actions | |
@@ -183,7 +186,7 @@ def from_proto(permission_proto: PermissionProto) -> Any: | |||
| 183 | 186 | permission = Permission( | |
| 184 | 187 | permission_proto.spec.name, | |
| 185 | 188 | types, | |
| 186 | - permission_proto.spec.name_pattern or None, | ||
| 189 | + name_patterns, | ||
| 187 | 190 | actions, | |
| 188 | 191 | Policy.from_proto(permission_proto.spec.policy), | |
| 189 | 192 | dict(permission_proto.spec.tags) or None, | |
@@ -220,7 +223,7 @@ def to_proto(self) -> PermissionProto: | |||
| 220 | 223 | permission_spec = PermissionSpecProto( | |
| 221 | 224 | name=self.name, | |
| 222 | 225 | types=types, | |
| 223 | - name_pattern=self.name_pattern if self.name_pattern is not None else "", | ||
| 226 | + name_patterns=self.name_patterns, | ||
| 224 | 227 | actions=actions, | |
| 225 | 228 | policy=self.policy.to_proto(), | |
| 226 | 229 | tags=self.tags, | |
@@ -236,10 +239,17 @@ def to_proto(self) -> PermissionProto: | |||
| 236 | 239 | return PermissionProto(spec=permission_spec, meta=meta) | |
| 237 | 240 | ||
| 238 | 241 | ||
| 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 | ||
| 243 | 253 | ||
| 244 | 254 | ||
| 245 | 255 | def _normalize_tags(tags: Optional[dict[str, str]]): | |
@@ -134,7 +134,7 @@ class PermissionSpec(google.protobuf.message.Message): | |||
| 134 | 134 | NAME_FIELD_NUMBER: builtins.int | |
| 135 | 135 | PROJECT_FIELD_NUMBER: builtins.int | |
| 136 | 136 | TYPES_FIELD_NUMBER: builtins.int | |
| 137 | - NAME_PATTERN_FIELD_NUMBER: builtins.int | ||
| 137 | + NAME_PATTERNS_FIELD_NUMBER: builtins.int | ||
| 138 | 138 | REQUIRED_TAGS_FIELD_NUMBER: builtins.int | |
| 139 | 139 | ACTIONS_FIELD_NUMBER: builtins.int | |
| 140 | 140 | POLICY_FIELD_NUMBER: builtins.int | |
@@ -145,7 +145,8 @@ class PermissionSpec(google.protobuf.message.Message): | |||
| 145 | 145 | """Name of Feast project.""" | |
| 146 | 146 | @property | |
| 147 | 147 | 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]: ... | ||
| 149 | 150 | @property | |
| 150 | 151 | def required_tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... | |
| 151 | 152 | @property | |
@@ -163,14 +164,14 @@ class PermissionSpec(google.protobuf.message.Message): | |||
| 163 | 164 | name: builtins.str = ..., | |
| 164 | 165 | project: builtins.str = ..., | |
| 165 | 166 | types: collections.abc.Iterable[global___PermissionSpec.Type.ValueType] | None = ..., | |
| 166 | - name_pattern: builtins.str = ..., | ||
| 167 | + name_patterns: collections.abc.Iterable[builtins.str] | None = ..., | ||
| 167 | 168 | required_tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., | |
| 168 | 169 | actions: collections.abc.Iterable[global___PermissionSpec.AuthzedAction.ValueType] | None = ..., | |
| 169 | 170 | policy: feast.core.Policy_pb2.Policy | None = ..., | |
| 170 | 171 | tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., | |
| 171 | 172 | ) -> None: ... | |
| 172 | 173 | 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: ... | ||
| 174 | 175 | ||
| 175 | 176 | global___PermissionSpec = PermissionSpec | |
| 176 | 177 | ||
0 commit comments