@@ -79,6 +79,29 @@ def _role_matches(rule: dict[str, Any], roles: set[str]) -> bool:
7979 return any (role in roles for role in rule .get ("roles" , []))
8080
8181
82+ def _path_prefix_matches (prefix : str , path : str ) -> bool :
83+ normalized_prefix = prefix .rstrip ("/" ) or "/"
84+ normalized_path = path .rstrip ("/" ) or "/"
85+ return (
86+ normalized_path == normalized_prefix
87+ or normalized_path .startswith (f"{ normalized_prefix } /" )
88+ )
89+
90+
91+ def _path_allowed_by_rule (access_control : dict [str , Any ], path : str , roles : set [str ]) -> bool :
92+ for rule in access_control .get ("rules" , []):
93+ if not _method_matches (rule , "GET" ) or not _role_matches (rule , roles ):
94+ continue
95+ path_prefix = rule .get ("path_prefix" )
96+ if (
97+ isinstance (path_prefix , str )
98+ and path_prefix
99+ and _path_prefix_matches (path_prefix , path )
100+ ):
101+ return True
102+ return False
103+
104+
82105def _filtered_collection (path : str , access_control : dict [str , Any ]) -> dict [str , Any ] | None :
83106 normalized_path = path .rstrip ("/" ) or "/"
84107 for collection in access_control .get ("filtered_collections" , []):
@@ -95,7 +118,6 @@ def _allowed_ids_for_template(
95118) -> tuple [bool , set [str ]]:
96119 allowed_ids : set [str ] = set ()
97120 allow_all = False
98-
99121 for rule in access_control .get ("resource_rules" , []):
100122 if not _method_matches (rule , "GET" ) or not _role_matches (rule , roles ):
101123 continue
@@ -114,6 +136,10 @@ def _to_path_id(identifier: str) -> str:
114136 return base64 .urlsafe_b64encode (identifier .encode ("utf-8" )).decode ("ascii" ).rstrip ("=" )
115137
116138
139+ def _id_allowed (identifier : str , allow_all : bool , allowed_ids : set [str ]) -> bool :
140+ return allow_all or identifier in allowed_ids or _to_path_id (identifier ) in allowed_ids
141+
142+
117143def _item_allowed (item : Any , allow_all : bool , allowed_ids : set [str ]) -> bool :
118144 if allow_all :
119145 return True
@@ -124,7 +150,114 @@ def _item_allowed(item: Any, allow_all: bool, allowed_ids: set[str]) -> bool:
124150 if not isinstance (item_id , str ) or not item_id :
125151 return False
126152
127- return item_id in allowed_ids or _to_path_id (item_id ) in allowed_ids
153+ return _id_allowed (item_id , allow_all , allowed_ids )
154+
155+
156+ def _submodel_reference_id (reference : Any ) -> str | None :
157+ if not isinstance (reference , dict ):
158+ return None
159+
160+ keys = reference .get ("keys" )
161+ if not isinstance (keys , list ):
162+ return None
163+
164+ for key in reversed (keys ):
165+ if not isinstance (key , dict ):
166+ continue
167+ key_type = key .get ("type" )
168+ value = key .get ("value" )
169+ if (
170+ isinstance (key_type , str )
171+ and key_type .lower () == "submodel"
172+ and isinstance (value , str )
173+ and value
174+ ):
175+ return value
176+
177+ return None
178+
179+
180+ def _submodel_reference_allowed (reference : Any , allow_all : bool , allowed_ids : set [str ]) -> bool :
181+ if allow_all :
182+ return True
183+
184+ submodel_id = _submodel_reference_id (reference )
185+ if not submodel_id :
186+ return False
187+
188+ return _id_allowed (submodel_id , allow_all , allowed_ids )
189+
190+
191+ def _configured_submodel_path_templates (access_control : dict [str , Any ]) -> set [str ]:
192+ path_templates = access_control .get ("submodel_reference_path_templates" , [])
193+ if not isinstance (path_templates , list ):
194+ return set ()
195+ return {template for template in path_templates if isinstance (template , str )}
196+
197+
198+ def _template_targets_submodel (path_template : Any ) -> bool :
199+ return isinstance (path_template , str ) and "{submodel" in path_template .lower ()
200+
201+
202+ def _rule_targets_submodel (rule : dict [str , Any ], configured_templates : set [str ]) -> bool :
203+ path_templates = rule .get ("path_templates" , [])
204+ if not isinstance (path_templates , list ):
205+ return False
206+ if configured_templates :
207+ return any (template in configured_templates for template in path_templates )
208+ return any (_template_targets_submodel (template ) for template in path_templates )
209+
210+
211+ def _resource_rule_submodel_access (
212+ access_control : dict [str , Any ],
213+ roles : set [str ],
214+ ) -> tuple [bool , set [str ]]:
215+ allowed_ids : set [str ] = set ()
216+ allow_all = False
217+ configured_templates = _configured_submodel_path_templates (access_control )
218+
219+ for rule in access_control .get ("resource_rules" , []):
220+ if not _method_matches (rule , "GET" ) or not _role_matches (rule , roles ):
221+ continue
222+ if not _rule_targets_submodel (rule , configured_templates ):
223+ continue
224+
225+ ids = set (rule .get ("ids" , []))
226+ if "*" in ids :
227+ allow_all = True
228+ allowed_ids .update (ids )
229+
230+ return allow_all , allowed_ids
231+
232+
233+ def _submodel_reference_access (
234+ access_control : dict [str , Any ],
235+ roles : set [str ],
236+ request_path : str ,
237+ ) -> tuple [bool , set [str ]]:
238+ allow_all , allowed_ids = _resource_rule_submodel_access (access_control , roles )
239+ if allow_all or allowed_ids :
240+ return allow_all , allowed_ids
241+ if _path_allowed_by_rule (access_control , request_path , roles ):
242+ return True , set ()
243+ return False , set ()
244+
245+
246+ def _has_submodel_references (item : Any ) -> bool :
247+ return isinstance (item , dict ) and isinstance (item .get ("submodels" ), list )
248+
249+
250+ def _filter_aas_submodel_references (item : Any , allow_all : bool , allowed_ids : set [str ]) -> Any :
251+ if not _has_submodel_references (item ):
252+ return item
253+
254+ filtered_item = deepcopy (item )
255+ filtered_item ["submodels" ] = [
256+ reference
257+ for reference in item ["submodels" ]
258+ if _submodel_reference_allowed (reference , allow_all , allowed_ids )
259+ ]
260+ return filtered_item
128261
129262
130263def _request_query_without_paging () -> list [tuple [str , str ]]:
@@ -285,8 +418,14 @@ def _handle_filtered_collection(path: str, collection: dict[str, Any], access_co
285418 collection .get ("item_path_template" , "" ),
286419 roles ,
287420 )
421+ request_path = "/" + path .strip ("/" )
422+ submodel_allow_all , submodel_allowed_ids = _submodel_reference_access (
423+ access_control ,
424+ roles ,
425+ request_path ,
426+ )
288427 filtered_items = [
289- item
428+ _filter_aas_submodel_references ( item , submodel_allow_all , submodel_allowed_ids )
290429 for item in items
291430 if _item_allowed (item , allow_all , allowed_ids )
292431 ]
@@ -295,14 +434,36 @@ def _handle_filtered_collection(path: str, collection: dict[str, Any], access_co
295434 return jsonify (payload ), status_code
296435
297436
298- def _proxy_request (path : str ) -> Response :
437+ def _proxy_request (path : str , access_control : dict [ str , Any ] ) -> Response :
299438 upstream_response = requests .request (
300439 request .method ,
301440 _upstream_url (path , list (request .args .items (multi = True ))),
302441 headers = _forward_headers (),
303442 data = request .get_data (),
304443 timeout = 30 ,
305444 )
445+
446+ if request .method == "GET" and upstream_response .status_code < 400 :
447+ try :
448+ payload = upstream_response .json ()
449+ except ValueError :
450+ pass
451+ else :
452+ if _has_submodel_references (payload ):
453+ roles = _token_roles ()
454+ request_path = "/" + path .strip ("/" )
455+ submodel_allow_all , submodel_allowed_ids = _submodel_reference_access (
456+ access_control ,
457+ roles ,
458+ request_path ,
459+ )
460+ filtered_payload = _filter_aas_submodel_references (
461+ payload ,
462+ submodel_allow_all ,
463+ submodel_allowed_ids ,
464+ )
465+ return jsonify (filtered_payload ), upstream_response .status_code
466+
306467 return Response (
307468 upstream_response .content ,
308469 status = upstream_response .status_code ,
@@ -317,18 +478,19 @@ def repository_proxy(path: str) -> Response:
317478 normalized_path = f"/{ path .strip ('/' )} "
318479 collection = _filtered_collection (normalized_path , access_control )
319480
320- if request .method == "GET" and collection :
321- try :
322- return _handle_filtered_collection (path , collection , access_control )
323- except requests .HTTPError as exc :
324- response = exc .response
325- return Response (
326- response .content ,
327- status = response .status_code ,
328- headers = _response_headers (response ),
329- )
330-
331- return _proxy_request (path )
481+ if request .method == "GET" :
482+ if collection :
483+ try :
484+ return _handle_filtered_collection (path , collection , access_control )
485+ except requests .HTTPError as exc :
486+ response = exc .response
487+ return Response (
488+ response .content ,
489+ status = response .status_code ,
490+ headers = _response_headers (response ),
491+ )
492+
493+ return _proxy_request (path , access_control )
332494
333495
334496if __name__ == "__main__" :
0 commit comments