Skip to content

Commit b3d8b3e

Browse files
committed
Add Registry filtering
1 parent af51db5 commit b3d8b3e

2 files changed

Lines changed: 206 additions & 24 deletions

File tree

  • server/example_configurations/access_control/general/filter_server

server/example_configurations/access_control/general/filter_server/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ WORKDIR /app
66
COPY ./server/example_configurations/access_control/general/filter_server/app.py ./
77

88
ENV UPSTREAM_REPOSITORY_URL="http://repository:80"
9+
ENV UPSTREAM_REGISTRY_URL="http://registry:80"
10+
ENV UPSTREAM_DISCOVERY_URL="http://discovery:80"
911
ENV POLICY_DATA_PATH="/policies/data.json"
1012
ENV PORT="8080"
1113

server/example_configurations/access_control/general/filter_server/app.py

Lines changed: 204 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
app = Flask(__name__)
1212

1313
UPSTREAM_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+
}
1421
POLICY_DATA_PATH = os.getenv("POLICY_DATA_PATH", "/policies/data.json")
1522
PORT = int(os.getenv("PORT", "8080"))
1623
UPSTREAM_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+
191217
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)}
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

198232
def _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+
202245
def _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+
211289
def _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+
263366
def _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+
268381
def _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+
278398
def _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+
461628
def _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

Comments
 (0)