Skip to content

Commit a71b03e

Browse files
committed
Refactor tuf metadata to model deserializtion (WIP)
TODO: - Clean up diff a bit more - add code docs - write proper commit message Signed-off-by: Lukas Puehringer <[email protected]>
1 parent 5617896 commit a71b03e

File tree

2 files changed

+90
-69
lines changed

2 files changed

+90
-69
lines changed

tests/test_api.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import tempfile
1717
import unittest
1818

19-
from datetime import timedelta
19+
from datetime import datetime, timedelta
2020
from dateutil.relativedelta import relativedelta
2121

2222
IS_PY_VERSION_SUPPORTED = sys.version_info >= (3, 6)
@@ -186,11 +186,11 @@ def test_metadata_base(self):
186186
self.assertEqual(md.signed.version, 1)
187187
md.signed.bump_version()
188188
self.assertEqual(md.signed.version, 2)
189-
self.assertEqual(md.signed.expires, '2030-01-01T00:00:00Z')
189+
self.assertEqual(md.signed.expires, datetime(2030, 1, 1, 0, 0))
190190
md.signed.bump_expiration()
191-
self.assertEqual(md.signed.expires, '2030-01-02T00:00:00Z')
191+
self.assertEqual(md.signed.expires, datetime(2030, 1, 2, 0, 0))
192192
md.signed.bump_expiration(timedelta(days=365))
193-
self.assertEqual(md.signed.expires, '2031-01-02T00:00:00Z')
193+
self.assertEqual(md.signed.expires, datetime(2031, 1, 2, 0, 0))
194194

195195

196196
def test_metadata_snapshot(self):
@@ -218,20 +218,20 @@ def test_metadata_timestamp(self):
218218
timestamp.signed.bump_version()
219219
self.assertEqual(timestamp.signed.version, 2)
220220

221-
self.assertEqual(timestamp.signed.expires, '2030-01-01T00:00:00Z')
221+
self.assertEqual(timestamp.signed.expires, datetime(2030, 1, 1, 0, 0))
222222
timestamp.signed.bump_expiration()
223-
self.assertEqual(timestamp.signed.expires, '2030-01-02T00:00:00Z')
223+
self.assertEqual(timestamp.signed.expires, datetime(2030, 1, 2, 0, 0))
224224
timestamp.signed.bump_expiration(timedelta(days=365))
225-
self.assertEqual(timestamp.signed.expires, '2031-01-02T00:00:00Z')
225+
self.assertEqual(timestamp.signed.expires, datetime(2031, 1, 2, 0, 0))
226226

227227
# Test whether dateutil.relativedelta works, this provides a much
228228
# easier to use interface for callers
229229
delta = relativedelta(days=1)
230230
timestamp.signed.bump_expiration(delta)
231-
self.assertEqual(timestamp.signed.expires, '2031-01-03T00:00:00Z')
231+
self.assertEqual(timestamp.signed.expires, datetime(2031, 1, 3, 0, 0))
232232
delta = relativedelta(years=5)
233233
timestamp.signed.bump_expiration(delta)
234-
self.assertEqual(timestamp.signed.expires, '2036-01-03T00:00:00Z')
234+
self.assertEqual(timestamp.signed.expires, datetime(2036, 1, 3, 0, 0))
235235

236236
hashes = {'sha256': '0ae9664468150a9aa1e7f11feecb32341658eb84292851367fea2da88e8a58dc'}
237237
fileinfo = timestamp.signed.meta['snapshot.json']

tuf/api/metadata.py

Lines changed: 81 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
import tempfile
3030

3131
from securesystemslib.formats import encode_canonical
32-
from securesystemslib.util import load_json_file, persist_temp_file
32+
from securesystemslib.util import (
33+
load_json_file,
34+
load_json_string,
35+
persist_temp_file
36+
)
3337
from securesystemslib.storage import StorageBackendInterface
3438
from securesystemslib.keys import create_signature, verify_signature
3539
from tuf.repository_lib import (
@@ -69,12 +73,34 @@ class Metadata():
6973
]
7074
7175
"""
72-
def __init__(
73-
self, signed: 'Signed' = None, signatures: list = None) -> None:
74-
# TODO: How much init magic do we want?
76+
def __init__(self, signed: 'Signed', signatures: list) -> None:
7577
self.signed = signed
7678
self.signatures = signatures
7779

80+
@classmethod
81+
def from_dict(cls, metadata: JsonDict) -> 'Metadata':
82+
_type = metadata['signed']['_type']
83+
84+
if _type == 'targets':
85+
signed_cls = Targets
86+
elif _type == 'snapshot':
87+
signed_cls = Snapshot
88+
elif _type == 'timestamp':
89+
signed_cls = Timestamp
90+
elif _type == 'root':
91+
# TODO: implement Root class
92+
raise NotImplementedError('Root not yet implemented')
93+
else:
94+
raise ValueError(f'unrecognized metadata type "{_type}"')
95+
96+
return Metadata(
97+
signed=signed_cls.from_dict(metadata['signed']),
98+
signatures=metadata['signatures'])
99+
100+
def from_json(metadata_json: str) -> 'Metadata':
101+
"""Deserialize a json string and pass to from_dict(). """
102+
return Metadata.from_dict(load_json_string(metadata_json))
103+
78104

79105
def to_json(self, compact: bool = False) -> None:
80106
"""Returns the optionally compacted JSON representation of self. """
@@ -174,27 +200,7 @@ def from_json_file(
174200
A TUF Metadata object.
175201
176202
"""
177-
signable = load_json_file(filename, storage_backend)
178-
179-
# TODO: Should we use constants?
180-
# And/or maybe a dispatch table? (<-- maybe too much magic)
181-
_type = signable['signed']['_type']
182-
183-
if _type == 'targets':
184-
inner_cls = Targets
185-
elif _type == 'snapshot':
186-
inner_cls = Snapshot
187-
elif _type == 'timestamp':
188-
inner_cls = Timestamp
189-
elif _type == 'root':
190-
# TODO: implement Root class
191-
raise NotImplementedError('Root not yet implemented')
192-
else:
193-
raise ValueError(f'unrecognized metadata type "{_type}"')
194-
195-
return Metadata(
196-
signed=inner_cls(**signable['signed']),
197-
signatures=signable['signatures'])
203+
return Metadata.from_dict(load_json_file(filename, storage_backend))
198204

199205
def to_json_file(
200206
self, filename: str, compact: bool = False,
@@ -237,41 +243,42 @@ class Signed:
237243
# we keep it to match spec terminology (I often refer to this as "payload",
238244
# or "inner metadata")
239245

240-
# TODO: Re-think default values. It might be better to pass some things
241-
# as args and not es kwargs. Then we'd need to pop those from
242-
# signable["signed"] in read_from_json and pass them explicitly, which
243-
# some say is better than implicit. :)
244246
def __init__(
245-
self, _type: str = None, version: int = 0,
246-
spec_version: str = None, expires: datetime = None
247-
) -> None:
248-
# TODO: How much init magic do we want?
247+
self, _type: str, version: int, spec_version: str,
248+
expires: datetime) -> None:
249249

250250
self._type = _type
251+
self.version = version
251252
self.spec_version = spec_version
253+
self.expires = expires
252254

253-
# We always intend times to be UTC
254-
# NOTE: we could do this with datetime.fromisoformat() but that is not
255-
# available in Python 2.7's datetime
256-
# NOTE: Store as datetime object for convenient handling, use 'expires'
257-
# property to get the TUF metadata format representation
258-
self.__expiration = iso8601.parse_date(expires).replace(tzinfo=None)
259-
255+
# TODO: Should we separate data validation from constructor?
260256
if version < 0:
261257
raise ValueError(f'version must be < 0, got {version}')
258+
262259
self.version = version
263260

264-
@property
265-
def signed_bytes(self) -> bytes:
266-
return encode_canonical(self.as_dict()).encode('UTF-8')
261+
@classmethod
262+
def from_dict(cls, signed_dict) -> 'Signed':
263+
# NOTE: Convert 'expires' TUF metadata string representation
264+
# to datetime object, as the constructor expects it. See 'to_dict' for
265+
# the inverse operation.
266+
signed_dict['expires'] = iso8601.parse_date(
267+
signed_dict['expires']).replace(tzinfo=None)
268+
269+
# NOTE: Any additional conversion of dict metadata should happen here
270+
# or in the corresponding derived class of 'Signed'. E.g. if the
271+
# delegations field
272+
273+
return cls(**signed_dict)
267274

268275
@property
269-
def expires(self) -> str:
270-
return self.__expiration.isoformat() + 'Z'
276+
def signed_bytes(self) -> bytes:
277+
return encode_canonical(self.to_dict()).encode('UTF-8')
271278

272279
def bump_expiration(self, delta: timedelta = timedelta(days=1)) -> None:
273280
"""Increments the expires attribute by the passed timedelta. """
274-
self.__expiration = self.__expiration + delta
281+
self.expires += delta
275282

276283
def bump_version(self) -> None:
277284
"""Increments the metadata version number by 1."""
@@ -283,7 +290,7 @@ def to_dict(self) -> JsonDict:
283290
'_type': self._type,
284291
'version': self.version,
285292
'spec_version': self.spec_version,
286-
'expires': self.expires
293+
'expires': self.expires.isoformat() + 'Z'
287294
}
288295

289296
class Timestamp(Signed):
@@ -305,12 +312,17 @@ class Timestamp(Signed):
305312
}
306313
307314
"""
308-
def __init__(self, meta: JsonDict = None, **kwargs) -> None:
309-
super().__init__(**kwargs)
310-
# TODO: How much init magic do we want?
311-
# TODO: Is there merit in creating classes for dict fields?
315+
def __init__(
316+
self, _type: str, version: int, spec_version: str,
317+
expires: datetime, meta: JsonDict) -> None:
318+
super().__init__(_type, version, spec_version, expires)
319+
# TODO: Add class for meta
312320
self.meta = meta
313321

322+
@classmethod
323+
def from_dict(cls, timestamp_dict):
324+
return super().from_dict(timestamp_dict)
325+
314326
def to_dict(self) -> JsonDict:
315327
"""Returns the JSON-serializable dictionary representation of self. """
316328
json_dict = super().to_dict()
@@ -354,12 +366,17 @@ class Snapshot(Signed):
354366
}
355367
356368
"""
357-
def __init__(self, meta: JsonDict = None, **kwargs) -> None:
358-
# TODO: How much init magic do we want?
359-
# TODO: Is there merit in creating classes for dict fields?
360-
super().__init__(**kwargs)
369+
def __init__(
370+
self, _type: str, version: int, spec_version: str,
371+
expires: datetime, meta: JsonDict) -> None:
372+
super().__init__(_type, version, spec_version, expires)
373+
# TODO: Add class for meta
361374
self.meta = meta
362375

376+
@classmethod
377+
def from_dict(cls, snapshot_dict):
378+
return super().from_dict(snapshot_dict)
379+
363380
def to_dict(self) -> JsonDict:
364381
"""Returns the JSON-serializable dictionary representation of self. """
365382
json_dict = super().to_dict()
@@ -437,14 +454,18 @@ class Targets(Signed):
437454
438455
"""
439456
def __init__(
440-
self, targets: JsonDict = None, delegations: JsonDict = None,
441-
**kwargs) -> None:
442-
# TODO: How much init magic do we want?
443-
# TODO: Is there merit in creating classes for dict fields?
444-
super().__init__(**kwargs)
457+
self, _type: str, version: int, spec_version: str,
458+
expires: datetime, targets: JsonDict, delegations: JsonDict
459+
) -> None:
460+
super().__init__(_type, version, spec_version, expires)
461+
# TODO: Add class for meta
445462
self.targets = targets
446463
self.delegations = delegations
447464

465+
@classmethod
466+
def from_dict(cls, targets_dict):
467+
return super().from_dict(targets_dict)
468+
448469
def to_dict(self) -> JsonDict:
449470
"""Returns the JSON-serializable dictionary representation of self. """
450471
json_dict = super().to_dict()

0 commit comments

Comments
 (0)