Skip to content

Commit

Permalink
Merge pull request #144 from andrewwhitehead/taa-routes
Browse files Browse the repository at this point in the history
Add initial TAA acceptance admin routes
  • Loading branch information
nrempel authored Dec 10, 2019
2 parents bd850d7 + 43986a6 commit 52238c9
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 32 deletions.
22 changes: 20 additions & 2 deletions aries_cloudagent/ledger/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ async def update_endpoint_for_did(self, did: str, endpoint: str) -> bool:
"""

@abstractmethod
async def register_nym(self, did: str, verkey: str, alias: str = None,
role: str = None):
async def register_nym(
self, did: str, verkey: str, alias: str = None, role: str = None
):
"""
Register a nym on the ledger.
Expand All @@ -68,3 +69,20 @@ def did_to_nym(self, did: str) -> str:
"""Remove the ledger's DID prefix to produce a nym."""
if did:
return re.sub(r"^did:\w+:", "", did)

async def get_txn_author_agreement(self, reload: bool = False):
"""Get the current transaction author agreement, fetching it if necessary."""

async def fetch_txn_author_agreement(self):
"""Fetch the current AML and TAA from the ledger."""

async def accept_txn_author_agreement(
self, taa_record: dict, mechanism: str, accept_time: int = None
):
"""Save a new record recording the acceptance of the TAA."""

async def get_latest_txn_author_acceptance(self):
"""Look up the latest TAA acceptance."""

def taa_digest(self, version: str, text: str):
"""Generate the digest of a TAA record."""
20 changes: 12 additions & 8 deletions aries_cloudagent/ledger/indy.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,10 +742,11 @@ async def fetch_txn_author_agreement(self):
)
response_json = await self._submit(get_taa_req, public_did=public_did)
taa_found = (json.loads(response_json))["result"]["data"]
taa_required = taa_found and taa_found["text"]
taa_required = bool(taa_found and taa_found["text"])
if taa_found:
taa_plaintext = taa_found["version"] + taa_found["text"]
taa_found["digest"] = sha256(taa_plaintext.encode("utf-8")).digest().hex()
taa_found["digest"] = self.taa_digest(
taa_found["version"], taa_found["text"]
)

return {
"aml_record": aml_found,
Expand All @@ -764,12 +765,15 @@ def taa_rough_timestamp(self) -> int:
"""
return int(datetime.combine(date.today(), datetime.min.time()).timestamp())

def taa_digest(self, version: str, text: str):
"""Generate the digest of a TAA record."""
if not version or not text:
raise ValueError("Bad input for TAA digest")
taa_plaintext = version + text
return sha256(taa_plaintext.encode("utf-8")).digest().hex()

async def accept_txn_author_agreement(
self,
taa_record: dict,
mechanism: str,
accept_time: int = None,
store: bool = False,
self, taa_record: dict, mechanism: str, accept_time: int = None
):
"""Save a new record recording the acceptance of the TAA."""
if not accept_time:
Expand Down
150 changes: 137 additions & 13 deletions aries_cloudagent/ledger/routes.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,84 @@
"""Ledger admin routes."""

from aiohttp import web
from aiohttp_apispec import docs
from aiohttp_apispec import docs, request_schema, response_schema

from marshmallow import fields, Schema

from .base import BaseLedger
from .error import LedgerTransactionError


class AMLRecordSchema(Schema):
"""Ledger AML record."""

version = fields.Str()
aml = fields.Dict(fields.Str(), fields.Str())
amlContext = fields.Str()


class TAARecordSchema(Schema):
"""Ledger TAA record."""

version = fields.Str()
text = fields.Str()
digest = fields.Str()


class TAAAcceptanceSchema(Schema):
"""TAA acceptance record."""

mechanism = fields.Str()
time = fields.Int()


class TAAInfoSchema(Schema):
"""Transaction author agreement info."""

aml_record = fields.Nested(AMLRecordSchema())
taa_record = fields.Nested(TAARecordSchema())
taa_required = fields.Bool()
taa_accepted = fields.Nested(TAAAcceptanceSchema())


class TAAResultSchema(Schema):
"""Result schema for a transaction author agreement."""

result = fields.Nested(TAAInfoSchema())


class TAAAcceptSchema(Schema):
"""Input schema for accepting the TAA."""

version = fields.Str()
text = fields.Str()
mechanism = fields.Str()


@docs(
tags=["ledger"],
summary="Send a NYM registration to the ledger.",
parameters=[
{"name": "did", "in": "query", "schema": {"type": "string"},
"required": True},
{"name": "verkey", "in": "query", "schema": {"type": "string"},
"required": True},
{"name": "alias", "in": "query", "schema": {"type": "string"},
"required": False},
{"name": "role", "in": "query", "schema": {"type": "string"},
"required": False}
]
{"name": "did", "in": "query", "schema": {"type": "string"}, "required": True},
{
"name": "verkey",
"in": "query",
"schema": {"type": "string"},
"required": True,
},
{
"name": "alias",
"in": "query",
"schema": {"type": "string"},
"required": False,
},
{
"name": "role",
"in": "query",
"schema": {"type": "string"},
"required": False,
},
],
)
async def register_ledger_nym(request: web.BaseRequest):
"""
Expand Down Expand Up @@ -54,7 +113,7 @@ async def register_ledger_nym(request: web.BaseRequest):
summary="Get the verkey for a DID from the ledger.",
parameters=[
{"name": "did", "in": "query", "schema": {"type": "string"}, "required": True}
]
],
)
async def get_did_verkey(request: web.BaseRequest):
"""
Expand Down Expand Up @@ -82,7 +141,7 @@ async def get_did_verkey(request: web.BaseRequest):
summary="Get the endpoint for a DID from the ledger.",
parameters=[
{"name": "did", "in": "query", "schema": {"type": "string"}, "required": True}
]
],
)
async def get_did_endpoint(request: web.BaseRequest):
"""
Expand All @@ -105,13 +164,78 @@ async def get_did_endpoint(request: web.BaseRequest):
return web.json_response({"endpoint": r})


@docs(tags=["ledger"], summary="Fetch the current transaction author agreement, if any")
@response_schema(TAAResultSchema, 200)
async def ledger_get_taa(request: web.BaseRequest):
"""
Request handler for fetching the transaction author agreement.
Args:
request: aiohttp request object
Returns:
The TAA information including the AML
"""
context = request.app["request_context"]
ledger: BaseLedger = await context.inject(BaseLedger, required=False)
if not ledger or ledger.LEDGER_TYPE != "indy":
raise web.HTTPForbidden()

taa_info = await ledger.get_txn_author_agreement()
accepted = None
if taa_info["taa_required"]:
accept_record = await ledger.get_latest_txn_author_acceptance()
if accept_record:
accepted = {
"mechanism": accept_record["mechanism"],
"time": accept_record["time"],
}
taa_info["taa_accepted"] = accepted

return web.json_response({"result": taa_info})


@docs(tags=["ledger"], summary="Accept the transaction author agreement")
@request_schema(TAAAcceptSchema)
async def ledger_accept_taa(request: web.BaseRequest):
"""
Request handler for accepting the current transaction author agreement.
Args:
request: aiohttp request object
Returns:
The DID list response
"""
context = request.app["request_context"]
ledger: BaseLedger = await context.inject(BaseLedger, required=False)
if not ledger or ledger.LEDGER_TYPE != "indy":
raise web.HTTPForbidden()

accept_input = await request.json()
taa_info = await ledger.get_txn_author_agreement()
if not taa_info["taa_required"]:
raise web.HTTPBadRequest()
taa_record = {
"version": accept_input["version"],
"text": accept_input["text"],
"digest": ledger.taa_digest(accept_input["version"], accept_input["text"]),
}
await ledger.accept_txn_author_agreement(taa_record, accept_input["mechanism"])
return web.json_response({})


async def register(app: web.Application):
"""Register routes."""

app.add_routes(
[
web.post("/ledger/register-nym", register_ledger_nym),
web.get("/ledger/did-verkey", get_did_verkey),
web.get("/ledger/did-endpoint", get_did_endpoint)
web.get("/ledger/did-endpoint", get_did_endpoint),
web.get("/ledger/taa", ledger_get_taa),
web.post("/ledger/taa/accept", ledger_accept_taa),
]
)
79 changes: 70 additions & 9 deletions aries_cloudagent/ledger/tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from asynctest import mock as async_mock
import pytest

from aiohttp.web import HTTPForbidden
from aiohttp.web import HTTPBadRequest, HTTPForbidden

from ...config.injection_context import InjectionContext
from ...ledger.base import BaseLedger
Expand Down Expand Up @@ -37,7 +37,6 @@ async def test_missing_ledger(self):
with self.assertRaises(HTTPForbidden):
await test_module.get_did_endpoint(request)


async def test_get_verkey(self):
request = async_mock.MagicMock()
request.app = self.app
Expand All @@ -48,7 +47,7 @@ async def test_get_verkey(self):
self.ledger.get_key_for_did.return_value = self.test_verkey
result = await test_module.get_did_verkey(request)
json_response.assert_called_once_with(
{"verkey": self.ledger.get_key_for_did.return_value }
{"verkey": self.ledger.get_key_for_did.return_value}
)
assert result is json_response.return_value

Expand All @@ -66,20 +65,82 @@ async def test_get_endpoint(self):
)
assert result is json_response.return_value


async def test_register_nym(self):
request = async_mock.MagicMock()
request.app = self.app
request.query = {
"did": self.test_did,
"verkey": self.test_verkey
}
request.query = {"did": self.test_did, "verkey": self.test_verkey}
with async_mock.patch.object(
test_module.web, "json_response", async_mock.Mock()
) as json_response:
self.ledger.register_nym.return_value = True
result = await test_module.register_ledger_nym(request)
json_response.assert_called_once_with(
{"success": self.ledger.register_nym.return_value }
{"success": self.ledger.register_nym.return_value}
)
assert result is json_response.return_value

async def test_taa_forbidden(self):
request = async_mock.MagicMock()
request.app = self.app

with self.assertRaises(HTTPForbidden):
await test_module.ledger_get_taa(request)

async def test_get_taa(self):
request = async_mock.MagicMock()
request.app = self.app
with async_mock.patch.object(
test_module.web, "json_response", async_mock.Mock()
) as json_response:
self.ledger.LEDGER_TYPE = "indy"
self.ledger.get_txn_author_agreement.return_value = {"taa_required": False}
self.ledger.get_latest_txn_author_acceptance.return_value = None
result = await test_module.ledger_get_taa(request)
json_response.assert_called_once_with(
{"result": {"taa_accepted": None, "taa_required": False}}
)
assert result is json_response.return_value

async def test_taa_accept_not_required(self):
request = async_mock.MagicMock()
request.app = self.app
request.json = async_mock.CoroutineMock(
return_value={
"version": "version",
"text": "text",
"mechanism": "mechanism",
}
)

with self.assertRaises(HTTPBadRequest):
self.ledger.LEDGER_TYPE = "indy"
self.ledger.get_txn_author_agreement.return_value = {"taa_required": False}
await test_module.ledger_accept_taa(request)

async def test_accept_taa(self):
request = async_mock.MagicMock()
request.app = self.app
request.json = async_mock.CoroutineMock(
return_value={
"version": "version",
"text": "text",
"mechanism": "mechanism",
}
)

with async_mock.patch.object(
test_module.web, "json_response", async_mock.Mock()
) as json_response:
self.ledger.LEDGER_TYPE = "indy"
self.ledger.get_txn_author_agreement.return_value = {"taa_required": True}
result = await test_module.ledger_accept_taa(request)
json_response.assert_called_once_with({})
self.ledger.accept_txn_author_agreement.assert_awaited_once_with(
{
"version": "version",
"text": "text",
"digest": self.ledger.taa_digest.return_value,
},
"mechanism",
)
assert result is json_response.return_value

0 comments on commit 52238c9

Please sign in to comment.