-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathpermission.py
More file actions
280 lines (234 loc) · 9.66 KB
/
permission.py
File metadata and controls
280 lines (234 loc) · 9.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import logging
import re
from abc import ABC
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from google.protobuf.json_format import MessageToJson
from feast.importer import import_class
from feast.permissions.action import ALL_ACTIONS, AuthzedAction
from feast.permissions.matcher import actions_match_config, resource_match_config
from feast.permissions.policy import AllowAll, Policy
from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto
from feast.protos.feast.core.Permission_pb2 import PermissionMeta as PermissionMetaProto
from feast.protos.feast.core.Permission_pb2 import PermissionSpec as PermissionSpecProto
if TYPE_CHECKING:
from feast.feast_object import FeastObject
logger = logging.getLogger(__name__)
"""
Constant to refer to all the managed types.
"""
class Permission(ABC):
"""
The Permission class defines the authorization policy to be validated whenever the identified actions are
requested on the matching resources.
Attributes:
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_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.
tags: A dictionary of key-value pairs to store arbitrary metadata.
required_tags: Dictionary of key-value pairs that must match the resource tags. All these tags must
"""
_name: str
_types: list["FeastObject"]
_name_patterns: list[str]
_actions: list[AuthzedAction]
_policy: Policy
_tags: Dict[str, str]
_required_tags: dict[str, str]
created_timestamp: Optional[datetime]
last_updated_timestamp: Optional[datetime]
def __init__(
self,
name: str,
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,
required_tags: Optional[dict[str, str]] = None,
):
from feast.feast_object import ALL_RESOURCE_TYPES
if not types:
types = ALL_RESOURCE_TYPES
for t in types if isinstance(types, list) else [types]:
if t not in ALL_RESOURCE_TYPES:
raise ValueError(f"{t} is not one of the managed types")
if actions is None or not actions:
raise ValueError("The list 'actions' must be non-empty.")
if not policy:
raise ValueError("The list 'policy' must be non-empty.")
self._name = name
self._types = types if isinstance(types, list) else [types]
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)
self._required_tags = _normalize_tags(required_tags)
self.created_timestamp = None
self.last_updated_timestamp = None
def __eq__(self, other):
if not isinstance(other, Permission):
raise TypeError("Comparisons should only involve Permission class objects.")
if (
self.name != other.name
or self.name_patterns != other.name_patterns
or self.tags != other.tags
or self.policy != other.policy
or self.actions != other.actions
or self.required_tags != other.required_tags
):
return False
if set(self.types) != set(other.types):
return False
return True
def __hash__(self):
return hash(self.name)
def __str__(self):
return str(MessageToJson(self.to_proto()))
@property
def name(self) -> str:
return self._name
@property
def types(self) -> list["FeastObject"]:
return self._types
@property
def name_patterns(self) -> list[str]:
return self._name_patterns
@property
def actions(self) -> list[AuthzedAction]:
return self._actions
@property
def policy(self) -> Policy:
return self._policy
@property
def tags(self) -> Dict[str, str]:
return self._tags
@property
def required_tags(self) -> Dict[str, str]:
return self._required_tags
def match_resource(self, resource: "FeastObject") -> bool:
"""
Returns:
`True` when the given resource matches the type, name and tags filters defined in the permission.
"""
return resource_match_config(
resource=resource,
expected_types=self.types,
name_patterns=self.name_patterns,
required_tags=self.required_tags,
)
def match_actions(self, requested_actions: list[AuthzedAction]) -> bool:
"""
Returns:
`True` when the given actions are included in the permitted actions.
"""
return actions_match_config(
allowed_actions=self.actions,
requested_actions=requested_actions,
)
@staticmethod
def from_proto(permission_proto: PermissionProto) -> Any:
"""
Converts permission config in protobuf spec to a Permission class object.
Args:
permission_proto: A protobuf representation of a Permission.
Returns:
A Permission class object.
"""
types = [
get_type_class_from_permission_type(
_PERMISSION_TYPES[PermissionSpecProto.Type.Name(t)]
)
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
]
permission = Permission(
permission_proto.spec.name,
types,
name_patterns,
actions,
Policy.from_proto(permission_proto.spec.policy),
dict(permission_proto.spec.tags) or None,
dict(permission_proto.spec.required_tags) or None,
)
if permission_proto.meta.HasField("created_timestamp"):
permission.created_timestamp = (
permission_proto.meta.created_timestamp.ToDatetime()
)
if permission_proto.meta.HasField("last_updated_timestamp"):
permission.last_updated_timestamp = (
permission_proto.meta.last_updated_timestamp.ToDatetime()
)
return permission
def to_proto(self) -> PermissionProto:
"""
Converts a PermissionProto object to its protobuf representation.
"""
types = [
PermissionSpecProto.Type.Value(
re.sub(r"([a-z])([A-Z])", r"\1_\2", t.__name__).upper() # type: ignore[union-attr]
)
for t in self.types
]
actions = [
PermissionSpecProto.AuthzedAction.Value(action.name)
for action in self.actions
]
permission_spec = PermissionSpecProto(
name=self.name,
types=types,
name_patterns=self.name_patterns,
actions=actions,
policy=self.policy.to_proto(),
tags=self.tags,
required_tags=self.required_tags,
)
meta = PermissionMetaProto()
if self.created_timestamp:
meta.created_timestamp.FromDatetime(self.created_timestamp)
if self.last_updated_timestamp:
meta.last_updated_timestamp.FromDatetime(self.last_updated_timestamp)
return PermissionProto(spec=permission_spec, meta=meta)
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]]):
if tags:
return {
k.strip(): v.strip() if isinstance(v, str) else v for k, v in tags.items()
}
return None
def get_type_class_from_permission_type(permission_type: str):
module_name, config_class_name = permission_type.rsplit(".", 1)
return import_class(module_name, config_class_name)
_PERMISSION_TYPES = {
"PROJECT": "feast.project.Project",
"FEATURE_VIEW": "feast.feature_view.FeatureView",
"ON_DEMAND_FEATURE_VIEW": "feast.on_demand_feature_view.OnDemandFeatureView",
"BATCH_FEATURE_VIEW": "feast.batch_feature_view.BatchFeatureView",
"STREAM_FEATURE_VIEW": "feast.stream_feature_view.StreamFeatureView",
"ENTITY": "feast.entity.Entity",
"FEATURE_SERVICE": "feast.feature_service.FeatureService",
"DATA_SOURCE": "feast.data_source.DataSource",
"VALIDATION_REFERENCE": "feast.saved_dataset.ValidationReference",
"SAVED_DATASET": "feast.saved_dataset.SavedDataset",
"PERMISSION": "feast.permissions.permission.Permission",
}