# Open Policy Agent Experiments ## Goal Evaluate integration of OPA as an ACA-py plugin to automate processes based on exchangable policies/business rules. The whole process that has been automated is the following: We have two agents with public DIDs 1. Manual start using Swagger: Agent A DIDX Request to Agent B 2. Agent B accepts request automatically (OPA Decision) 3. Agent B automatically requests proof according to template managed as OPA data. We use an unrestricted request for the name attribute 4. Agent A fullfills request based on OPA data ## Architecture ACA-Py has a name-spaced event bus. The built-in namespaces are ``` acapy::record::[record_type]::[[record_state]] acapy::webhook::[protocol]::[[event]] ``` The admin server is subscribed to both namespaces and queues the events to deliver webhooks to all subscribed controller applications. Similar to the admin server, the OPA plugin subscribes to all events and forwards them to the appropriate OPA policies ```plantuml @startuml "Event Bus"->"OPA Plugin" ++: Notify event "OPA Plugin"->"OPA Plugin": Inject additional context "OPA Plugin"->"OPA": POST: /v1/data/{event.topic} \n with event.payload and context "OPA"->"OPA Plugin": Action "OPA"->"OPA Plugin": Handle action "OPA Plugin"->"ACA-Py Protocol Managers" --: Execute "ACA-Py Protocol Managers"->"Event Bus": Notify @enduml ``` Protocol Managers can trigger state transistions in protocols, i.e. send messages to other agents, and upate records. ## General Pattern - Event -> Policy -> Action ## Use Case ### Auto-accept connection request Event: `acapy::record::connections::request` Policy: ``` package connections action[{"type": type, "data": payload}] { input.state == "request" input.accept == "manual" input.their_role == "invitee" type := "connection" payload := {"accept": true} } ``` Action: ``` if action["type"] == "connection": if action["data"]["accept"]: session = await profile.session(self._context) didx_mgr = DIDXManager(session) conn_rec = await ConnRecord.retrieve_by_id( session, event.payload["connection_id"] ) async def respond(didx_mgr, conn_rec, session): await asyncio.sleep(2) didx_resp = await didx_mgr.create_response(conn_rec) responder = session.inject(BaseResponder, required=True) if responder: await responder.send_reply( didx_resp, connection_id=conn_rec.connection_id ) loop = asyncio.get_event_loop() loop.create_task(respond(didx_mgr, conn_rec, session)) ``` #### Learnings Here we can see the first issue I've encountered. When I wanted to create a response the request message was not yet attached to the connection record: ``` File "/home/indy/aries_cloudagent/connections/models/conn_record.py", line 378, in retrieve_request self.RECORD_TYPE_REQUEST, {"connection_id": self.connection_id} File "/home/indy/aries_cloudagent/storage/base.py", line 92, in find_record raise StorageNotFoundError("Record not found") aries_cloudagent.storage.error.StorageNotFoundError: Record not found ``` Therefore, I hacked this asynchronous task. ### Send Proof Request based on template on established connection Event: `acapy::record::connections::completed` Policy: ``` action[{"type": type, "data": payload}] { input.state == "completed" input.their_role == "invitee" type := "request proof" payload := {"proof_request": data.proof_template["IwantToKnowYourName"]} } ``` Action: Create and send proof request (see code for details) ### Auto-present with self attested attributes managed in OPA Answer a presentation request with a self attested attribute which can be managed in a OPA data file. Event: `acapy::record::present_proof::request-received` Here it would be possible to get additional context from ACA-Py by - letting ACA-Py provide the credentials that would fit to satisfy the proof request. - Retrieving tags from the connection record Policy: ``` package present_proof action[{"type": type, "data": payload}] { input.state == "request_received" input.role == "prover" type := "present proof" req_attr := input.presentation_request.requested_attributes k := [ i | some i; _ := {req_attr[i]: data.attributes[req_attr[i].name]}] self_attested_attr := {x | x := { k[i]: data.attributes[req_attr[k[i]].name] }} payload := {"self_attested_attr": self_attested_attr} } ``` Action: Create and presentation (see code for details) #### Learnings Using the internal methods gets really messy here. Although the general flow works, the record states are not properly updated. * If I don't implement some memory I end up in an infinite loop because for some reason after the `response-sent` event another `request-received` arrives and the process starts again * I end up in the `request-received` state with both agents allthough the proof arrives at the verifier. ### Tag connection Not implemented yet, but could easily be added: * Associate a proof template with a tag in OPA data or policy * Add action type "tag connection" that stores tag in connection meta data. These tags can then be used as input for policy decision. ## Conclusion - OPA used in this way could be used to configure controllers - Configurations could be adapted during runtime - OPA should however be better used on a controller level. Intervention on lower layers leads to issues - A suitable simple use case would be the configuration of verifier agents P.S. The approach using a generic controller is much better!! See: https://dev.azure.com/economy-of-things/MasterDataManagement/_git/acapy-js-opa ## Appendix ### Plugin Code https://github.com/boschresearch/aries-cloudagent-python/blob/opa/aries_cloudagent/opa/base.py ### OPA Policies/Data and instructions https://dev.azure.com/economy-of-things/MasterDataManagement/_git/aries-policies ### OPA Data ``` { "attributes": { "name": "Bob" }, "proof_template": { "IwantToKnowYourName": { "name": "Proof request", "requested_predicates": {}, "requested_attributes": { "IwantToKnowYourName": { "name": "name" } }, "version": "0.1" }, "verifiedOrg": { "name": "Proof request", "requested_predicates": {}, "requested_attributes": { "bizNum": { "name": "business_number", "restrictions": [{ "schema_id": "3AwEHTco9jJPPjygaPzeks:2:test_bc_reg:0.1" }] } }, "version": "0.1" }, "verifiedTSP": { "name": "Proof request", "requested_predicates": {}, "requested_attributes": { "bizNum": { "name": "business_number" } }, "version": "0.1" } } } ```