Skip to content

Commit ab3fe2e

Browse files
Paul Hewletteccles
authored andcommitted
Make sharing of subject keys easier
Problem: Sharing of self subject keys is not bidirectional. Solution: Add subjects.share() method which efficiently shares self subjects between 2 archivist instances Signed-off-by: Paul Hewlett <[email protected]>
1 parent 63c5613 commit ab3fe2e

File tree

5 files changed

+140
-42
lines changed

5 files changed

+140
-42
lines changed

archivist/subjects.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@
2525
from base64 import b64decode
2626
from json import loads as json_loads
2727
from logging import getLogger
28-
from typing import Any, Optional
28+
from typing import Any, Optional, Tuple
2929

3030
# pylint:disable=cyclic-import # but pylint doesn't understand this feature
3131
from . import archivist
3232

3333
from .constants import (
34-
SUBJECTS_SUBPATH,
3534
SUBJECTS_LABEL,
35+
SUBJECTS_SELF_ID,
36+
SUBJECTS_SUBPATH,
3637
)
3738
from . import subjects_confirmer
3839
from .dictmerge import _deepmerge
@@ -91,20 +92,46 @@ def create(
9192
),
9293
)
9394

94-
def import_subject(self, name: str, subject: Subject) -> Subject:
95+
def share(
96+
self, name: str, other_name: str, other_archivist: archivist.Archivist
97+
) -> Tuple[Subject, Subject]:
98+
"""Import the self subjects from the foreign archivist connection
99+
from another organization - mutually share.
100+
101+
Args:
102+
name (str): display_name of the foreign self subject in this archivist
103+
other_name (str): display_name of the self subject in other archivist
104+
other_archivist (Archivist): Archivist object
105+
106+
Returns:
107+
2-tuple of :class:`Subject` instance
108+
109+
"""
110+
subject1 = self.import_subject(
111+
name, other_archivist.subjects.read(SUBJECTS_SELF_ID)
112+
)
113+
subject2 = other_archivist.subjects.import_subject(
114+
other_name, self.read(SUBJECTS_SELF_ID)
115+
)
116+
subject1 = self.wait_for_confirmation(subject1["identity"])
117+
subject2 = other_archivist.subjects.wait_for_confirmation(subject2["identity"])
118+
119+
return subject1, subject2
120+
121+
def import_subject(self, display_name: str, subject: Subject) -> Subject:
95122
"""Create subject from another subject usually
96123
from another organization.
97124
98125
Args:
99-
name (str): of the subject
126+
display_name (str): display_name of the subject
100127
subject (Subject): Subject object
101128
102129
Returns:
103130
:class:`Subject` instance
104131
105132
"""
106133
return self.create(
107-
name,
134+
display_name,
108135
subject["wallet_pub_key"],
109136
subject["tessera_pub_key"],
110137
)
@@ -289,6 +316,8 @@ def list(
289316
iterable that returns :class:`Subject` instances
290317
291318
"""
319+
320+
LOGGER.debug("List '%s'", display_name)
292321
return (
293322
Subject(**a)
294323
for a in self._archivist.list(

functests/execaccess_policies.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from archivist.archivist import Archivist
1313
from archivist.constants import ASSET_BEHAVIOURS
14-
from archivist.constants import SUBJECTS_SELF_ID
1514
from archivist.proof_mechanism import ProofMechanism
1615
from archivist.utils import get_auth
1716

@@ -296,24 +295,15 @@ def setUp(self):
296295
self.arch_2 = Archivist(getenv("TEST_ARCHIVIST"), auth_2, verify=False)
297296

298297
# creates reciprocal subjects for arch 1 and arch 2.
299-
my_subject_1 = self.arch.subjects.read(SUBJECTS_SELF_ID)
300-
my_subject_2 = self.arch_2.subjects.read(SUBJECTS_SELF_ID)
301-
302298
# subject 1 contains details of subject 2 to be shared
303-
self.subject_1 = self.arch.subjects.import_subject(
299+
self.subject_1, self.subject_2 = self.arch.subjects.share(
304300
"org2_subject",
305-
my_subject_2,
306-
)
307-
308-
# subject 2 contains details of subject 1 to be shared
309-
self.subject_2 = self.arch_2.subjects.import_subject(
310301
"org1_subject",
311-
my_subject_1,
302+
self.arch_2,
312303
)
313-
314-
# check the subjects are confirmed
315-
self.arch.subjects.wait_for_confirmation(self.subject_1["identity"])
316-
self.arch_2.subjects.wait_for_confirmation(self.subject_2["identity"])
304+
print()
305+
print("Org1: subject_1", json_dumps(self.subject_1, indent=4))
306+
print("Org2: subject_2", json_dumps(self.subject_2, indent=4))
317307

318308
def _create_asset(self, label, arch, uuid):
319309
asset_data = deepcopy(REQUEST_EXISTS_ATTACHMENTS)

notebooks/Share_Asset.ipynb

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,13 @@
111111
"metadata": {},
112112
"outputs": [],
113113
"source": [
114-
"def import_subject(acme, weyland):\n",
115-
" \"\"\"Add subjects record for weyland in acme's environment\"\"\"\n",
116-
" subject = acme.subjects.import_subject(\n",
117-
" \"weyland\",\n",
118-
" weyland.subjects.read(SUBJECTS_SELF_ID),\n",
119-
" )\n",
120-
"\n",
121-
" # must wait for confirmation\n",
122-
" acme.subjects.wait_for_confirmation(subject[\"identity\"])\n",
123-
"\n",
124-
" return subject\n"
114+
"def share_subjects(name1, arch1, name2, arch2):\n",
115+
" \"\"\"Share subjects between 2 organizations\"\"\"\n",
116+
" return arch1.subjects.share(\n",
117+
" name1,\n",
118+
" name2,\n",
119+
" arch2,\n",
120+
" )"
125121
]
126122
},
127123
{
@@ -227,8 +223,9 @@
227223
"source": [
228224
"# set a subject for weyland in acme's environment. The identity will be used as a\n",
229225
"# filter in the access permissions of the access_policy.\n",
230-
"weyland_subject_on_acme = import_subject(acme, weyland)\n",
231-
"print(\"weyland_subject\", json_dumps(weyland_subject_on_acme, indent=4))"
226+
"weyland_subject_on_acme, acme_subject_on_weyland = share_subjects(\"weyland on acme\", acme, \"acme_on_weyland\", weyland)\n",
227+
"print(\"weyland_subject on acme\", json_dumps(weyland_subject_on_acme, indent=4))\n",
228+
"print(\"acme_subject on acme\", json_dumps(acme_subject_on_weyland, indent=4))"
232229
]
233230
},
234231
{
@@ -306,14 +303,6 @@
306303
"weyland_asset = weyland.assets.read(acme_asset[\"identity\"])\n",
307304
"print(\"asset read from weyland\", json_dumps(weyland_asset, indent=4))"
308305
]
309-
},
310-
{
311-
"cell_type": "code",
312-
"execution_count": null,
313-
"id": "2486fd97",
314-
"metadata": {},
315-
"outputs": [],
316-
"source": []
317306
}
318307
],
319308
"metadata": {

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ coverage[toml]~=6.4
77
pip-audit~=2.4
88
pycodestyle~=2.9
99
pylint~=2.14
10-
pyright~=1.1.265
10+
pyright~=1.1.271
1111

1212
# uploading to pypi
1313
build~=0.8

unittests/testsubjects.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Test subjects
33
"""
44

5+
from os import environ
56
from unittest import TestCase, mock
67

78
from archivist.archivist import Archivist
@@ -13,15 +14,19 @@
1314
SUBJECTS_LABEL,
1415
)
1516
from archivist.errors import ArchivistBadRequestError, ArchivistUnconfirmedError
17+
from archivist.logger import set_logger
1618

1719
from .mock_response import MockResponse
1820

21+
if "TEST_DEBUG" in environ and environ["TEST_DEBUG"]:
22+
set_logger(environ["TEST_DEBUG"])
1923

2024
# pylint: disable=missing-docstring
2125
# pylint: disable=protected-access
2226
# pylint: disable=unused-variable
2327

2428
DISPLAY_NAME = "Subject display name"
29+
DISPLAY_NAME2 = "Subject display name2"
2530
WALLET_PUB_KEY = [
2631
(
2732
"04c1173bf7844bf1c607b79c18db091b9558ffe581bf132b8cf3b37657230fa321a088"
@@ -47,6 +52,7 @@
4752
)
4853

4954
IDENTITY = f"{SUBJECTS_LABEL}/xxxxxxxx"
55+
IDENTITY2 = f"{SUBJECTS_LABEL}/yyyyyyyy"
5056
SUBPATH = f"{SUBJECTS_SUBPATH}/{SUBJECTS_LABEL}"
5157

5258
RESPONSE = {
@@ -56,6 +62,14 @@
5662
"wallet_address": WALLET_ADDRESSES,
5763
"tessera_pub_key": TESSERA_PUB_KEY,
5864
}
65+
# response when getting from second archivist when sharing
66+
RESPONSE2 = {
67+
"identity": IDENTITY2,
68+
"display_name": DISPLAY_NAME2,
69+
"wallet_pub_key": WALLET_PUB_KEY,
70+
"wallet_address": WALLET_ADDRESSES,
71+
"tessera_pub_key": TESSERA_PUB_KEY,
72+
}
5973
RESPONSE_WITH_PENDING = {
6074
**RESPONSE,
6175
"confirmation_status": "PENDING",
@@ -64,11 +78,20 @@
6478
**RESPONSE,
6579
"confirmation_status": "CONFIRMED",
6680
}
81+
RESPONSE2_WITH_CONFIRMATION = {
82+
**RESPONSE2,
83+
"confirmation_status": "CONFIRMED",
84+
}
6785
REQUEST = {
6886
"display_name": DISPLAY_NAME,
6987
"wallet_pub_key": WALLET_PUB_KEY,
7088
"tessera_pub_key": TESSERA_PUB_KEY,
7189
}
90+
REQUEST2 = {
91+
"display_name": DISPLAY_NAME2,
92+
"wallet_pub_key": WALLET_PUB_KEY,
93+
"tessera_pub_key": TESSERA_PUB_KEY,
94+
}
7295
UPDATE = {"display_name": DISPLAY_NAME}
7396

7497

@@ -81,6 +104,7 @@ class TestSubjects(TestCase):
81104

82105
def setUp(self):
83106
self.arch = Archivist("url", "authauthauth", max_time=1)
107+
self.arch2 = Archivist("url", "authauthauth", max_time=1)
84108

85109
def test_subjects_str(self):
86110
"""
@@ -125,6 +149,72 @@ def test_subjects_create(self):
125149
msg="CREATE method called incorrectly",
126150
)
127151

152+
def test_subjects_share(self):
153+
"""
154+
Test subject share
155+
"""
156+
with mock.patch.object(
157+
self.arch.session, "post"
158+
) as mock_post1, mock.patch.object(
159+
self.arch.session, "get"
160+
) as mock_get1, mock.patch.object(
161+
self.arch2.session, "post"
162+
) as mock_post2, mock.patch.object(
163+
self.arch2.session, "get"
164+
) as mock_get2:
165+
mock_post1.return_value = MockResponse(200, **RESPONSE_WITH_CONFIRMATION)
166+
mock_get1.return_value = MockResponse(200, **RESPONSE_WITH_CONFIRMATION)
167+
mock_post2.return_value = MockResponse(200, **RESPONSE2_WITH_CONFIRMATION)
168+
mock_get2.return_value = MockResponse(200, **RESPONSE2_WITH_CONFIRMATION)
169+
170+
subject1, subject2 = self.arch.subjects.share(
171+
DISPLAY_NAME, DISPLAY_NAME2, self.arch2
172+
)
173+
args, kwargs = mock_post1.call_args
174+
self.assertEqual(
175+
args,
176+
(f"url/{ROOT}/{SUBPATH}",),
177+
msg="CREATE method args called incorrectly",
178+
)
179+
self.assertEqual(
180+
kwargs,
181+
{
182+
"json": REQUEST,
183+
"headers": {
184+
"authorization": "Bearer authauthauth",
185+
},
186+
"verify": True,
187+
},
188+
msg="CREATE method kwargs called incorrectly",
189+
)
190+
self.assertEqual(
191+
subject1,
192+
RESPONSE_WITH_CONFIRMATION,
193+
msg="CREATE method called incorrectly",
194+
)
195+
args, kwargs = mock_post2.call_args
196+
self.assertEqual(
197+
args,
198+
(f"url/{ROOT}/{SUBPATH}",),
199+
msg="CREATE method args called incorrectly",
200+
)
201+
self.assertEqual(
202+
kwargs,
203+
{
204+
"json": REQUEST2,
205+
"headers": {
206+
"authorization": "Bearer authauthauth",
207+
},
208+
"verify": True,
209+
},
210+
msg="CREATE method kwargs called incorrectly",
211+
)
212+
self.assertEqual(
213+
subject2,
214+
RESPONSE2_WITH_CONFIRMATION,
215+
msg="CREATE method called incorrectly",
216+
)
217+
128218
def test_subjects_import_subject(self):
129219
"""
130220
Test subject import_subject

0 commit comments

Comments
 (0)