From a141580e0e81be1b8cedf84b39ab82c576e72fc9 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Tue, 24 Nov 2020 12:49:58 +0200 Subject: [PATCH 1/9] New API: Add root use case in couple of tests Add a use case for the root class to be tested in test_generic_read and test_read_write_read_compare tests in test_apy.py Signed-off-by: Martin Vrachev --- tests/test_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 5d2dce0269..7ed6a61e54 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -33,6 +33,7 @@ def setUpModule(): import tuf.exceptions from tuf.api.metadata import ( Metadata, + Root, Snapshot, Timestamp, Targets @@ -92,6 +93,7 @@ def tearDownClass(cls): def test_generic_read(self): for metadata, inner_metadata_cls in [ + ('root', Root), ('snapshot', Snapshot), ('timestamp', Timestamp), ('targets', Targets)]: @@ -137,7 +139,7 @@ def test_compact_json(self): def test_read_write_read_compare(self): - for metadata in ['snapshot', 'timestamp', 'targets']: + for metadata in ['root', 'snapshot', 'timestamp', 'targets']: path = os.path.join(self.repo_dir, 'metadata', metadata + '.json') metadata_obj = Metadata.from_json_file(path) From 7cfd100eeb37a7878741722a1b766b5313022287 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Tue, 24 Nov 2020 15:31:16 +0200 Subject: [PATCH 2/9] New API: Timestamp - make length and hashes optional As per the specification (v1.0.1) length and hashes fields in timestamp metadata are optional. We have implement this in the older API (see https://github.com/theupdateframework/tuf/pull/1031) and we should implement it in the new API. Signed-off-by: Martin Vrachev --- tests/test_api.py | 8 ++++++++ tuf/api/metadata.py | 17 ++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 7ed6a61e54..5e1a4ff4f3 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -232,6 +232,10 @@ def test_metadata_snapshot(self): self.assertNotEqual(snapshot.signed.meta, fileinfo) snapshot.signed.update('role1', 2, 123, hashes) self.assertEqual(snapshot.signed.meta, fileinfo) + # Update only version. Length and hashes are optional. + snapshot.signed.update('role1', 3) + fileinfo['role1.json'] = {'version': 3} + self.assertEqual(snapshot.signed.meta, fileinfo) def test_metadata_timestamp(self): @@ -267,6 +271,10 @@ def test_metadata_timestamp(self): self.assertNotEqual(timestamp.signed.meta['snapshot.json'], fileinfo) timestamp.signed.update(2, 520, hashes) self.assertEqual(timestamp.signed.meta['snapshot.json'], fileinfo) + # Update only version. Length and hashes are optional. + timestamp.signed.update(3) + fileinfo = {'version': 3} + self.assertEqual(timestamp.signed.meta['snapshot.json'], fileinfo) def test_metadata_root(self): diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index a747be6d13..f306baae7f 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -431,7 +431,7 @@ class Timestamp(Signed): '': '', '': '', ... - } + } // optional } } @@ -455,13 +455,16 @@ def to_dict(self) -> JsonDict: # Modification. - def update(self, version: int, length: int, hashes: JsonDict) -> None: + def update(self, version: int, length: Optional[int] = None, + hashes: Optional[JsonDict] = None) -> None: """Assigns passed info about snapshot metadata to meta dict. """ - self.meta['snapshot.json'] = { - 'version': version, - 'length': length, - 'hashes': hashes - } + + self.meta['snapshot.json'] = {'version': version} + if length is not None: + self.meta['snapshot.json']['length'] = length + + if hashes is not None: + self.meta['snapshot.json']['hashes'] = hashes class Snapshot(Signed): From b3e98b9640900d3fc463bab7f43b64b988f4259b Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Tue, 1 Dec 2020 16:04:49 +0200 Subject: [PATCH 3/9] NEW API: Fix documentation indentation From the reST/sphinx docs: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks I added new lines and an identation where it was missed. Signed-off-by: Martin Vrachev --- tuf/api/metadata.py | 97 +++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index f306baae7f..0e23ce5962 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -342,30 +342,33 @@ class Root(Signed): supports consistent snapshots. keys: A dictionary that contains a public key store used to verify top level roles metadata signatures:: - { - '': { - 'keytype': '', - 'scheme': '', - 'keyid_hash_algorithms': [ - '', - '' - ... - ], - 'keyval': { - 'public': '' - } + + { + '': { + 'keytype': '', + 'scheme': '', + 'keyid_hash_algorithms': [ + '', + '' + ... + ], + 'keyval': { + 'public': '' + } + }, + ... }, - ... - }, + roles: A dictionary that contains a list of signing keyids and a signature threshold for each top level role:: - { - '': { - 'keyids': ['', ...], - 'threshold': , - }, - ... - } + + { + '': { + 'keyids': ['', ...], + 'threshold': , + }, + ... + } """ # TODO: determine an appropriate value for max-args and fix places where @@ -548,34 +551,34 @@ class Targets(Signed): roles and public key store used to verify their metadata signatures:: - { - 'keys' : { - '': { - 'keytype': '', - 'scheme': '', - 'keyid_hash_algorithms': [ - '', - '' - ... - ], - 'keyval': { - 'public': '' - } + { + 'keys' : { + '': { + 'keytype': '', + 'scheme': '', + 'keyid_hash_algorithms': [ + '', + '' + ... + ], + 'keyval': { + 'public': '' + } + }, + ... }, + 'roles': [ + { + 'name': '', + 'keyids': ['', ...], + 'threshold': , + 'terminating': , + 'path_hash_prefixes': ['', ... ], // or + 'paths' : ['PATHPATTERN', ... ], + }, ... - }, - 'roles': [ - { - 'name': '', - 'keyids': ['', ...], - 'threshold': , - 'terminating': , - 'path_hash_prefixes': ['', ... ], // or - 'paths' : ['PATHPATTERN', ... ], - }, - ... - ] - } + ] + } """ # TODO: determine an appropriate value for max-args and fix places where From cbf466407494ef2a98d3a413ba1ad176c909406b Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Mon, 18 Jan 2021 18:35:25 +0200 Subject: [PATCH 4/9] New API: Document that _type is not protected Signed-off-by: Martin Vrachev --- tuf/api/metadata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 0e23ce5962..35d9b277d3 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -273,6 +273,8 @@ def __init__( self, _type: str, version: int, spec_version: str, expires: datetime) -> None: + # "_type" is not actually meant to be "protected" in the OOP sense + # but rather is a workaround to not shadow the built-in "type" name. self._type = _type self.version = version self.spec_version = spec_version From 8cc785cc4616293e1de7aad87166fdd727156f78 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Mon, 23 Nov 2020 20:17:29 +0200 Subject: [PATCH 5/9] New API: Add MetadataInfo class Add MetaMetaFileFile class to tuf.api.metadata module. This class is be used for the "meta" field in Timestamp and Snapshot metadata files described the specification. Signed-off-by: Martin Vrachev --- tests/test_api.py | 28 +++++----- tuf/api/metadata.py | 124 +++++++++++++++++++++++++++++--------------- 2 files changed, 97 insertions(+), 55 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 5e1a4ff4f3..5f7ddfaa38 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -33,6 +33,7 @@ def setUpModule(): import tuf.exceptions from tuf.api.metadata import ( Metadata, + MetadataInfo, Root, Snapshot, Timestamp, @@ -224,17 +225,18 @@ def test_metadata_snapshot(self): # Create a dict representing what we expect the updated data to be fileinfo = copy.deepcopy(snapshot.signed.meta) hashes = {'sha256': 'c2986576f5fdfd43944e2b19e775453b96748ec4fe2638a6d2f32f1310967095'} - fileinfo['role1.json']['version'] = 2 - fileinfo['role1.json']['hashes'] = hashes - fileinfo['role1.json']['length'] = 123 + fileinfo['role1.json'].version = 2 + fileinfo['role1.json'].hashes = hashes + fileinfo['role1.json'].length = 123 self.assertNotEqual(snapshot.signed.meta, fileinfo) snapshot.signed.update('role1', 2, 123, hashes) self.assertEqual(snapshot.signed.meta, fileinfo) + # Update only version. Length and hashes are optional. snapshot.signed.update('role1', 3) - fileinfo['role1.json'] = {'version': 3} + fileinfo['role1.json'] = MetadataInfo(3) self.assertEqual(snapshot.signed.meta, fileinfo) @@ -263,18 +265,18 @@ def test_metadata_timestamp(self): self.assertEqual(timestamp.signed.expires, datetime(2036, 1, 3, 0, 0)) hashes = {'sha256': '0ae9664468150a9aa1e7f11feecb32341658eb84292851367fea2da88e8a58dc'} - fileinfo = copy.deepcopy(timestamp.signed.meta['snapshot.json']) - fileinfo['hashes'] = hashes - fileinfo['version'] = 2 - fileinfo['length'] = 520 - - self.assertNotEqual(timestamp.signed.meta['snapshot.json'], fileinfo) + fileinfo = copy.deepcopy(timestamp.signed.meta) + fileinfo['snapshot.json'].hashes = hashes + fileinfo['snapshot.json'].version = 2 + fileinfo['snapshot.json'].length = 520 + self.assertNotEqual(timestamp.signed.meta, fileinfo) timestamp.signed.update(2, 520, hashes) - self.assertEqual(timestamp.signed.meta['snapshot.json'], fileinfo) + self.assertEqual(timestamp.signed.meta, fileinfo) + # Update only version. Length and hashes are optional. timestamp.signed.update(3) - fileinfo = {'version': 3} - self.assertEqual(timestamp.signed.meta['snapshot.json'], fileinfo) + fileinfo['snapshot.json'] = MetadataInfo(version=3) + self.assertEqual(timestamp.signed.meta, fileinfo) def test_metadata_root(self): diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 35d9b277d3..be3c0945e1 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -420,6 +420,50 @@ def remove_key(self, role: str, keyid: str) -> None: del self.keys[keyid] +class MetadataInfo: + """A container with information about a particular metadata file. + Instances of MetadataInfo are used as values in a dictionary called + "meta" in Timestamp and Snapshot. + + Attributes: + version: An integer indicating the version of the metadata file. + length: An optional integer indicating the length of the metadata file. + hashes: A optional dictionary containing hash algorithms and the + hashes resulting from applying them over the metadata file.:: + + 'hashes': { + '': '', + '': '', + ... + } + + """ + + def __init__(self, version: int, length: Optional[int] = None, + hashes: Optional[JsonDict] = None) -> None: + self.version = version + self.length = length + self.hashes = hashes + + + def __eq__(self, other: 'MetadataInfo') -> bool: + """Compare objects by their values instead of by their addresses.""" + return (self.version == other.version and + self.length == other.length and + self.hashes == other.hashes) + + + def to_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ + json_dict = {'version': self.version} + + if self.length is not None: + json_dict['length'] = self.length + + if self.hashes is not None: + json_dict['hashes'] = self.hashes + + return json_dict class Timestamp(Signed): @@ -429,23 +473,14 @@ class Timestamp(Signed): meta: A dictionary that contains information about snapshot metadata:: { - 'snapshot.json': { - 'version': , - 'length': , // optional - 'hashes': { - '': '', - '': '', - ... - } // optional - } + 'snapshot.json': } """ def __init__( self, _type: str, version: int, spec_version: str, - expires: datetime, meta: JsonDict) -> None: + expires: datetime, meta: Dict[str, MetadataInfo]) -> None: super().__init__(_type, version, spec_version, expires) - # TODO: Add class for meta self.meta = meta @@ -454,22 +489,29 @@ def to_dict(self) -> JsonDict: """Returns the JSON-serializable dictionary representation of self. """ json_dict = super().to_dict() json_dict.update({ - 'meta': self.meta + 'meta': { + 'snapshot.json': self.meta['snapshot.json'].to_dict() + } }) return json_dict + @classmethod + def from_dict(cls, signed_dict: JsonDict) -> 'Timestamp': + """Creates Timestamp object from its JSON/dict representation. """ + + signed_dict['meta']['snapshot.json'] = MetadataInfo( + **signed_dict['meta']['snapshot.json']) + + return super().from_dict(signed_dict) + + # Modification. def update(self, version: int, length: Optional[int] = None, hashes: Optional[JsonDict] = None) -> None: """Assigns passed info about snapshot metadata to meta dict. """ - self.meta['snapshot.json'] = {'version': version} - if length is not None: - self.meta['snapshot.json']['length'] = length - - if hashes is not None: - self.meta['snapshot.json']['hashes'] = hashes + self.meta['snapshot.json'] = MetadataInfo(version, length, hashes) class Snapshot(Signed): @@ -479,38 +521,41 @@ class Snapshot(Signed): meta: A dictionary that contains information about targets metadata:: { - 'targets.json': { - 'version': , - 'length': , // optional - 'hashes': { - '': '', - '': '', - ... - } // optional - }, - '.json': { - ... - }, - '.json': { - ... - }, + 'targets.json': , + '.json': , + '.json': , ... } """ def __init__( self, _type: str, version: int, spec_version: str, - expires: datetime, meta: JsonDict) -> None: + expires: datetime, meta: Dict[str, MetadataInfo]) -> None: super().__init__(_type, version, spec_version, expires) - # TODO: Add class for meta self.meta = meta + + @classmethod + def from_dict(cls, signed_dict: JsonDict) -> 'Snapshot': + """Creates Snapshot object from its JSON/dict representation. """ + + for meta_path in signed_dict['meta'].keys(): + signed_dict['meta'][meta_path] = MetadataInfo( + **signed_dict['meta'][meta_path]) + + return super().from_dict(signed_dict) + + # Serialization. def to_dict(self) -> JsonDict: """Returns the JSON-serializable dictionary representation of self. """ json_dict = super().to_dict() + meta_dict = {} + for meta_path, meta_info in self.meta.items(): + meta_dict[meta_path] = meta_info.to_dict() + json_dict.update({ - 'meta': self.meta + 'meta': meta_dict }) return json_dict @@ -522,12 +567,7 @@ def update( """Assigns passed (delegated) targets role info to meta dict. """ metadata_fn = f'{rolename}.json' - self.meta[metadata_fn] = {'version': version} - if length is not None: - self.meta[metadata_fn]['length'] = length - - if hashes is not None: - self.meta[metadata_fn]['hashes'] = hashes + self.meta[metadata_fn] = MetadataInfo(version, length, hashes) class Targets(Signed): From 6259f31f30911f91977eccdad03d6e53d6fa4882 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Wed, 25 Nov 2020 18:06:54 +0200 Subject: [PATCH 6/9] New API: Add TargetFile class Add a new TargetFile class to tuf.api.metadata module make Targets class to use it. This class will contain information about the "targets" field from targets.json Also, update the tests for that change. Signed-off-by: Martin Vrachev --- tests/test_api.py | 10 +++--- tuf/api/metadata.py | 88 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 5f7ddfaa38..939434fbbe 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -37,7 +37,8 @@ def setUpModule(): Root, Snapshot, Timestamp, - Targets + Targets, + TargetInfo ) from securesystemslib.interface import ( @@ -324,15 +325,12 @@ def test_metadata_targets(self): "sha512": "ef5beafa16041bcdd2937140afebd485296cd54f7348ecd5a4d035c09759608de467a7ac0eb58753d0242df873c305e8bffad2454aa48f44480f15efae1cacd0" }, - fileinfo = { - 'hashes': hashes, - 'length': 28 - } + fileinfo = TargetInfo(length=28, hashes=hashes) # Assert that data is not aleady equal self.assertNotEqual(targets.signed.targets[filename], fileinfo) # Update an already existing fileinfo - targets.signed.update(filename, fileinfo) + targets.signed.update(filename, fileinfo.to_dict()) # Verify that data is updated self.assertEqual(targets.signed.targets[filename], fileinfo) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index be3c0945e1..09bdfd9d61 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -570,6 +570,58 @@ def update( self.meta[metadata_fn] = MetadataInfo(version, length, hashes) +class TargetInfo: + """A container with information about a particular target file. + Instances of TargetInfo are used as values in a dictionary + called "targets" in Targets. + + Attributes: + length: An integer indicating the length of the target file. + hashes: A dictionary containing hash algorithms and the + hashes resulted from applying them over the target file:: + + 'hashes': { + '': '', + '': '', + ... + } + + custom: An optional dictionary which may include version numbers, + dependencies, or any other data that the application wants + to include to describe the target file:: + + 'custom': { + 'type': 'metadata', + 'file_permissions': '0644', + ... + } // optional + + """ + + def __init__(self, length: int, hashes: JsonDict, + custom: Optional[JsonDict] = None) -> None: + self.length = length + self.hashes = hashes + self.custom = custom + + + def __eq__(self, other: 'TargetInfo') -> bool: + """Compare objects by their values instead of by their addresses.""" + return (self.length == other.length and + self.hashes == other.hashes and + self.custom == other.custom) + + + def to_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ + json_dict = {'length': self.length, 'hashes': self.hashes} + + if self.custom is not None: + json_dict['custom'] = self.custom + + return json_dict + + class Targets(Signed): """A container for the signed part of targets metadata. @@ -577,15 +629,7 @@ class Targets(Signed): targets: A dictionary that contains information about target files:: { - '': { - 'length': , - 'hashes': { - '': '', - '': '', - ... - }, - 'custom': // optional - }, + '': , ... } @@ -629,20 +673,35 @@ class Targets(Signed): # pylint: disable=too-many-arguments def __init__( self, _type: str, version: int, spec_version: str, - expires: datetime, targets: JsonDict, delegations: JsonDict - ) -> None: + expires: datetime, targets: Dict[str, TargetInfo], + delegations: JsonDict) -> None: super().__init__(_type, version, spec_version, expires) - # TODO: Add class for meta self.targets = targets + + # TODO: Add Key and Role classes self.delegations = delegations + @classmethod + def from_dict(cls, signed_dict: JsonDict) -> 'Targets': + """Creates Targets object from its JSON/dict representation. """ + for target_path in signed_dict['targets'].keys(): + signed_dict['targets'][target_path] = TargetInfo( + **signed_dict['targets'][target_path]) + + return super().from_dict(signed_dict) + + # Serialization. def to_dict(self) -> JsonDict: """Returns the JSON-serializable dictionary representation of self. """ json_dict = super().to_dict() + target_dict = {} + for target_path, target_file_obj in self.targets.items(): + target_dict[target_path] = target_file_obj.to_dict() + json_dict.update({ - 'targets': self.targets, + 'targets': target_dict, 'delegations': self.delegations, }) return json_dict @@ -650,4 +709,5 @@ def to_dict(self) -> JsonDict: # Modification. def update(self, filename: str, fileinfo: JsonDict) -> None: """Assigns passed target file info to meta dict. """ - self.targets[filename] = fileinfo + self.targets[filename] = TargetInfo(fileinfo['length'], + fileinfo['hashes'], fileinfo.get('custom')) From c37bdd2608dbbcce7b86c161f30243e4af8d9ed7 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Tue, 1 Dec 2020 15:46:45 +0200 Subject: [PATCH 7/9] New API: Make sure we are not changing dict arg In the Signed.from_dict function we are passing signed - a dictionary. Dictionaries are passed by reference and thus we want to make sure we are not changing the dictionary passed as a function argument. I had to give default function arguments for all class arguments assigned in __init__ for Root, Snapshot, Targets, and Timestamp classes. I chose "None" as the easiest solution as a default argument, but we definitely want to add proper validation which will ensure we are not creating empty or partially populated objects. I didn't want to create a discussion for sensible defaults and argument validation here. There is already existing issue for that: https://github.com/theupdateframework/tuf/issues/1140 Signed-off-by: Martin Vrachev --- tuf/api/metadata.py | 88 +++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 09bdfd9d61..8e6ab35319 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -281,8 +281,6 @@ def __init__( self.expires = expires # TODO: Should we separate data validation from constructor? - if version < 0: - raise ValueError(f'version must be < 0, got {version}') self.version = version @@ -291,22 +289,24 @@ def __init__( def from_dict(cls, signed_dict: JsonDict) -> 'Signed': """Creates Signed object from its JSON/dict representation. """ + # Create empty object with default or parametrized constructor with + # default arguments. + obj = cls() + obj._type = signed_dict['_type'] + obj.version = signed_dict['version'] + obj.spec_version = signed_dict['spec_version'] # Convert 'expires' TUF metadata string to a datetime object, which is # what the constructor expects and what we store. The inverse operation # is implemented in 'to_dict'. - signed_dict['expires'] = tuf.formats.expiry_string_to_datetime( + obj.expires = tuf.formats.expiry_string_to_datetime( signed_dict['expires']) - # NOTE: We write the converted 'expires' back into 'signed_dict' above - # so that we can pass it to the constructor as '**signed_dict' below, - # along with other fields that belong to Signed subclasses. - # Any 'from_dict'(-like) conversions of fields that correspond to a - # subclass should be performed in the 'from_dict' method of that - # subclass and also be written back into 'signed_dict' before calling - # super().from_dict. + # NOTE: Any 'from_dict'(-like) conversions of fields that correspond + # to a subclass should be performed in the 'from_dict' method of that + # subclass and assigned to the below returned object. # NOTE: cls might be a subclass of Signed, if 'from_dict' was called on # that subclass (see e.g. Metadata.from_dict). - return cls(**signed_dict) + return obj # Serialization. @@ -378,9 +378,9 @@ class Root(Signed): # default max-args value for pylint is 5 # pylint: disable=too-many-arguments def __init__( - self, _type: str, version: int, spec_version: str, - expires: datetime, consistent_snapshot: bool, - keys: JsonDict, roles: JsonDict) -> None: + self, _type: str=None, version: int=None, spec_version: str=None, + expires: datetime=None, consistent_snapshot: bool=None, + keys: JsonDict=None, roles: JsonDict=None) -> None: super().__init__(_type, version, spec_version, expires) # TODO: Add classes for keys and roles self.consistent_snapshot = consistent_snapshot @@ -388,6 +388,18 @@ def __init__( self.roles = roles + @classmethod + def from_dict(cls, signed_dict: JsonDict) -> 'Root': + """Creates Root object from its JSON/dict representation. """ + + # Get a parent object with its attributes already assigned. + obj = super().from_dict(signed_dict) + obj.consistent_snapshot = signed_dict['consistent_snapshot'] + obj.keys = signed_dict['keys'] + obj.roles = signed_dict['roles'] + return obj + + # Serialization. def to_dict(self) -> JsonDict: """Returns the JSON-serializable dictionary representation of self. """ @@ -478,8 +490,9 @@ class Timestamp(Signed): """ def __init__( - self, _type: str, version: int, spec_version: str, - expires: datetime, meta: Dict[str, MetadataInfo]) -> None: + self, _type: str=None, version: int=None, spec_version: str=None, + expires: datetime=None, meta: Dict[str, MetadataInfo]=None + ) -> None: super().__init__(_type, version, spec_version, expires) self.meta = meta @@ -500,10 +513,14 @@ def to_dict(self) -> JsonDict: def from_dict(cls, signed_dict: JsonDict) -> 'Timestamp': """Creates Timestamp object from its JSON/dict representation. """ - signed_dict['meta']['snapshot.json'] = MetadataInfo( - **signed_dict['meta']['snapshot.json']) + # Get a parent object with its attributes already assigned. + obj = super().from_dict(signed_dict) + obj.meta = {} + meta = signed_dict['meta']['snapshot.json'] + obj.meta['snapshot.json'] = MetadataInfo(meta['version'], + meta.get('length'), meta.get('hashes')) - return super().from_dict(signed_dict) + return obj # Modification. @@ -529,8 +546,9 @@ class Snapshot(Signed): """ def __init__( - self, _type: str, version: int, spec_version: str, - expires: datetime, meta: Dict[str, MetadataInfo]) -> None: + self, _type: str=None, version: int=None, spec_version: str=None, + expires: datetime=None, meta: Dict[str, MetadataInfo]=None + ) -> None: super().__init__(_type, version, spec_version, expires) self.meta = meta @@ -539,11 +557,15 @@ def __init__( def from_dict(cls, signed_dict: JsonDict) -> 'Snapshot': """Creates Snapshot object from its JSON/dict representation. """ + # Get a parent object with its attributes already assigned. + obj = super().from_dict(signed_dict) + obj.meta = {} for meta_path in signed_dict['meta'].keys(): - signed_dict['meta'][meta_path] = MetadataInfo( - **signed_dict['meta'][meta_path]) + meta = signed_dict['meta'][meta_path] + obj.meta[meta_path] = MetadataInfo(meta['version'], + meta.get('length'), meta.get('hashes')) - return super().from_dict(signed_dict) + return obj # Serialization. @@ -672,9 +694,9 @@ class Targets(Signed): # default max-args value for pylint is 5 # pylint: disable=too-many-arguments def __init__( - self, _type: str, version: int, spec_version: str, - expires: datetime, targets: Dict[str, TargetInfo], - delegations: JsonDict) -> None: + self, _type: str=None, version: int=None, spec_version: str=None, + expires: datetime=None, targets: Dict[str, TargetInfo]=None, + delegations: JsonDict=None) -> None: super().__init__(_type, version, spec_version, expires) self.targets = targets @@ -685,11 +707,17 @@ def __init__( @classmethod def from_dict(cls, signed_dict: JsonDict) -> 'Targets': """Creates Targets object from its JSON/dict representation. """ + + # Get a parent object with its attributes already assigned. + obj = super().from_dict(signed_dict) + obj.targets = {} for target_path in signed_dict['targets'].keys(): - signed_dict['targets'][target_path] = TargetInfo( - **signed_dict['targets'][target_path]) + info = signed_dict['targets'][target_path] + obj.targets[target_path] = TargetInfo(info['length'], + info['hashes'], info.get('custom')) - return super().from_dict(signed_dict) + obj.delegations = signed_dict['delegations'] + return obj # Serialization. From b9c70aae06d5c16d266ff739d151ce6de0a8280d Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Fri, 11 Dec 2020 13:15:34 +0200 Subject: [PATCH 8/9] New API: Add a TODO comments Signed-off-by: Martin Vrachev --- tuf/api/metadata.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 8e6ab35319..b845bde321 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -381,6 +381,10 @@ def __init__( self, _type: str=None, version: int=None, spec_version: str=None, expires: datetime=None, consistent_snapshot: bool=None, keys: JsonDict=None, roles: JsonDict=None) -> None: + + # TODO: Add sensible defaults when we have input validation. + # See issue https://github.com/theupdateframework/tuf/issues/1140 + # We need default values to create empty objects in Signed.from_dict() super().__init__(_type, version, spec_version, expires) # TODO: Add classes for keys and roles self.consistent_snapshot = consistent_snapshot @@ -493,6 +497,10 @@ def __init__( self, _type: str=None, version: int=None, spec_version: str=None, expires: datetime=None, meta: Dict[str, MetadataInfo]=None ) -> None: + + # TODO: Add sensible defaults when we have input validation. + # See issue https://github.com/theupdateframework/tuf/issues/1140 + # We need default values to create empty objects in Signed.from_dict() super().__init__(_type, version, spec_version, expires) self.meta = meta @@ -528,6 +536,8 @@ def update(self, version: int, length: Optional[int] = None, hashes: Optional[JsonDict] = None) -> None: """Assigns passed info about snapshot metadata to meta dict. """ + # TODO: Consider renaming this function: + # see: https://github.com/theupdateframework/tuf/issues/1230 self.meta['snapshot.json'] = MetadataInfo(version, length, hashes) @@ -549,6 +559,10 @@ def __init__( self, _type: str=None, version: int=None, spec_version: str=None, expires: datetime=None, meta: Dict[str, MetadataInfo]=None ) -> None: + + # TODO: Add sensible defaults when we have input validation. + # See issue https://github.com/theupdateframework/tuf/issues/1140 + # We need default values to create empty objects in Signed.from_dict() super().__init__(_type, version, spec_version, expires) self.meta = meta @@ -587,6 +601,9 @@ def update( self, rolename: str, version: int, length: Optional[int] = None, hashes: Optional[JsonDict] = None) -> None: """Assigns passed (delegated) targets role info to meta dict. """ + + # TODO: Consider renaming this function: + # see: https://github.com/theupdateframework/tuf/issues/1230 metadata_fn = f'{rolename}.json' self.meta[metadata_fn] = MetadataInfo(version, length, hashes) @@ -697,6 +714,10 @@ def __init__( self, _type: str=None, version: int=None, spec_version: str=None, expires: datetime=None, targets: Dict[str, TargetInfo]=None, delegations: JsonDict=None) -> None: + + # TODO: Add sensible defaults when we have input validation. + # See issue https://github.com/theupdateframework/tuf/issues/1140 + # We need default values to create empty objects in Signed.from_dict() super().__init__(_type, version, spec_version, expires) self.targets = targets @@ -737,5 +758,8 @@ def to_dict(self) -> JsonDict: # Modification. def update(self, filename: str, fileinfo: JsonDict) -> None: """Assigns passed target file info to meta dict. """ + + # TODO: Consider renaming this function: + # see: https://github.com/theupdateframework/tuf/issues/1230 self.targets[filename] = TargetInfo(fileinfo['length'], fileinfo['hashes'], fileinfo.get('custom')) From 383e26044ee96e8616861838d7e620c07ae89b5f Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Tue, 5 Jan 2021 17:44:16 +0200 Subject: [PATCH 9/9] New API: Disable irrelevant pylint warnings Warnings E1120 and W0212 are irrelevant in one specific case, but rule E1101 creates a lot of false-positives in tuf/metadata/api. For example, there are 4 false-positives in this file related to "meta" and "targets" fields not existing in "signed_dict" in "from_dict" functions in Timestamp, Snapshot and Targets classes. Signed-off-by: Martin Vrachev --- tuf/api/metadata.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index b845bde321..7da694cc01 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -1,3 +1,5 @@ +# pylint: disable=E1101 + """TUF role metadata model. This module provides container classes for TUF role metadata, including methods @@ -291,8 +293,11 @@ def from_dict(cls, signed_dict: JsonDict) -> 'Signed': # Create empty object with default or parametrized constructor with # default arguments. - obj = cls() - obj._type = signed_dict['_type'] + # Warnings about rules E1120 (no-value-for-parameter) and W0212 + # (protected-access) are not relevant here because cls is most likely + # a descendant of "Signed" and we need to setup the appropriate fields. + obj = cls() # pylint: disable=E1120 + obj._type = signed_dict['_type'] # pylint: disable=W0212 obj.version = signed_dict['version'] obj.spec_version = signed_dict['spec_version'] # Convert 'expires' TUF metadata string to a datetime object, which is