From c793afbb4faac7ee6159be7a3ae7f0e2408121ed Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 4 Oct 2016 17:58:12 +0200 Subject: [PATCH 1/5] Use alice and bob for users --- tests/conftest.py | 9 +++++++-- tests/test_api.py | 22 +++++++++++----------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index df96e65..d966338 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,12 @@ def app(): @pytest.fixture -def user(client): +def alice(client): + return client.post(url_for('user_views.userapi')).json + + +@pytest.fixture +def bob(client): return client.post(url_for('user_views.userapi')).json @@ -25,7 +30,7 @@ def created_manifestation_resp(client, user): 'datePublished': '29-07-1954', 'url': 'http://localhost/lordoftherings.txt', }, - 'copyrightHolder': user, + 'copyrightHolder': alice, 'work': { 'name': 'The Lord of the Rings Triology', 'author': 'J. R. R. Tolkien', diff --git a/tests/test_api.py b/tests/test_api.py index beec3d6..bc54402 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -10,14 +10,14 @@ def test_create_user(client): assert resp.json['privateKey'] -def test_create_manifestation(client, user): +def test_create_manifestation(client, alice): payload = { 'manifestation': { 'name': 'The Fellowship of the Ring', 'datePublished': '29-07-1954', 'url': 'http://localhost/lordoftherings.txt', }, - 'copyrightHolder': user, + 'copyrightHolder': alice, 'work': { 'name': 'The Lord of the Rings Triology', 'author': 'J. R. R. Tolkien', @@ -70,13 +70,13 @@ def test_create_manifestation(client, user): assert resp.status_code == 200 -def test_create_manifestation_missing_single_attribute(client, user): +def test_create_manifestation_missing_single_attribute(client, alice): payload = { 'manifestation': { 'name': 'The Fellowship of the Ring', 'url': 'http://localhost/lordoftherings.txt', }, - 'copyrightHolder': user, + 'copyrightHolder': alice, 'work': { 'name': 'The Lord of the Rings Triology', 'author': 'J. R. R. Tolkien', @@ -108,12 +108,12 @@ def test_create_manifestation_missing_argument_in_body(client): 'Missing required parameter in the JSON body' -def test_create_right(client, user, created_manifestation_resp): +def test_create_right(client, alice, created_manifestation_resp): copyright_id = created_manifestation_resp['copyright']['@id'] copyright_id = copyright_id.split('../rights/')[1] payload = { - 'currentHolder': user, + 'currentHolder': alice, 'right': { 'license': 'http://www.ascribe.io/terms', }, @@ -126,7 +126,7 @@ def test_create_right(client, user, created_manifestation_resp): '@type': 'Right', 'source': payload['sourceRightId'], 'license': 'http://www.ascribe.io/terms', - } + }, } resp = client.post(url_for('right_views.rightapi'), @@ -140,9 +140,9 @@ def test_create_right(client, user, created_manifestation_resp): assert resp.status_code == 200 -def test_create_right_missing_single_attribute(client, user): +def test_create_right_missing_single_attribute(client, alice): payload = { - 'currentHolder': user, + 'currentHolder': alice, 'right': { 'notALicense': 'this is not a license', }, @@ -156,9 +156,9 @@ def test_create_right_missing_single_attribute(client, user): "'`license` must be provided'" -def test_create_right_missing_argument_in_body(client, user): +def test_create_right_missing_argument_in_body(client, alice): payload = { - 'currentHolder': user, + 'currentHolder': alice, 'right': { 'license': 'http://www.ascribe.io/terms', }, From 104f36c879ff66bca7a7414b6ef98c138b4ef7ee Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 5 Oct 2016 13:37:00 +0200 Subject: [PATCH 2/5] Implement Rights transfers --- tests/conftest.py | 31 +++++++++++++++++++++++- tests/test_api.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ web/views/rights.py | 47 ++++++++++++++++++++++++++++++++++++- 3 files changed, 133 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d966338..b25309a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,12 @@ def bob(client): @pytest.fixture -def created_manifestation_resp(client, user): +def carly(client): + return client.post(url_for('user_views.userapi')).json + + +@pytest.fixture +def created_manifestation_resp(client, alice): import json from time import sleep payload = { @@ -44,3 +49,27 @@ def created_manifestation_resp(client, user): # Sleep for a bit to let the transaction become valid sleep(3) return resp.json + + +@pytest.fixture +def created_derived_right(client, alice, created_manifestation_resp): + import json + from time import sleep + + copyright_id = created_manifestation_resp['copyright']['@id'] + copyright_id = copyright_id.split('../rights/')[1] + payload = { + 'currentHolder': alice, + 'right': { + 'license': 'http://www.ascribe.io/terms', + }, + 'sourceRightId': copyright_id, + } + + resp = client.post(url_for('right_views.rightapi'), + data=json.dumps(payload), + headers={'Content-Type': 'application/json'}) + + # Sleep for a bit to let the transaction become valid + sleep(3) + return resp.json['right'] diff --git a/tests/test_api.py b/tests/test_api.py index bc54402..c37f622 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -169,3 +169,60 @@ def test_create_right_missing_argument_in_body(client, alice): assert resp.status_code == 400 assert resp.json['message']['sourceRightId'] == \ 'Missing required parameter in the JSON body' + + +def test_transfer_right(client, alice, bob, carly, created_derived_right): + from time import sleep + + payload = { + 'rightId': created_derived_right['@id'], + 'rightsAssignment': { + 'action': 'loan', + }, + 'currentHolder': alice, + 'to': bob, + } + + expected = { + 'rightsAssignment': { + '@context': ['', 'http://schema.org/'], + '@type': 'RightsTransferAction', + '@id': '', + 'action': 'loan', + }, + } + + resp = client.post(url_for('right_views.righttransferapi'), + data=json.dumps(payload), + headers={'Content-Type': 'application/json'}) + assert resp.status_code == 200 + assert resp.json == expected + + # Test re-transfer, after waiting for the first transfer to become valid + sleep(3) + retransfer_payload = { + 'rightId': created_derived_right['@id'], + 'rightsAssignment': { + 'action': 'reloan', + }, + 'currentHolder': bob, + 'to': { + 'publicKey': carly['publicKey'], + 'privateKey': None, + } + } + + retransfer_expected = { + 'rightsAssignment': { + '@context': ['', 'http://schema.org/'], + '@type': 'RightsTransferAction', + '@id': '', + 'action': 'reloan', + }, + } + + resp = client.post(url_for('right_views.righttransferapi'), + data=json.dumps(retransfer_payload), + headers={'Content-Type': 'application/json'}) + assert resp.status_code == 200 + assert resp.json == retransfer_expected diff --git a/web/views/rights.py b/web/views/rights.py index 6f94fe0..4241587 100644 --- a/web/views/rights.py +++ b/web/views/rights.py @@ -1,7 +1,7 @@ from flask import Blueprint from flask_restful import reqparse, Resource, Api -from coalaip import CoalaIp +from coalaip import CoalaIp, ModelDataError, entities from coalaip_bigchaindb.plugin import Plugin from web.models import right_model, user_model from web.utils import get_bigchaindb_api_url @@ -42,4 +42,49 @@ def post(self): return res +class RightTransferApi(Resource): + def post(self): + parser = reqparse.RequestParser() + parser.add_argument('rightId', type=str, required=True, + location='json') + parser.add_argument('currentHolder', type=user_model, required=True, + location='json') + parser.add_argument('to', type=user_model, required=True, + location='json') + parser.add_argument('rightsAssignment', type=dict, location='json') + args = parser.parse_args() + + right_id = args['rightId'] + current_holder = args['currentHolder'] + to = args['to'] + rights_assignment = args['rightsAssignment'] + + for user in [current_holder, to]: + user['public_key'] = user.pop('publicKey') + user['private_key'] = user.pop('privateKey') + + # We can't be sure of the type of Right that's given by using just the + # id, so let's assume it's a normal Right first before trying to make a + # Copyright + try: + right = entities.Right.from_persist_id(right_id, + plugin=coalaip.plugin, + force_load=True) + except ModelDataError: + right = entities.Copyright.from_persist_id(right_id, + plugin=coalaip.plugin, + force_load=True) + + res = coalaip.transfer_right(right=right, + rights_assignment_data=rights_assignment, + current_holder=current_holder, + to=to) + + res = {'rightsAssignment': res.to_jsonld()} + + return res + + right_api.add_resource(RightApi, '/rights', strict_slashes=False) +right_api.add_resource(RightTransferApi, '/rights/transfer', + strict_slashes=False) From 3058771cff95e0fed3016ebe269b2d70a6b78fbd Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 5 Oct 2016 13:48:47 +0200 Subject: [PATCH 3/5] Add documentation for rights transfers --- README.md | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6195cce..e030975 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ PAYLOAD: "publicKey": "", "privateKey": "" }, - "sourceRightId": "" + "sourceRightId": "" } RETURNS: @@ -296,3 +296,44 @@ RETURNS: To check if your POST was successful, follow the steps in [registering a manifestation](#was-my-post-to-manifestations-successful) and use the returned Right's data instead. + + +### Transfer a Right + +You may only transfer a Right that you are currently holding. RightsAssignment +entities are automatically created for each transfer and may include additional, +arbitrary attributes if a `rightsAssignment` dict is given in the payload. + +``` +POST /api/v1/rights/transfer +HEADERS {"Content-Type": "application/json"} + +PAYLOAD: +{ + "rightId": "", + "rightsAssignment": { + ... + }, + "currentHolder": { + "publicKey": "", + "privateKey": "" + }, + "to": { + "publicKey": "", + "privateKey": "" + } +} + +RETURNS: +{ + "rightsAssignment": { + "@id": "", + "@type": "RightsTransferAction", + ... (provided `rightsAssignment`) + } +} +``` + +To check if your POST was successful, follow the steps in [registering a +manifestation](#was-my-post-to-manifestations-successful) and use the returned +Right's data instead. From de8f03cd4cab3003f8a3e8371ce5650a69833221 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 10 Feb 2017 11:20:14 +0100 Subject: [PATCH 4/5] Avoid passing secret details of recipient in transfer --- README.md | 6 +++++- tests/test_api.py | 5 ++++- web/models.py | 1 + web/utils.py | 2 +- web/views/rights.py | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e030975..07a2e20 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,7 @@ PAYLOAD: }, "to": { "publicKey": "", - "privateKey": "" + "privateKey": null } } @@ -334,6 +334,10 @@ RETURNS: } ``` +Note that the `to` field in the payload may avoid specifying the new holder's +private details (i.e. `signingKey`), but should still provide the keys needed to +conform to the [user model](#create-users). + To check if your POST was successful, follow the steps in [registering a manifestation](#was-my-post-to-manifestations-successful) and use the returned Right's data instead. diff --git a/tests/test_api.py b/tests/test_api.py index c37f622..e453540 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -180,7 +180,10 @@ def test_transfer_right(client, alice, bob, carly, created_derived_right): 'action': 'loan', }, 'currentHolder': alice, - 'to': bob, + 'to': { + 'publicKey': bob['publicKey'], + 'privateKey': None, + } } expected = { diff --git a/web/models.py b/web/models.py index 397bf40..2217443 100644 --- a/web/models.py +++ b/web/models.py @@ -2,6 +2,7 @@ user_model = parse_model(['publicKey', 'privateKey']) +public_user_model = parse_model(['publicKey']) manifestation_model = parse_model(['name', 'datePublished', 'url']) work_model = parse_model(['name', 'author']) right_model = parse_model(['license']) diff --git a/web/utils.py b/web/utils.py index 2a661a5..8ea95f3 100644 --- a/web/utils.py +++ b/web/utils.py @@ -35,7 +35,7 @@ def _parse_model(inputs): value = inputs[field] except KeyError: raise KeyError('`{}` must be provided'.format(field)) - if bool(value) is not True: + if not value: raise ValueError("`{}`'s value must be defined".format(field)) return inputs return _parse_model diff --git a/web/views/rights.py b/web/views/rights.py index 4241587..d8bfabd 100644 --- a/web/views/rights.py +++ b/web/views/rights.py @@ -3,7 +3,7 @@ from coalaip import CoalaIp, ModelDataError, entities from coalaip_bigchaindb.plugin import Plugin -from web.models import right_model, user_model +from web.models import right_model, public_user_model, user_model from web.utils import get_bigchaindb_api_url @@ -49,7 +49,7 @@ def post(self): location='json') parser.add_argument('currentHolder', type=user_model, required=True, location='json') - parser.add_argument('to', type=user_model, required=True, + parser.add_argument('to', type=public_user_model, required=True, location='json') parser.add_argument('rightsAssignment', type=dict, location='json') args = parser.parse_args() From bbe8ec035627c41467b873ee11262cb3bc7fbb36 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 16 Feb 2017 17:57:21 +0100 Subject: [PATCH 5/5] Add documentation on end-to-end use case --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 07a2e20..d5c00ca 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ interface (runnable in Docker) of the functionalities provided in [pycoalaip](https://github.com/bigchaindb/pycoalaip). -## Whats the status of this? +## What's the status of this? Super-pre-alpha. At the moment all you can do is hit against two endpoints that are being exposed. Minimal error paths are provided. @@ -134,6 +134,22 @@ The API server can be configured with a number of environment variables [see .env_template](./.env_template). +## Quick Guide + +Let's assume you have an artwork you'd like to loan to someone else. These are +the steps you'll need to follow in order to register the work and any +transactions relating to the work via COALA IP. + +1. First, [create some users](#create-users) for both yourself and those + "someone elses" +1. [Register your artwork as a Manifestation](#reigster-a-manifestation) +1. [Derive a special "usage" Right](#register-a-right-allowed-by-another-right-or-copyright) + from the Copyright that resulted from registering your artwork +1. Finally, [transfer that special "usage" Right](#transfer-a-right) to someone + else, optionally including a contract and other information as part of the + transfer. + + ## REST API @@ -300,6 +316,12 @@ Right's data instead. ### Transfer a Right +A contract between a rightsholder and a licensee can be recorded through the +transferring of a Right, via an accompanying RightsAssignment entity. The +transfer transaction recorded on BigchainDB links to that contract (via the +RightsAssignment) and provides a record of the parties, the date of the +transaction, and the contract that applies. + You may only transfer a Right that you are currently holding. RightsAssignment entities are automatically created for each transfer and may include additional, arbitrary attributes if a `rightsAssignment` dict is given in the payload.