diff --git a/demos/demo2/broker_api.py b/demos/demo2/broker_api.py index 26ba39f324efa3a997a512284d589fe6b58e98c5..583175f3732c62f56290bd2007367ebbbf3c62c0 100644 --- a/demos/demo2/broker_api.py +++ b/demos/demo2/broker_api.py @@ -42,14 +42,14 @@ s3ib_to_rest = {"getValueRequest": "READ", "setValueRequest": "WRITE", "createAt class S3IBServer: def __init__(self, access_token: str, provider: api.ModelProvider, id: str, - loop, security_id: str = None, security_enabled = False): + 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() + 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] = {} @@ -205,6 +205,7 @@ class S3IBAsyncClient: self.broker.create_event_queue() self.channel_open = self.loop.create_future() self.requests: dict[str, asyncio.Future] = {} + self.conditionals: dict[asyncio.Future, dict[str, str]] = {} self.deserializer = AASFromJsonDecoder() self.serializer = AASToJsonEncoder() self.broker.connect() @@ -230,6 +231,14 @@ class S3IBAsyncClient: timestamp = msg.base_msg.get("timestamp") logger.info(f"[Event] [{topic}] [{datetime.fromtimestamp(timestamp)}] {content}") self.requests[topic].set_result(content) + elif not self.requests.get(msg_id): + for future, attributes in self.conditionals.items(): + cond = True + for attr, value in attributes.items(): + if msg.base_msg.get(attr) != value: + cond = False + if cond: + future.set_result(msg.base_msg) else: future: asyncio.Future = self.requests.get(msg_id) if future: @@ -335,4 +344,28 @@ class S3IBAsyncClient: await reply return reply.result() + async def sendUserMessage(self, receiver_id: str, endpoint: str, subject: str, text: str): + if not self.channel_open.done(): + await self.channel_open + msg = broker_message.UserMessage() + receiver_endpoint = endpoint + my_endpoint = "s3ibs://" + self.id + msg_id = "s3i:" + str(uuid4()) + msg.fillUserMessage( + sender=self.id, + receivers=[receiver_id], + message_id=msg_id, + reply_to_endpoint=my_endpoint, + subject=subject, + text=text + ) + self.broker.send([receiver_endpoint], json.dumps(msg.base_msg)) + return + + async def awaitMessage(self, attributes: 'dict[str, str]'): + future = self.loop.create_future() + self.conditionals[future] = attributes + await future + return future.result() + diff --git a/demos/demo2/demo2_dzwald.py b/demos/demo2/demo2_dzwald.py index be8f5ebedd1458b0f533565a8871726daff5c80c..875f3738a9719b8f6c71cc929b3f83a465dd7e53 100644 --- a/demos/demo2/demo2_dzwald.py +++ b/demos/demo2/demo2_dzwald.py @@ -540,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() diff --git a/demos/demo2/demo2_forestmanager.py b/demos/demo2/demo2_forestmanager.py index d9dc7a2b179290c19d0cd27bd9792fa66d9379c2..6201652fbb94bd396e3f3c33b4a72e1b2b0e05d2 100644 --- a/demos/demo2/demo2_forestmanager.py +++ b/demos/demo2/demo2_forestmanager.py @@ -23,8 +23,8 @@ logger = logging.getLogger("broker_api") logger.setLevel(logging.INFO) # These are our HMI's credentials -forestmanager_hmi_id = "s3i:e8ef672c-109b-4c36-8999-f4ababa0bffc" -forestmanager_hmi_secret = "cfs2YMj2bzPIS3kDWpHYFyUKzCorAQuV" +forestmanager_hmi_id = "s3i:8a8ee1ab-63d2-42ea-92d1-1ae682a55e7a" +forestmanager_hmi_secret = "U9AkkeEXbOZNNFUYQvH5hb3WhbBGaw9a" # The id of forstify's HMI and dz_wald is known beforehand forstify_hmi_id = "s3i:8a8ee1ab-63d2-42ea-92d1-1ae682a55e7a" @@ -54,6 +54,9 @@ async def main(): # use async client to access AAS and its submodels through their REST API asynchronously client = broker_api.S3IBAsyncClient(access_token, forestmanager_hmi_id, loop) + # await access grant from waldbesitzer + await client.awaitMessage({"text": "Authorized"}) + # Speichern einer Beobachtung beobachtung_submodel = create_beobachtung() beobachtung_id_encoded = base64.urlsafe_b64encode(beobachtung_submodel.id.encode()).decode() diff --git a/demos/demo2/demo2_forestmanager_waldbesitzer.py b/demos/demo2/demo2_forestmanager_waldbesitzer.py new file mode 100644 index 0000000000000000000000000000000000000000..29b311ab39ae7aae7e022001c51648caea170472 --- /dev/null +++ b/demos/demo2/demo2_forestmanager_waldbesitzer.py @@ -0,0 +1,90 @@ +from basyx.aas import model +from s3i import IdentityProvider, TokenType, Directory +import asyncio +import logging +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) +sys.path.insert(0, parentdir) +sys.path.insert(0, os.path.dirname(parentdir)) + +from model import enums, security, utils + + +# print info logs to console +logger = logging.getLogger("broker_api") +logger.setLevel(logging.INFO) + +# These are our HMI's credentials +forestmanager_waldbesitzer_id = "s3i:3f23c856-d252-42c9-a590-0b87d87478d4" +forestmanager_waldbesitzer_secret = "14jXsgYN3JlE31QRniN4ruMFBoVN0Uin" + +# The id of forstify's HMI and dz_wald is known beforehand +forstify_hmi_id = "s3i:8a8ee1ab-63d2-42ea-92d1-1ae682a55e7a" +dzwald_id = "s3i:bc30c279-02c5-4918-a2ad-761e927214dd" + + +def authenticate(): + idp = IdentityProvider( + grant_type="client_credentials", + client_id=forestmanager_waldbesitzer_id, + client_secret=forestmanager_waldbesitzer_secret, + ) + return idp + + +async def main(): + idp = authenticate() + 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] + + # use async client to access AAS and its submodels through their REST API asynchronously + client = broker_api.S3IBAsyncClient(access_token, forestmanager_waldbesitzer_id, loop) + + resource_path = "aas" + permission_receiver = forstify_hmi_id + + security_sm: model.Submodel = await client.getValue(dzwald_id, dzwald_endpoint, + f"/aas/submodels/{helpers.encode_id('https://www.company.com/security')}/submodel") + access_control: model.SubmodelElementList = security_sm.get_referable("AccessControl") + + new_rules = [security.AccessPermissionRule(permission_receiver, + security.PermissionKind.ALLOW, + security.Permission.READ), + security.AccessPermissionRule(permission_receiver, + security.PermissionKind.ALLOW, + security.Permission.WRITE)] + + for apc in access_control: + target: model.ReferenceElement = apc.get_referable("Target") + target_path = helpers.id_short_path_from_ref(target.value) + if target_path == resource_path: + rules: model.SubmodelElementList = apc.get_referable("Rules") + for rule in rules: + user: model.Property = rule.get_referable("User") + if user.value == permission_receiver: + rules.remove_referable(rule.id_short) + utils.add_items_to_se_list(rules, new_rules) + + await client.sendUserMessage(forstify_hmi_id, "s3ibs://" + forstify_hmi_id, "Authorized", "Authorized") + + + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(main()) + except KeyboardInterrupt: + loop.stop() diff --git a/model/security.py b/model/security.py index 0a7a50f1195d6edb6b2137c8ae1739a84403aba0..f8468587a7c5873ca3af048d2c51482b5330e313 100644 --- a/model/security.py +++ b/model/security.py @@ -3,8 +3,7 @@ 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 basyx.aas.model import Submodel, SubmodelElementCollection, SubmodelElementList from model import utils @@ -20,7 +19,7 @@ class Permission(str, Enum): WRITE = "WRITE" -class AccessPermissionRule(model.SubmodelElementCollection): +class AccessPermissionRule(SubmodelElementCollection): def __init__(self, user_id: str, @@ -28,11 +27,11 @@ class AccessPermissionRule(model.SubmodelElementCollection): 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)) + self.add_referable(model.Property(id_short="PermissionKind", value_type=model.datatypes.Integer, value=permission_kind.value)) + self.add_referable(model.Property(id_short="Permission", value_type=model.datatypes.String, value=permission.value)) -class AccessPermissionCollection(model.SubmodelElementCollection): +class AccessPermissionCollection(SubmodelElementCollection): def __init__(self, target: model.ModelReference, @@ -41,7 +40,7 @@ class AccessPermissionCollection(model.SubmodelElementCollection): 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) + rules_se_list = model.SubmodelElementList(id_short="Rules", type_value_list_element=SubmodelElementCollection) utils.add_items_to_se_list(rules_se_list, rules) self.add_referable(rules_se_list) @@ -52,14 +51,15 @@ class AccessPermissionCollection(model.SubmodelElementCollection): rules_se_list.add_referable(i) -class AccessControl(model.SubmodelElementList): +class AccessControl(SubmodelElementList): def __init__(self, permissions: Optional[Iterable[AccessPermissionCollection]] = None): - super().__init__(id_short="AccessControl", type_value_list_element=AccessPermissionCollection) + super().__init__(id_short="AccessControl", type_value_list_element=SubmodelElementCollection) utils.add_items_to_se_list(self, permissions) -class Security(model.Submodel): + +class Security(Submodel): def __init__(self, id: model.Identifier,