Skip to content

Commit 435f638

Browse files
committed
New API: accept metadata with unrecognized fields
In order to support ADR 0008 we would want to accept unrecognized fields in all metadata classes, including the classes that would be added representing a subportion of a role like "meta", "delegations" and "roles". Also, we should test that we support unrecognized fields when adding new classes or modifying existing ones to make sure we support ADR 0008. Signed-off-by: Martin Vrachev <[email protected]>
1 parent 08f48d5 commit 435f638

File tree

2 files changed

+53
-2
lines changed

2 files changed

+53
-2
lines changed

tests/test_api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,30 @@ def test_metadata_targets(self):
330330
# Verify that data is updated
331331
self.assertEqual(targets.signed.targets[filename], fileinfo)
332332

333+
def setup_json_dict_with_unrecognized_field(self, file_path):
334+
json_dict = {}
335+
with open(file_path) as f:
336+
json_dict = json.loads(f.read())
337+
# We are changing the json dict without changing the signature.
338+
# This could be a problem if we want to do verification on this dict.
339+
json_dict["signed"]["foo"] = "bar"
340+
return json_dict
341+
342+
def test_support_for_unrecognized_fields(self):
343+
for metadata in ["root", "timestamp", "snapshot", "targets"]:
344+
path = os.path.join(self.repo_dir, "metadata", metadata + ".json")
345+
json_dict = self.setup_json_dict_with_unrecognized_field(path)
346+
# Test that the metadata classes store unrecognized fields when
347+
# initializing and passes them when casting the instance to a dict.
348+
349+
# TODO: Remove the deepcopy when Metadata.from_dict() doesn't have
350+
# the side effect to destroy the passed dictionary.
351+
temp_copy = copy.deepcopy(json_dict)
352+
metadata_obj = Metadata.from_dict(temp_copy)
353+
354+
self.assertEqual(json_dict["signed"], metadata_obj.signed.to_dict())
355+
356+
333357
# Run unit test.
334358
if __name__ == '__main__':
335359
utils.configure_test_logging(sys.argv)

tuf/api/metadata.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,13 @@ class Signed:
300300
spec_version: The TUF specification version number (semver) the
301301
metadata format adheres to.
302302
expires: The metadata expiration datetime object.
303+
known_fields: A dictionary containing the known fields(attributes). Used
304+
when filtering the known fields from all fields in order to retrieve
305+
and store the unrecognized fields. See ADR 0008 for context.
303306
304307
"""
305308

309+
known_fields = ["_type", "spec_version", "expires", "version"]
306310
# NOTE: Signed is a stupid name, because this might not be signed yet, but
307311
# we keep it to match spec terminology (I often refer to this as "payload",
308312
# or "inner metadata")
@@ -319,6 +323,19 @@ def __init__(
319323
raise ValueError(f"version must be >= 0, got {version}")
320324
self.version = version
321325

326+
@classmethod
327+
def _store_unrecognized_fields(
328+
cls, all_fields: Mapping[str, Any]
329+
) -> Mapping[str, Any]:
330+
"""Utility function filtering the known fields from all in order to
331+
retrieve and store the unrecognized fields."""
332+
unrecognized_fields = {}
333+
for key, value in all_fields.items():
334+
if key not in cls.known_fields:
335+
unrecognized_fields[key] = value
336+
337+
cls.unrecognized_fields = unrecognized_fields
338+
322339
@staticmethod
323340
def _common_fields_from_dict(signed_dict: Mapping[str, Any]) -> list:
324341
"""Returns common fields of 'Signed' instances from the passed dict
@@ -423,6 +440,8 @@ def from_dict(cls, root_dict: Mapping[str, Any]) -> "Root":
423440
consistent_snapshot = root_dict.pop("consistent_snapshot")
424441
keys = root_dict.pop("keys")
425442
roles = root_dict.pop("roles")
443+
cls.known_fields += ["consistent_snapshot", "keys", "roles"]
444+
cls._store_unrecognized_fields(root_dict)
426445
return cls(*common_args, consistent_snapshot, keys, roles)
427446

428447
def to_dict(self) -> Dict[str, Any]:
@@ -433,6 +452,7 @@ def to_dict(self) -> Dict[str, Any]:
433452
"consistent_snapshot": self.consistent_snapshot,
434453
"keys": self.keys,
435454
"roles": self.roles,
455+
**self.unrecognized_fields,
436456
}
437457
)
438458
return root_dict
@@ -495,12 +515,14 @@ def from_dict(cls, timestamp_dict: Mapping[str, Any]) -> "Timestamp":
495515
"""Creates Timestamp object from its dict representation. """
496516
common_args = cls._common_fields_from_dict(timestamp_dict)
497517
meta = timestamp_dict.pop("meta")
518+
cls.known_fields += ["meta"]
519+
cls._store_unrecognized_fields(timestamp_dict)
498520
return cls(*common_args, meta)
499521

500522
def to_dict(self) -> Dict[str, Any]:
501523
"""Returns the dict representation of self. """
502524
timestamp_dict = self._common_fields_to_dict()
503-
timestamp_dict.update({"meta": self.meta})
525+
timestamp_dict.update({"meta": self.meta, **self.unrecognized_fields})
504526
return timestamp_dict
505527

506528
# Modification.
@@ -559,12 +581,14 @@ def from_dict(cls, snapshot_dict: Mapping[str, Any]) -> "Snapshot":
559581
"""Creates Snapshot object from its dict representation. """
560582
common_args = cls._common_fields_from_dict(snapshot_dict)
561583
meta = snapshot_dict.pop("meta")
584+
cls.known_fields += ["meta"]
585+
cls._store_unrecognized_fields(snapshot_dict)
562586
return cls(*common_args, meta)
563587

564588
def to_dict(self) -> Dict[str, Any]:
565589
"""Returns the dict representation of self. """
566590
snapshot_dict = self._common_fields_to_dict()
567-
snapshot_dict.update({"meta": self.meta})
591+
snapshot_dict.update({"meta": self.meta, **self.unrecognized_fields})
568592
return snapshot_dict
569593

570594
# Modification.
@@ -664,6 +688,8 @@ def from_dict(cls, targets_dict: Mapping[str, Any]) -> "Targets":
664688
common_args = cls._common_fields_from_dict(targets_dict)
665689
targets = targets_dict.pop("targets")
666690
delegations = targets_dict.pop("delegations")
691+
cls.known_fields += ["meta", "targets", "delegations"]
692+
cls._store_unrecognized_fields(targets_dict)
667693
return cls(*common_args, targets, delegations)
668694

669695
def to_dict(self) -> Dict[str, Any]:
@@ -673,6 +699,7 @@ def to_dict(self) -> Dict[str, Any]:
673699
{
674700
"targets": self.targets,
675701
"delegations": self.delegations,
702+
**self.unrecognized_fields,
676703
}
677704
)
678705
return targets_dict

0 commit comments

Comments
 (0)