Skip to content

Commit 1f1f49f

Browse files
authored
Merge pull request #51 from OpenMined/howard/#21-revocation
Implement Revocation API
2 parents 2f04d47 + 1a44510 commit 1f1f49f

File tree

14 files changed

+1437
-42
lines changed

14 files changed

+1437
-42
lines changed

libs/aries-basic-controller/aries_basic_controller/aries_controller.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .controllers.server import ServerController
1818
from .controllers.oob import OOBController
1919
from .controllers.action_menu import ActionMenuController
20+
from .controllers.revocation import RevocationController
2021

2122
import logging
2223

@@ -36,6 +37,7 @@ def __init__(
3637
messaging: bool = True,
3738
issuer: bool = True,
3839
action_menu: bool = True,
40+
revocations: bool = True,
3941
api_key: str = None,
4042
):
4143

@@ -75,6 +77,11 @@ def __init__(
7577

7678
if action_menu:
7779
self.action_menu = ActionMenuController(self.admin_url, self.client_session)
80+
if revocations:
81+
self.revocations = RevocationController(
82+
self.admin_url,
83+
self.client_session
84+
)
7885

7986
def register_listeners(self, listeners, defaults=True):
8087
if defaults:
@@ -85,7 +92,6 @@ def register_listeners(self, listeners, defaults=True):
8592
if self.proofs:
8693
pub.subscribe(self.proofs.default_handler, "present_proof")
8794

88-
8995
for listener in listeners:
9096
pub.subscribe(listener["handler"], listener["topic"])
9197

@@ -108,11 +114,7 @@ async def handle_webhook(self, topic, payload):
108114
pub.sendMessage(topic, payload=payload)
109115
return web.Response(status=200)
110116

111-
112117
async def terminate(self):
113118
await self.client_session.close()
114119
if self.webhook_site:
115120
await self.webhook_site.stop()
116-
117-
118-

libs/aries-basic-controller/aries_basic_controller/controllers/base.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
EVENT_LOGGER = logging.getLogger("event")
1818

19+
1920
class repr_json:
2021
def __init__(self, val):
2122
self.val = val
@@ -52,7 +53,6 @@ def handle_output(self, *output, source: str = None, **kwargs):
5253
color = None
5354
log_msg(*output, color=color, prefix=self.prefix_str, end=end, **kwargs)
5455

55-
5656
async def admin_request(
5757
self, method, path, json_data=None, text=False, params=None, data=None
5858
) -> ClientResponse:
@@ -71,7 +71,6 @@ async def admin_request(
7171
raise Exception(f"Error decoding JSON: {resp_text}") from e
7272
return resp_text
7373

74-
7574
async def admin_GET(self, path, text=False, params=None) -> ClientResponse:
7675
try:
7776
EVENT_LOGGER.debug("Controller GET %s request to Agent", path)
@@ -95,13 +94,54 @@ async def admin_POST(
9594
)
9695
response = await self.admin_request("POST", path, json_data, text, params, data)
9796
EVENT_LOGGER.debug(
98-
"Response from POST %s received: \n%s", path, repr_json(response),
97+
"Response from POST %s received: \n%s",
98+
path,
99+
repr_json(response),
99100
)
100101
return response
101102
except ClientError as e:
102103
self.log(f"Error during POST {path}: {str(e)}")
103104
raise
104105

106+
async def admin_PATCH(
107+
self, path, json_data=None, text=False, params=None, data=None
108+
) -> ClientResponse:
109+
try:
110+
EVENT_LOGGER.debug(
111+
"Controller PATCH %s request to Agent%s",
112+
path,
113+
(" with data: \n{}".format(repr_json(json_data)) if json_data else ""),
114+
)
115+
response = await self.admin_request("PATCH", path, json_data, text, params, data)
116+
EVENT_LOGGER.debug(
117+
"Response from PATCH %s received: \n%s",
118+
path,
119+
repr_json(response)
120+
)
121+
return response
122+
except ClientError as e:
123+
self.log(f"Error during PATCH {path}: {str(e)}")
124+
raise
125+
126+
async def admin_PUT(
127+
self, path, json_data=None, text=False, params=None, data=None
128+
) -> ClientResponse:
129+
try:
130+
EVENT_LOGGER.debug(
131+
"Controller PUT %s request to Agent%s",
132+
path,
133+
(" with data: \n{}".format(repr_json(json_data)) if json_data else ""),
134+
)
135+
response = await self.admin_request("PUT", path, json_data, text, params, data)
136+
EVENT_LOGGER.debug(
137+
"Response from PUT %s received: \n%s",
138+
path,
139+
repr_json(response)
140+
)
141+
return response
142+
except ClientError as e:
143+
self.log(f"Error during PUT {path}: {str(e)}")
144+
raise
105145

106146
async def admin_DELETE(
107147
self, path, text=False, params=None

libs/aries-basic-controller/aries_basic_controller/controllers/credential.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ async def get_all(self, wql_query: str = None, count: int = None, start: int = N
3030
return await self.admin_GET("/credentials", params=params)
3131

3232
async def is_revoked(self, credential_id):
33-
return await self.admin_GET(f"credential/revoked/{credential_id}")
33+
return await self.admin_GET(f"/credential/revoked/{credential_id}")
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
from .base import BaseController
2+
from ..models.errors import InputError
3+
4+
from aiohttp import (
5+
ClientSession,
6+
)
7+
import asyncio
8+
9+
10+
class RevocationController(BaseController):
11+
12+
def __init__(self, admin_url: str, client_session: ClientSession):
13+
super().__init__(admin_url, client_session)
14+
self.base_url = "/revocation"
15+
self.revocation_states = set([
16+
"init", "generated", "posted", "active", "full"
17+
])
18+
19+
async def revoke_credential(self, cred_ex_id: str = "", cred_rev_id: str = "", rev_reg_id: str = "", publish: bool = False):
20+
"""
21+
Revoke an issued credential.
22+
23+
This method does not publish revocation to the ledger immediately by default - instead, marks it as pending.
24+
Either (cred_ex_id) OR (cred_rev_id AND rev_reg_id) are required.
25+
"""
26+
27+
req_body = {
28+
"publish": publish
29+
}
30+
if cred_ex_id:
31+
req_body["cred_ex_id"] = cred_ex_id
32+
elif cred_rev_id and rev_reg_id:
33+
req_body["cred_rev_id"] = cred_rev_id
34+
req_body["rev_reg_id"] = rev_reg_id
35+
else:
36+
raise InputError(
37+
"Either (cred_ex_id) OR (cred_rev_id AND rev_reg_id) are required."
38+
)
39+
40+
return await self.admin_POST(f"{self.base_url}/revoke", json_data=req_body)
41+
42+
async def publish_pending_revocations(self, pending_revs):
43+
"""
44+
Publish pending revocations to ledger.
45+
46+
Takes in a list of dicts with str key-values, transforms into the correct
47+
API schema.
48+
49+
eg. pending_revs = [{ "sample-rev-reg-id": "sample-cred-rev-id" }]
50+
"""
51+
52+
req_body = {
53+
"rrid2crid": {}
54+
}
55+
56+
for rev in pending_revs:
57+
# check if revocation registry id already exists in map, if so, simply append to array
58+
if rev.rev_reg_id in req_body["rrid2crid"]:
59+
req_body["rrid2crid"][rev.rev_reg_id].append(rev.cred_rev_id)
60+
else:
61+
req_body["rrid2crid"][rev.rev_reg_id] = [rev.cred_rev_id]
62+
63+
return await self.admin_POST(f"{self.base_url}/publish-revocations", json_data=req_body)
64+
65+
async def clear_pending_revocations(self, pending_revs):
66+
"""
67+
Clear pending revocations.
68+
69+
Takes in a list of dicts with str key-values, transforms into the correct
70+
API schema.
71+
72+
eg. pending_revs = [{ "sample-rev-reg-id": "sample-cred-rev-id" }]
73+
"""
74+
75+
req_body = {
76+
"purge": {}
77+
}
78+
79+
for rev in pending_revs:
80+
# check if revocation registry id already exists in map, if so, simply append to array
81+
if rev.rev_reg_id in req_body["purge"]:
82+
req_body["purge"][rev.rev_reg_id].append(rev.cred_rev_id)
83+
else:
84+
req_body["purge"][rev.rev_reg_id] = [rev.cred_rev_id]
85+
86+
return await self.admin_POST(f"{self.base_url}/clear-pending-revocations", json_data=req_body)
87+
88+
async def get_credential_revocation_status(self, cred_ex_id: str, cred_rev_id: str, rev_reg_id: str):
89+
"""
90+
Get credential revocation status.
91+
"""
92+
93+
params = {}
94+
if cred_ex_id:
95+
params["cred_ex_id"] = cred_ex_id
96+
if cred_rev_id:
97+
params["cred_rev_id"] = cred_rev_id
98+
if rev_reg_id:
99+
params["rev_reg_id"] = rev_reg_id
100+
101+
return await self.admin_GET(f"{self.base_url}/credential-record", params=params)
102+
103+
async def get_created_revocation_registries(self, cred_def_id: str, rev_reg_state: str):
104+
"""
105+
Search for matching revocation registries that current agnet created.
106+
"""
107+
108+
params = {}
109+
if cred_def_id:
110+
params["cred_def_id"] = cred_def_id
111+
if rev_reg_state:
112+
if not self.__validate_revocation_registry_state(rev_reg_state):
113+
raise InputError("invalid revocation registry state input")
114+
params["state"] = rev_reg_state
115+
116+
return await self.admin_GET(f"{self.base_url}/registries/created", params=params)
117+
118+
async def get_revocation_registry(self, rev_reg_id: str):
119+
"""
120+
Get revocation registry by revocation registry id
121+
"""
122+
123+
return await self.admin_GET(f"{self.base_url}/registry/{rev_reg_id}")
124+
125+
async def update_revocation_registry_tails_file(self, rev_reg_id: str, tail_file_uri: str):
126+
"""
127+
Update revocation registry by revocation registry id.
128+
"""
129+
130+
req_body = {
131+
"tails_public_uri": tail_file_uri
132+
}
133+
134+
return await self.admin_PATCH(f"{self.base_url}/registry/{rev_reg_id}", json_data=req_body)
135+
136+
async def get_active_revocation_registry_by_cred_def(self, cred_def_id: str):
137+
"""
138+
Get current active revocation registry by credential definition id.
139+
"""
140+
141+
return await self.admin_GET(f"{self.base_url}/active-registry/{cred_def_id}")
142+
143+
async def get_num_credentials_issued_by_revocation_registry(self, rev_reg_id: str):
144+
"""
145+
Get number of credentials issued against revocation registry.
146+
"""
147+
148+
response = await self.admin_GET(f"{self.base_url}/registry/{rev_reg_id}/issued")
149+
return response["result"]
150+
151+
async def create_revocation_registry(self, cred_def_id: str, max_cred_num: int):
152+
"""
153+
Creates a new revocation registry.
154+
"""
155+
156+
req_body = {
157+
"credential_definition_id": cred_def_id,
158+
"max_cred_num": max_cred_num
159+
}
160+
161+
return await self.admin_POST(f"{self.base_url}/create-registry", json_data=req_body)
162+
163+
async def send_revocation_registry_definition(self, rev_reg_id: str):
164+
"""
165+
Send revocation registry definition to ledger.
166+
"""
167+
168+
return await self.admin_POST(f"{self.base_url}/registry/{rev_reg_id}/definition")
169+
170+
async def send_revocation_registry_entry(self, rev_reg_id: str):
171+
"""
172+
Send revocation registry entry to ledger.
173+
"""
174+
175+
return await self.admin_POST(f"{self.base_url}/registry/{rev_reg_id}/entry")
176+
177+
async def upload_revocation_registry_tails_file(self, rev_reg_id: str):
178+
"""
179+
Upload local tails file to server.
180+
"""
181+
182+
return await self.admin_PUT(f"{self.base_url}/registry/{rev_reg_id}/tails-file")
183+
184+
"""
185+
TODO: this API call downloads the Tails file as an octet stream. We need to consider if we
186+
want to build support for this
187+
"""
188+
# async def get_revocation_registry_tails_file(self, rev_reg_id: str):
189+
# """
190+
# Download tails file.
191+
# """
192+
# return await self.admin_GET(f"{self.base_url}/registry/{rev_reg_id}/tails-file")
193+
194+
async def update_revocation_registry_state(self, rev_reg_id: str, state: str):
195+
"""
196+
Set revocation registry state manually.
197+
"""
198+
199+
params = {}
200+
if state:
201+
if not self.__validate_revocation_registry_state(state):
202+
raise InputError("invalid revocation registry state input")
203+
params["state"] = state
204+
205+
return await self.admin_PATCH(f"{self.base_url}/registry/{rev_reg_id}/set-state", params=params)
206+
207+
"""
208+
Private utility methods.
209+
"""
210+
211+
def __validate_revocation_registry_state(self, state: str) -> bool:
212+
"""
213+
Validate if state input is one of init, generated, posted, active, full.
214+
"""
215+
216+
return state in self.revocation_states
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class Error(Exception):
2+
"""Base class for exceptions in this module."""
3+
pass
4+
5+
6+
class InputError(Error):
7+
"""Exception raised for errors in the input.
8+
9+
Attributes:
10+
message -- explanation of the error
11+
"""
12+
13+
def __init__(self, message):
14+
self.message = message

0 commit comments

Comments
 (0)