# 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"
}
}
}
```