1111app = Flask (__name__ )
1212
1313UPSTREAM_REPOSITORY_URL = os .getenv ("UPSTREAM_REPOSITORY_URL" , "http://repository:80" ).rstrip ("/" )
14+ UPSTREAM_REGISTRY_URL = os .getenv ("UPSTREAM_REGISTRY_URL" , "http://registry:80" ).rstrip ("/" )
15+ UPSTREAM_DISCOVERY_URL = os .getenv ("UPSTREAM_DISCOVERY_URL" , "http://discovery:80" ).rstrip ("/" )
16+ UPSTREAM_URLS_BY_PREFIX = {
17+ "repository" : UPSTREAM_REPOSITORY_URL ,
18+ "registry" : UPSTREAM_REGISTRY_URL ,
19+ "discovery" : UPSTREAM_DISCOVERY_URL ,
20+ }
1421POLICY_DATA_PATH = os .getenv ("POLICY_DATA_PATH" , "/policies/data.json" )
1522PORT = int (os .getenv ("PORT" , "8080" ))
1623UPSTREAM_PAGE_LIMIT = int (os .getenv ("UPSTREAM_PAGE_LIMIT" , "500" ))
@@ -188,17 +195,53 @@ def _submodel_reference_allowed(reference: Any, allow_all: bool, allowed_ids: se
188195 return _id_allowed (submodel_id , allow_all , allowed_ids )
189196
190197
198+ def _configured_path_templates (access_control : dict [str , Any ], * keys : str ) -> set [str ]:
199+ configured_templates : set [str ] = set ()
200+ for key in keys :
201+ path_templates = access_control .get (key , [])
202+ if isinstance (path_templates , list ):
203+ configured_templates .update (
204+ template for template in path_templates if isinstance (template , str )
205+ )
206+ return configured_templates
207+
208+
209+ def _configured_aas_path_templates (access_control : dict [str , Any ]) -> set [str ]:
210+ return _configured_path_templates (
211+ access_control ,
212+ "aas_reference_path_templates" ,
213+ "aas_resource_path_templates" ,
214+ )
215+
216+
191217def _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 )}
218+ return _configured_path_templates (
219+ access_control ,
220+ "submodel_reference_path_templates" ,
221+ "submodel_resource_path_templates" ,
222+ )
223+
224+
225+ def _template_targets_aas (path_template : Any ) -> bool :
226+ if not isinstance (path_template , str ):
227+ return False
228+ lower_template = path_template .lower ()
229+ return "{aas" in lower_template and "{submodel" not in lower_template
196230
197231
198232def _template_targets_submodel (path_template : Any ) -> bool :
199233 return isinstance (path_template , str ) and "{submodel" in path_template .lower ()
200234
201235
236+ def _rule_targets_aas (rule : dict [str , Any ], configured_templates : set [str ]) -> bool :
237+ path_templates = rule .get ("path_templates" , [])
238+ if not isinstance (path_templates , list ):
239+ return False
240+ if configured_templates :
241+ return any (template in configured_templates for template in path_templates )
242+ return any (_template_targets_aas (template ) for template in path_templates )
243+
244+
202245def _rule_targets_submodel (rule : dict [str , Any ], configured_templates : set [str ]) -> bool :
203246 path_templates = rule .get ("path_templates" , [])
204247 if not isinstance (path_templates , list ):
@@ -208,6 +251,41 @@ def _rule_targets_submodel(rule: dict[str, Any], configured_templates: set[str])
208251 return any (_template_targets_submodel (template ) for template in path_templates )
209252
210253
254+ def _resource_rule_aas_access (
255+ access_control : dict [str , Any ],
256+ roles : set [str ],
257+ ) -> tuple [bool , set [str ]]:
258+ allowed_ids : set [str ] = set ()
259+ allow_all = False
260+ configured_templates = _configured_aas_path_templates (access_control )
261+
262+ for rule in access_control .get ("resource_rules" , []):
263+ if not _method_matches (rule , "GET" ) or not _role_matches (rule , roles ):
264+ continue
265+ if not _rule_targets_aas (rule , configured_templates ):
266+ continue
267+
268+ ids = set (rule .get ("ids" , []))
269+ if "*" in ids :
270+ allow_all = True
271+ allowed_ids .update (ids )
272+
273+ return allow_all , allowed_ids
274+
275+
276+ def _aas_resource_access (
277+ access_control : dict [str , Any ],
278+ roles : set [str ],
279+ request_path : str ,
280+ ) -> tuple [bool , set [str ]]:
281+ allow_all , allowed_ids = _resource_rule_aas_access (access_control , roles )
282+ if allow_all or allowed_ids :
283+ return allow_all , allowed_ids
284+ if _path_allowed_by_rule (access_control , request_path , roles ):
285+ return True , set ()
286+ return False , set ()
287+
288+
211289def _resource_rule_submodel_access (
212290 access_control : dict [str , Any ],
213291 roles : set [str ],
@@ -260,11 +338,46 @@ def _filter_aas_submodel_references(item: Any, allow_all: bool, allowed_ids: set
260338 return filtered_item
261339
262340
341+ def _has_nested_submodel_descriptors (item : Any ) -> bool :
342+ return isinstance (item , dict ) and isinstance (item .get ("submodelDescriptors" ), list )
343+
344+
345+ def _filter_shell_descriptor_submodel_descriptors (
346+ item : Any ,
347+ allow_all : bool ,
348+ allowed_ids : set [str ],
349+ ) -> Any :
350+ if not _has_nested_submodel_descriptors (item ):
351+ return item
352+
353+ filtered_item = deepcopy (item )
354+ filtered_item ["submodelDescriptors" ] = [
355+ descriptor
356+ for descriptor in item ["submodelDescriptors" ]
357+ if _item_allowed (descriptor , allow_all , allowed_ids )
358+ ]
359+ return filtered_item
360+
361+
362+ def _path_segments (path : str ) -> list [str ]:
363+ return [segment for segment in path .strip ("/" ).split ("/" ) if segment ]
364+
365+
263366def _is_submodel_refs_path (path : str ) -> bool :
264- segments = [ segment for segment in path . strip ( "/" ). split ( "/" ) if segment ]
367+ segments = _path_segments ( path )
265368 return len (segments ) >= 3 and segments [- 3 ] == "shells" and segments [- 1 ] == "submodel-refs"
266369
267370
371+ def _is_shell_descriptors_path (path : str ) -> bool :
372+ segments = _path_segments (path )
373+ return bool (segments ) and segments [- 1 ] == "shell-descriptors"
374+
375+
376+ def _is_submodel_descriptors_path (path : str ) -> bool :
377+ segments = _path_segments (path )
378+ return bool (segments ) and segments [- 1 ] == "submodel-descriptors"
379+
380+
268381def _request_query_without_paging () -> list [tuple [str , str ]]:
269382 query_items : list [tuple [str , str ]] = []
270383 for key in request .args :
@@ -275,8 +388,15 @@ def _request_query_without_paging() -> list[tuple[str, str]]:
275388 return query_items
276389
277390
391+ def _upstream_base_url (path : str ) -> str :
392+ segments = _path_segments (path )
393+ if segments :
394+ return UPSTREAM_URLS_BY_PREFIX .get (segments [0 ], UPSTREAM_REPOSITORY_URL )
395+ return UPSTREAM_REPOSITORY_URL
396+
397+
278398def _upstream_url (path : str , query_items : list [tuple [str , str ]] | None = None ) -> str :
279- url = f"{ UPSTREAM_REPOSITORY_URL } /{ path .lstrip ('/' )} "
399+ url = f"{ _upstream_base_url ( path ) } /{ path .lstrip ('/' )} "
280400 if query_items :
281401 return f"{ url } ?{ urlencode (query_items )} "
282402 return url
@@ -458,6 +578,53 @@ def _handle_filtered_submodel_refs(path: str, access_control: dict[str, Any]) ->
458578 return jsonify (payload ), status_code
459579
460580
581+ def _handle_filtered_shell_descriptors (path : str , access_control : dict [str , Any ]) -> Response :
582+ roles = _token_roles ()
583+ original_payload , items , status_code = _fetch_collection (path )
584+ request_path = "/" + path .strip ("/" )
585+ aas_allow_all , aas_allowed_ids = _aas_resource_access (
586+ access_control ,
587+ roles ,
588+ request_path ,
589+ )
590+ submodel_allow_all , submodel_allowed_ids = _submodel_reference_access (
591+ access_control ,
592+ roles ,
593+ request_path ,
594+ )
595+ filtered_items = [
596+ _filter_shell_descriptor_submodel_descriptors (
597+ descriptor ,
598+ submodel_allow_all ,
599+ submodel_allowed_ids ,
600+ )
601+ for descriptor in items
602+ if _item_allowed (descriptor , aas_allow_all , aas_allowed_ids )
603+ ]
604+
605+ payload = _filtered_payload (original_payload , filtered_items )
606+ return jsonify (payload ), status_code
607+
608+
609+ def _handle_filtered_submodel_descriptors (path : str , access_control : dict [str , Any ]) -> Response :
610+ roles = _token_roles ()
611+ original_payload , items , status_code = _fetch_collection (path )
612+ request_path = "/" + path .strip ("/" )
613+ allow_all , allowed_ids = _submodel_reference_access (
614+ access_control ,
615+ roles ,
616+ request_path ,
617+ )
618+ filtered_items = [
619+ descriptor
620+ for descriptor in items
621+ if _item_allowed (descriptor , allow_all , allowed_ids )
622+ ]
623+
624+ payload = _filtered_payload (original_payload , filtered_items )
625+ return jsonify (payload ), status_code
626+
627+
461628def _proxy_request (path : str , access_control : dict [str , Any ]) -> Response :
462629 upstream_response = requests .request (
463630 request .method ,
@@ -488,6 +655,21 @@ def _proxy_request(path: str, access_control: dict[str, Any]) -> Response:
488655 )
489656 return jsonify (filtered_payload ), upstream_response .status_code
490657
658+ if _has_nested_submodel_descriptors (payload ):
659+ roles = _token_roles ()
660+ request_path = "/" + path .strip ("/" )
661+ submodel_allow_all , submodel_allowed_ids = _submodel_reference_access (
662+ access_control ,
663+ roles ,
664+ request_path ,
665+ )
666+ filtered_payload = _filter_shell_descriptor_submodel_descriptors (
667+ payload ,
668+ submodel_allow_all ,
669+ submodel_allowed_ids ,
670+ )
671+ return jsonify (filtered_payload ), upstream_response .status_code
672+
491673 return Response (
492674 upstream_response .content ,
493675 status = upstream_response .status_code ,
@@ -503,27 +685,25 @@ def repository_proxy(path: str) -> Response:
503685 collection = _filtered_collection (normalized_path , access_control )
504686
505687 if request .method == "GET" :
506- if collection :
507- try :
688+ try :
689+ if _is_shell_descriptors_path (normalized_path ):
690+ return _handle_filtered_shell_descriptors (path , access_control )
691+
692+ if _is_submodel_descriptors_path (normalized_path ):
693+ return _handle_filtered_submodel_descriptors (path , access_control )
694+
695+ if collection :
508696 return _handle_filtered_collection (path , collection , access_control )
509- except requests .HTTPError as exc :
510- response = exc .response
511- return Response (
512- response .content ,
513- status = response .status_code ,
514- headers = _response_headers (response ),
515- )
516697
517- if _is_submodel_refs_path (normalized_path ):
518- try :
698+ if _is_submodel_refs_path (normalized_path ):
519699 return _handle_filtered_submodel_refs (path , access_control )
520- except requests .HTTPError as exc :
521- response = exc .response
522- return Response (
523- response .content ,
524- status = response .status_code ,
525- headers = _response_headers (response ),
526- )
700+ except requests .HTTPError as exc :
701+ response = exc .response
702+ return Response (
703+ response .content ,
704+ status = response .status_code ,
705+ headers = _response_headers (response ),
706+ )
527707
528708 return _proxy_request (path , access_control )
529709
0 commit comments