diff --git a/demos/demo2/api.py b/demos/demo2/api.py index fb0071c6194b2f1195b1848db7e005d8cb80432f..c6c1695bf82f1e9a1997eb521c26cd8b732f3aa2 100644 --- a/demos/demo2/api.py +++ b/demos/demo2/api.py @@ -1,4 +1,4 @@ -from typing import Set +from typing import Set, Callable from basyx.aas import model import base64 @@ -166,3 +166,4 @@ class ModelProvider: return if path_elems[4] == SUBMODELELEMENTS: return self.submodel_api[submodel_id].set_submodel_element_value_by_path(path_elems[5], value) + diff --git a/demos/demo2/broker_api.py b/demos/demo2/broker_api.py index 38c0c674785baf63c3065fd38b6f37d7bc2fe62a..8ead5ecc667ea9b2611211c0bfd688962c46efcd 100644 --- a/demos/demo2/broker_api.py +++ b/demos/demo2/broker_api.py @@ -1,5 +1,6 @@ import asyncio from uuid import uuid4 +from typing import Callable from s3i.broker import BrokerAMQP from s3i import broker_message from s3i.exception import S3IBMessageError @@ -12,11 +13,20 @@ import traceback import base64 from typing import Iterable from datetime import datetime -import time +import sys, os, inspect import api +import opa_api import helpers +currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parentdir = os.path.dirname(currentdir) +sys.path.insert(0, parentdir) +sys.path.insert(0, os.path.dirname(parentdir)) + +from model import security + + logger = logging.getLogger(__name__) ch = logging.StreamHandler() ch.setLevel(logging.INFO) @@ -25,14 +35,32 @@ ch.setFormatter(formatter) logger.addHandler(ch) +NOT_AUTHORIZED = "NOT_AUTHORIZED" +s3ib_to_rest = {"getValueRequest": "READ", "setValueRequest": "WRITE", "createAttributeRequest": "WRITE", + "serviceRequest": "WRITE"} + + class S3IBServer: - def __init__(self, access_token: str, provider: api.ModelProvider, id: str, loop): + def __init__(self, access_token: str, provider: api.ModelProvider, id: str, + loop, security_id: str = None, security_enabled = False, opa_server_url="http://localhost:8181"): self.id = id self.provider = provider self.loop = loop self.broker = BrokerAMQP(access_token, "s3ibs://" + self.id, self.callback, loop) self.serializer = AASToJsonEncoder() self.deserializer = AASFromJsonDecoder() + self.opa = opa_api.Opa(opa_server_url) + self.security_id = base64.urlsafe_b64encode(security_id.encode()).decode() + self.security_enabled = security_enabled + self.callables: dict[str, Callable] = {} + + if self.security_enabled: + self._update_security() + + def _update_security(self): + security_sm = self.provider.getValue(f"/aas/submodels/{self.security_id}/submodel") + data = security.get_dic_from_security_submodel(security_sm) + self.opa.update_data(data, "api") def _check_for_events(self, path) -> Iterable[BasicEventElement]: triggered_events = [] @@ -44,16 +72,25 @@ class S3IBServer: event: BasicEventElement = event obs_path = helpers.id_short_path_from_ref(event.observed) req_path = helpers.id_short_path_from_path(path) + print(obs_path) + print(req_path) if obs_path == req_path: triggered_events.append(event) logger.info(f"[Event] [{event.message_topic}] {event.id_short} was triggered") return triggered_events - + + def add_callable(self, id: str, callable: Callable): + self.callables[id] = callable + + def _invoke_callable(self, id: str, **kwargs): + callable = self.callables[id] + return callable(**kwargs) + def callback(self, ch, method, properties, body): try: msg = broker_message.Message(base_msg=body) - path = msg.base_msg["attributePath"] + path = msg.base_msg.get("attributePath", msg.base_msg.get("serviceType")) receiver_endpoint = msg.base_msg["replyToEndpoint"] receiver_id = msg.base_msg["sender"] received_msg_id = msg.base_msg["identifier"] @@ -61,7 +98,20 @@ class S3IBServer: logger.info(f"[S3I] Received {message_type} from {receiver_id}") - if message_type == "getValueRequest": + if self.security_enabled and not self.opa.query(receiver_id, s3ib_to_rest[message_type], helpers.id_short_path_from_path(path)): + reply = broker_message.UserMessage() + reply.fillUserMessage( + sender=self.id, + receivers=[receiver_id], + message_id="s3i:" + str(uuid4()), + subject=NOT_AUTHORIZED, + text=NOT_AUTHORIZED, + replying_to_msg=received_msg_id, + reply_to_endpoint="s3ibs://" + self.id + ) + self.broker.send([receiver_endpoint], json.dumps(reply.base_msg)) + + elif message_type == "getValueRequest": value = self.provider.getValue(path) reply = broker_message.GetValueReply() reply.fillGetValueReply( @@ -100,6 +150,10 @@ class S3IBServer: content={path: self.deserializer.decode(new_value)} ) self.broker.publish_event(json.dumps(evt_msg.base_msg), event.message_topic) + + # update opa server if security overwrittem + if self.security_enabled and path.startswith(self.security_id): + self._update_security() elif message_type == "createAttributeRequest": @@ -114,6 +168,24 @@ class S3IBServer: reply.base_msg["replyingToMessage"] = received_msg_id reply.base_msg["ok"] = True self.broker.send([receiver_endpoint], json.dumps(reply.base_msg)) + + elif message_type == "serviceRequest": + callable_id = msg.base_msg["serviceType"] + parameters = msg.base_msg["parameters"] + service_type = msg.base_msg["serviceType"] + res = self.callables.get(callable_id)(**parameters) + reply = broker_message.ServiceReply() + reply.fillServiceReply( + sender=self.id, + receivers=[receiver_id], + message_id="s3i:" + str(uuid4()), + reply_to_endpoint="s3ibs://" + self.id, + replying_to_msg=received_msg_id, + service_type=service_type, + results=res + ) + self.broker.send([receiver_endpoint], json.dumps(reply.base_msg)) + except S3IBMessageError as e: self.broker.send([receiver_endpoint], json.dumps(e.error_msg)) except Exception as e: @@ -150,6 +222,7 @@ class S3IBAsyncClient: message_type = msg.base_msg["messageType"] value = msg.base_msg.get("value") ok = msg.base_msg.get("ok") + results = msg.base_msg.get("results") logger.info(f"[S3I] Received {message_type} from {sender}") @@ -160,14 +233,18 @@ class S3IBAsyncClient: logger.info(f"[Event] [{topic}] [{datetime.fromtimestamp(timestamp)}] {content}") self.requests[topic].set_result(content) else: - future: asyncio.Future = self.requests[msg_id] - if value: - future.set_result(self.deserializer.decode(value)) - else: - future.set_result(ok) + future: asyncio.Future = self.requests.get(msg_id) + if future: + if value: + future.set_result(self.deserializer.decode(value)) + elif results: + future.set_result(results) + else: + future.set_result(ok) - except S3IBMessageError as e: - raise Exception(body) + except Exception as e: + print(traceback.format_exc()) + print(e.args) async def getValue(self, receiver_id: str, endpoint: str, path: str): if not self.channel_open.done(): @@ -230,10 +307,34 @@ class S3IBAsyncClient: await reply return reply.result() - async def subscribeToEvent(self, topic: str): + async def awaitEvent(self, topic: str): if not self.channel_open.done(): await self.channel_open - self.broker.subscribe_topic(topic) handle = self.loop.create_future() - self.requests[topic] = handle - return handle \ No newline at end of file + if not self.requests.get(topic): + self.broker.subscribe_topic(topic) + self.requests[topic] = handle + return handle + + async def invokeOperation(self, receiver_id: str, endpoint: str, service_path: str, parameters: dict): + if not self.channel_open.done(): + await self.channel_open + msg = broker_message.ServiceRequest() + receiver_endpoint = endpoint + my_endpoint = "s3ibs://" + self.id + msg_id = "s3i:" + str(uuid4()) + msg.fillServiceRequest( + sender=self.id, + receivers=[receiver_id], + message_id=msg_id, + service_type=service_path, + parameters=parameters, + reply_to_endpoint=my_endpoint, + ) + self.broker.send([receiver_endpoint], json.dumps(msg.base_msg)) + reply = self.loop.create_future() + self.requests[msg_id] = reply + await reply + return reply.result() + + diff --git a/demos/demo2/demo2_dzwald.py b/demos/demo2/demo2_dzwald.py index 19c1e074f2581c40b99ae809aeba5815d98bb661..be8f5ebedd1458b0f533565a8871726daff5c80c 100644 --- a/demos/demo2/demo2_dzwald.py +++ b/demos/demo2/demo2_dzwald.py @@ -11,6 +11,7 @@ import logging import datetime import base64 from urllib.parse import quote +import time import api import broker_api @@ -21,7 +22,7 @@ parentdir = os.path.dirname(currentdir) sys.path.insert(0, parentdir) sys.path.insert(0, os.path.dirname(parentdir)) -from model import enums, models, configs +from model import enums, models, configs, security, utils # print info logs to console logger = logging.getLogger("broker_api") @@ -382,6 +383,8 @@ def create_aas(): events.add_referable(event) + security_sm = security.Security("https://www.company.com/security") + submodels = [ arbeitsauftrag, beobachtung, @@ -391,9 +394,12 @@ def create_aas(): verkaufslos, waldweg, zu_faellende_baeume, - events + events, + security_sm ] + """ + def remove_iteration_ending(val: str): # remove iteration ending like {00} val = re.sub(r'\{\d+\}$', '', val) @@ -431,6 +437,8 @@ def create_aas(): for submodel in submodels: set_semantic_id_for_each_in(submodel) + """ + aas_dz_wald = models.DZWald( dzwald_id="https://www.company.com/dz_wald/1", asset=basyx.aas.model.AssetInformation(), @@ -445,6 +453,34 @@ def create_aas(): holzpreisbereiche=basyx.aas.model.ModelReference.from_referable( holzpreisbereiche), ) + + rules = [security.AccessPermissionRule(forestmanager_hmi_id, + security.PermissionKind.ALLOW, + security.Permission.READ), + security.AccessPermissionRule(forestmanager_hmi_id, + security.PermissionKind.ALLOW, + security.Permission.WRITE)] + + rules_smc = security.AccessPermissionCollection( + target=model.ModelReference.from_referable(aas_dz_wald), + rules=rules + ) + + # print(rules_smc.get_referable("Target").value) + + access_control = security.AccessControl(permissions=[rules_smc]) + security_sm.add_referable(access_control) + # print(security.get_dic_from_security_submodel(security_sm)) + + getHolzpreisbereich_op = model.Operation( + id_short="getHolzpreisbereich", + input_variable=[model.OperationVariable(model.Property( + id_short="HolzlisteId", + value_type=model.datatypes.String + ))] + ) + holzliste.add_referable(getHolzpreisbereich_op) + return aas_dz_wald, submodels @@ -457,6 +493,44 @@ def write_aas(): with open("dz_wald_example.json", "w", encoding='utf-8') as file: json_serialization.write_aas_json_file(file, objstore) +# Function corrosponding to the "getHolzpreisbereich" operation. +# Note that the function's parameters are named after the operation's +# input variables. +def getHolzpreisbereich(provider: api.ModelProvider, HolzlisteId: str): + holzliste_sm = provider.getValue(f"/aas/submodels/{helpers.encode_id(HolzlisteId)}/submodel") + # TODO: REST + # CALL to Forstify + # create a placeholder reply for now + reply = { + "Baumart": "kie", + "PreisVon": 14500.0, + "PreisBis": 14500.0, + "Sorte": "ab", + "MittendurchmesserVon": 100.0, + "MittendurchmesserBis": 200.0, + "Datum": "8/4/2023, 10:56:39 AM", + "Quelle": "Forstify Marktplatz Angebote" + } + date_format = '%m/%d/%Y, %H:%M:%S %p' + holzpreisbereich = models.Holzpreisbereich( + baumart=enums.Holzart(reply["Baumart"]), + preis_von=reply["PreisVon"], + preis_bis=reply["PreisBis"], + sorte=enums.Sorte(reply["Sorte"]), + mittendurchmesser_bis=reply["MittendurchmesserBis"], + mittendurchmesser_von=reply["MittendurchmesserVon"], + datum=datetime.datetime.strptime(reply["Datum"], date_format), + quelle=reply["Quelle"], + holzliste=model.ModelReference.from_referable(holzliste_sm) + ) + holzpreisbereiche_id = "https://www.company.com/submodels/holzpreisbereiche" + holzpreisbereiche: model.SubmodelElementList = provider.getValue( + f"/aas/submodels/{helpers.encode_id(holzpreisbereiche_id)}/submodel" + ) + utils.add_items_to_se_list(holzpreisbereiche, [holzpreisbereich]) + return reply + + def main(): # obtain access token @@ -466,8 +540,8 @@ def main(): # update policy for other things to read the S3I-Directory entry of your thing # view helpers.py for a better insight # function call is commented because it needs to be done a single time - s3i_dir = Directory("https://dir.s3i.vswf.dev/api/2/", access_token) - helpers.grant_entry_read_permissions(s3i_dir, dzwald_id, [forestmanager_hmi_id, forstify_hmi_id]) + # s3i_dir = Directory("https://dir.s3i.vswf.dev/api/2/", access_token) + # helpers.grant_entry_read_permissions(s3i_dir, dzwald_id, [forestmanager_hmi_id, forstify_hmi_id]) # create AAS aas, submodels = create_aas() @@ -496,8 +570,16 @@ def main(): # now create a server instance that will translate incoming S3I-B messages # to methods exposed by the ModelProvider + HolzlisteId = 'https://www.company.com/holzliste/1' loop = asyncio.get_event_loop() - server = broker_api.S3IBServer(access_token, provider, dzwald_id, loop) + + # set last flag to True to enable security + server = broker_api.S3IBServer(access_token, provider, dzwald_id, loop, "https://www.company.com/security", False) + + # add callable associated with operation 'getHolzpreisbereich' to server + callable = lambda **kwargs : getHolzpreisbereich(provider, **kwargs) + operation_path = f"/aas/submodels/{helpers.encode_id(HolzlisteId)}/submodel/submodelElements/getHolzpreisbereich" + server.add_callable(operation_path, callable) try: server.run() @@ -506,5 +588,7 @@ def main(): + if __name__ == "__main__": main() + #create_aas() diff --git a/demos/demo2/demo2_forestmanager.py b/demos/demo2/demo2_forestmanager.py index 6bbc5d997ff8540e9bd4244cf8c83a8d59664c55..1a6f09f868a2b04d09f15505c46fbadf86e7f3a0 100644 --- a/demos/demo2/demo2_forestmanager.py +++ b/demos/demo2/demo2_forestmanager.py @@ -6,6 +6,7 @@ import os, sys, inspect import base64 import broker_api +import helpers currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) parentdir = os.path.dirname(currentdir) @@ -44,11 +45,11 @@ async def main(): access_token = idp.get_token(TokenType.ACCESS_TOKEN) # obtain dzwald's endpoint by checking its entry within the S3I-Directory - s3i_dir = Directory("https://dir.s3i.vswf.dev/api/2/", access_token) - dzwald_dir_entry = s3i_dir.queryThingIDBased(dzwald_id) - s3ib_endpoints = [i for i in dzwald_dir_entry["attributes"]["allEndpoints"] if - i.startswith('s3ib')] - dzwald_endpoint = s3ib_endpoints[0] + #s3i_dir = Directory("https://dir.s3i.vswf.dev/api/2/", access_token) + #dzwald_dir_entry = s3i_dir.queryThingIDBased(dzwald_id) + #s3ib_endpoints = [i for i in dzwald_dir_entry["attributes"]["allEndpoints"] if + # i.startswith('s3ib')] + dzwald_endpoint = "s3ib://s3i:bc30c279-02c5-4918-a2ad-761e927214dd" # use async client to access AAS and its submodels through their REST API asynchronously client = broker_api.S3IBAsyncClient(access_token, forestmanager_hmi_id, loop) @@ -62,11 +63,10 @@ async def main(): # listen on event "Auftragsstatus_Updated" events_submodel_id = "https://www.company.com/submodels/events" events_submodel_id_encoded = base64.urlsafe_b64encode(events_submodel_id.encode()).decode() - print(events_submodel_id_encoded) event: model.BasicEventElement = await client.getValue(dzwald_id, dzwald_endpoint, f"/aas/submodels/{events_submodel_id_encoded} \ /submodel/submodelElements/Auftragsstatus_Updated") - auftragsstatus_updated = await client.subscribeToEvent(event.message_topic) + auftragsstatus_updated = await client.awaitEvent(event.message_topic) # Speichern eines Waldweges waldweg_submodel = create_waldweg() @@ -114,6 +114,17 @@ async def main(): print( f"{auftragsstatus.id_short}: {enums.Auftragsstatus(int(auftragsstatus.value))}") + # Invoke Operation 'getHolzpreisbereich' + HolzlisteId = "https://www.company.com/holzliste/1" + reply4 = await client.invokeOperation( + dzwald_id, + dzwald_endpoint, + f"/aas/submodels/{helpers.encode_id(HolzlisteId)}/submodel/submodelElements/getHolzpreisbereich", + {"HolzlisteId": HolzlisteId} + ) + print(f"result of operation 'getHolzpreisbereich': {reply4}") + + # await event await asyncio.wait([auftragsstatus_updated]) diff --git a/demos/demo2/helpers.py b/demos/demo2/helpers.py index a012abaee2f094df040a427113274842a1e60ab0..5e032ad36d690b77509de3bf5a3a2b410efa48a4 100644 --- a/demos/demo2/helpers.py +++ b/demos/demo2/helpers.py @@ -4,7 +4,7 @@ from urllib.parse import unquote from basyx.aas import model from s3i import Directory -from api import SUBMODELS, SUBMODELELEMENTS +from api import SUBMODELS, SUBMODELELEMENTS, SUBMODEL, AAS def add_aas_to_dir_entry(aas: model.AssetAdministrationShell, dir: Directory, thing_id: str): @@ -52,12 +52,14 @@ def grant_entry_read_permissions(dir: Directory, thing_id, receivers: list): def id_short_path_from_ref(ref: model.ModelReference): - arr = [] + arr = ["aas"] for i, key in enumerate(ref.key): if key.value.isnumeric(): arr[i-1] = f"{arr[i-1]}[{key.value}]" elif key.type == model.KeyTypes.SUBMODEL: arr.append(base64.urlsafe_b64encode(key.value.encode()).decode()) + elif key.type == model.KeyTypes.ASSET_ADMINISTRATION_SHELL: + continue else: arr.append(key.value) return ".".join(arr) @@ -65,12 +67,18 @@ def id_short_path_from_ref(ref: model.ModelReference): def id_short_path_from_path(path: str): arr = path.split("/") - try: - submodel_id_index = arr.index(SUBMODELS) + 1 - id_short_path_index = arr.index(SUBMODELELEMENTS) + 1 - except: - return "" - return f"{arr[submodel_id_index]}.{unquote(arr[id_short_path_index])}" + submodel_id_index = arr.index(SUBMODEL) - 1 if SUBMODEL in arr else None + id_short_path_index = arr.index(SUBMODELELEMENTS) + 1 if SUBMODELELEMENTS in arr else None + if id_short_path_index: + return f"{AAS}.{arr[submodel_id_index]}.{unquote(arr[id_short_path_index])}" + elif submodel_id_index: + return f"{AAS}.{arr[submodel_id_index]}" + else: + return AAS + +def encode_id(id: str): + return base64.urlsafe_b64encode(id.encode()).decode() + diff --git a/demos/demo2/opa/data.json b/demos/demo2/opa/data.json new file mode 100644 index 0000000000000000000000000000000000000000..2bd77f702821c4297c4fc35978bdb797022d8030 --- /dev/null +++ b/demos/demo2/opa/data.json @@ -0,0 +1,12 @@ +{ + "api": { + "https://www.company.com/holzliste/1": { + "READ": [ + "s3i:e8ef672c-109b-4c36-8999-f4ababa0bffc" + ], + "WRITE": [ + "s3i:e8ef672c-109b-4c36-8999-f4ababa0bffc" + ] + } + } +} \ No newline at end of file diff --git a/demos/demo2/opa/input.json b/demos/demo2/opa/input.json new file mode 100644 index 0000000000000000000000000000000000000000..c0e22092f60925f16c5a5fbfa68a43cc2fe41f8b --- /dev/null +++ b/demos/demo2/opa/input.json @@ -0,0 +1,5 @@ +{ + "user": "s3i:e8ef672c-109b-4c36-8999-f4ababa0bffc", + "method": "WRITE", + "path": "https://www.company.com/holzliste/1" +} \ No newline at end of file diff --git a/demos/demo2/opa/opa.exe b/demos/demo2/opa/opa.exe new file mode 100644 index 0000000000000000000000000000000000000000..ea0fe2f7596b71b4f0e0c2808f1fd61235fe0f98 Binary files /dev/null and b/demos/demo2/opa/opa.exe differ diff --git a/demos/demo2/opa/policy.rego b/demos/demo2/opa/policy.rego new file mode 100644 index 0000000000000000000000000000000000000000..6f7ca43e50d98f3f89721aaad01a5cf4e488db89 --- /dev/null +++ b/demos/demo2/opa/policy.rego @@ -0,0 +1,23 @@ +package policy + +import future.keywords.in +import input + +default allow = false + +allow { + path_arr := split(input.path, "/") + print(path_arr) + path_id_short := split(path_arr[count(path_arr)-1], ".") + z := array.slice(path_arr, 0, count(path_arr)-1) + path_full := array.concat(z, path_id_short) + print(path_id_short) + print(path_full) + some i, _ in path_full + path_arr_slice := array.slice(path_full, 0, i+1) + path := concat("/", path_arr_slice) + print(path) + print(data.api[path]) + some user in data.api[path][input.method] + user == input.user +} diff --git a/demos/demo2/opa_api.py b/demos/demo2/opa_api.py new file mode 100644 index 0000000000000000000000000000000000000000..b7f05f6cdd122bb09e1c93e3790c6ca2d20dba11 --- /dev/null +++ b/demos/demo2/opa_api.py @@ -0,0 +1,28 @@ +import requests +import json + +class Opa: + + def __init__(self, server_url="http://localhost:8181"): + self.server_url = server_url + + def query(self, user, method, path): + body = {"input": {"user": user, "method": method, "path": path}} + url = f"{self.server_url}/v1/data/policy/allow" + response = requests.post(url=url, data=json.dumps(body)) + result = json.loads(response.text)["result"] + return result + + def get_data(self, path=""): + url = f"{self.server_url}/v1/data/{path}" + response = requests.get(url=url) + result = json.loads(response.text)["result"] + return result + + def update_data(self, data, path=""): + url = f"{self.server_url}/v1/data/{path}" + response = requests.put(url=url, data=json.dumps(data)) + return response.status_code + #result = json.loads(response.text)["result"] + #return result + diff --git a/model/security.py b/model/security.py new file mode 100644 index 0000000000000000000000000000000000000000..0a7a50f1195d6edb6b2137c8ae1739a84403aba0 --- /dev/null +++ b/model/security.py @@ -0,0 +1,120 @@ +import base64 +import json +from enum import Enum, IntEnum +from typing import Iterable, Optional +from basyx.aas import model +from basyx.aas.model import base +from basyx.aas.model.submodel import SubmodelElement +from model import utils + + +class PermissionKind(IntEnum): + ALLOW = 0, + DENY = 1, + IN_APPLICABLE = 3, + UNDEFINED = 4 + + +class Permission(str, Enum): + READ = "READ", + WRITE = "WRITE" + + +class AccessPermissionRule(model.SubmodelElementCollection): + + def __init__(self, + user_id: str, + permission_kind: PermissionKind, + permission: Permission): + super().__init__(id_short="AccessPermissionRule") + self.add_referable(model.Property(id_short="User", value_type=model.datatypes.String, value=user_id)) + self.add_referable(model.Property(id_short="PermissionKind", value_type=model.datatypes.Integer, value=permission_kind)) + self.add_referable(model.Property(id_short="Permission", value_type=model.datatypes.String, value=permission)) + + +class AccessPermissionCollection(model.SubmodelElementCollection): + + def __init__(self, + target: model.ModelReference, + rules: Optional[Iterable[AccessPermissionRule]] = None): + super().__init__( + id_short="AccessPermissionCollection", + ) + self.add_referable(model.ReferenceElement(id_short="Target", value=target)) + rules_se_list = model.SubmodelElementList(id_short="Rules", type_value_list_element=AccessPermissionRule) + utils.add_items_to_se_list(rules_se_list, rules) + self.add_referable(rules_se_list) + + def add_rules(self, + rules: Iterable[AccessPermissionRule]): + rules_se_list: model.SubmodelElementList = self.get_referable("Rules") + for i in rules: + rules_se_list.add_referable(i) + + +class AccessControl(model.SubmodelElementList): + + def __init__(self, + permissions: Optional[Iterable[AccessPermissionCollection]] = None): + super().__init__(id_short="AccessControl", type_value_list_element=AccessPermissionCollection) + utils.add_items_to_se_list(self, permissions) + +class Security(model.Submodel): + + def __init__(self, + id: model.Identifier, + access_control: Optional[AccessControl] = None + ): + super().__init__(id_=id, id_short="Security") + if access_control: + self.add_referable(access_control) + +""" +def id_short_path_from_ref(ref: model.ModelReference): + arr = [] + for i, key in enumerate(ref.key): + if key.value.isnumeric(): + arr[i-1] = f"{arr[i-1]}[{key.value}]" + elif key.type == model.KeyTypes.SUBMODEL: + arr.append(base64.urlsafe_b64encode(key.value.encode()).decode()) + elif key.type == model.KeyTypes.ASSET_ADMINISTRATION_SHELL: + arr.append("aas") + else: + arr.append(key.value) + return ".".join(arr) +""" + +def id_short_path_from_ref(ref: model.ModelReference): + arr = ["aas"] + for i, key in enumerate(ref.key): + if key.value.isnumeric(): + arr[i-1] = f"{arr[i-1]}[{key.value}]" + elif key.type == model.KeyTypes.SUBMODEL: + arr.append(base64.urlsafe_b64encode(key.value.encode()).decode()) + elif key.type == model.KeyTypes.ASSET_ADMINISTRATION_SHELL: + continue + else: + arr.append(key.value) + return ".".join(arr) + +def get_dic_from_security_submodel(security_sm: Security): + dic = {} + access_control: AccessControl = security_sm.get_referable("AccessControl") + for permission in access_control.value: + permission: AccessPermissionCollection = permission + target: model.ReferenceElement = permission.get_referable("Target") + id_short_path = id_short_path_from_ref(target.value) + rules: model.SubmodelElementList = permission.get_referable("Rules") + for rule in rules: + user: model.Property = rule.get_referable("User") + grant: model.Property = rule.get_referable("Permission") + if not dic.get(id_short_path): + dic[id_short_path] = {} + dic[id_short_path][Permission.READ.value] = [] + dic[id_short_path][Permission.WRITE.value] = [] + dic[id_short_path][grant.value].append(user.value) + return dic + + + +