Skip to content

Commit 2b89dc0

Browse files
author
Martin Vrachev
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 <mvrachev@vmware.com>
1 parent 08f48d5 commit 2b89dc0

2 files changed

Lines changed: 76 additions & 9 deletions

File tree

tests/test_api.py

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

333+
def setup_dict_with_unrecognized_field(self, file_path, field, value):
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"][field] = value
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+
dict1 = self.setup_dict_with_unrecognized_field(path, "f", "b")
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(dict1)
352+
metadata_obj = Metadata.from_dict(temp_copy)
353+
354+
self.assertEqual(dict1["signed"], metadata_obj.signed.to_dict())
355+
356+
# Test that two instances of the same class could have different
357+
# unrecognized fields.
358+
dict2 = self.setup_dict_with_unrecognized_field(path, "f2", "b2")
359+
temp_copy2 = copy.deepcopy(dict2)
360+
metadata_obj2 = Metadata.from_dict(temp_copy2)
361+
self.assertNotEqual(
362+
metadata_obj.signed.to_dict(), metadata_obj2.signed.to_dict()
363+
)
364+
365+
333366
# Run unit test.
334367
if __name__ == '__main__':
335368
utils.configure_test_logging(sys.argv)

tuf/api/metadata.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -300,14 +300,22 @@ 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+
unrecognized_fields: An optional dictionary storing all unrecognized
304+
fields. Used for backward compatibility. If None is provided, an
305+
empty dictionary will be created.
303306
304307
"""
305308

306309
# NOTE: Signed is a stupid name, because this might not be signed yet, but
307310
# we keep it to match spec terminology (I often refer to this as "payload",
308311
# or "inner metadata")
309312
def __init__(
310-
self, _type: str, version: int, spec_version: str, expires: datetime
313+
self,
314+
_type: str,
315+
version: int,
316+
spec_version: str,
317+
expires: datetime,
318+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
311319
) -> None:
312320

313321
self._type = _type
@@ -318,6 +326,9 @@ def __init__(
318326
if version < 0:
319327
raise ValueError(f"version must be >= 0, got {version}")
320328
self.version = version
329+
if unrecognized_fields is None:
330+
unrecognized_fields = {}
331+
self.unrecognized_fields = unrecognized_fields
321332

322333
@staticmethod
323334
def _common_fields_from_dict(signed_dict: Mapping[str, Any]) -> list:
@@ -349,6 +360,7 @@ def _common_fields_to_dict(self) -> Dict[str, Any]:
349360
"version": self.version,
350361
"spec_version": self.spec_version,
351362
"expires": self.expires.isoformat() + "Z",
363+
**self.unrecognized_fields,
352364
}
353365

354366
# Modification.
@@ -409,8 +421,11 @@ def __init__(
409421
consistent_snapshot: bool,
410422
keys: Mapping[str, Any],
411423
roles: Mapping[str, Any],
424+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
412425
) -> None:
413-
super().__init__(_type, version, spec_version, expires)
426+
super().__init__(
427+
_type, version, spec_version, expires, unrecognized_fields
428+
)
414429
# TODO: Add classes for keys and roles
415430
self.consistent_snapshot = consistent_snapshot
416431
self.keys = keys
@@ -423,7 +438,11 @@ def from_dict(cls, root_dict: Mapping[str, Any]) -> "Root":
423438
consistent_snapshot = root_dict.pop("consistent_snapshot")
424439
keys = root_dict.pop("keys")
425440
roles = root_dict.pop("roles")
426-
return cls(*common_args, consistent_snapshot, keys, roles)
441+
# All fields left in the root_dict and unrecognized.
442+
unrecognized_fields = root_dict
443+
return cls(
444+
*common_args, consistent_snapshot, keys, roles, unrecognized_fields
445+
)
427446

428447
def to_dict(self) -> Dict[str, Any]:
429448
"""Returns the dict representation of self. """
@@ -485,8 +504,11 @@ def __init__(
485504
spec_version: str,
486505
expires: datetime,
487506
meta: Mapping[str, Any],
507+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
488508
) -> None:
489-
super().__init__(_type, version, spec_version, expires)
509+
super().__init__(
510+
_type, version, spec_version, expires, unrecognized_fields
511+
)
490512
# TODO: Add class for meta
491513
self.meta = meta
492514

@@ -495,7 +517,9 @@ def from_dict(cls, timestamp_dict: Mapping[str, Any]) -> "Timestamp":
495517
"""Creates Timestamp object from its dict representation. """
496518
common_args = cls._common_fields_from_dict(timestamp_dict)
497519
meta = timestamp_dict.pop("meta")
498-
return cls(*common_args, meta)
520+
# All fields left in the timestamp_dict and unrecognized.
521+
unrecognized_fields = timestamp_dict
522+
return cls(*common_args, meta, unrecognized_fields)
499523

500524
def to_dict(self) -> Dict[str, Any]:
501525
"""Returns the dict representation of self. """
@@ -549,8 +573,11 @@ def __init__(
549573
spec_version: str,
550574
expires: datetime,
551575
meta: Mapping[str, Any],
576+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
552577
) -> None:
553-
super().__init__(_type, version, spec_version, expires)
578+
super().__init__(
579+
_type, version, spec_version, expires, unrecognized_fields
580+
)
554581
# TODO: Add class for meta
555582
self.meta = meta
556583

@@ -559,7 +586,9 @@ def from_dict(cls, snapshot_dict: Mapping[str, Any]) -> "Snapshot":
559586
"""Creates Snapshot object from its dict representation. """
560587
common_args = cls._common_fields_from_dict(snapshot_dict)
561588
meta = snapshot_dict.pop("meta")
562-
return cls(*common_args, meta)
589+
# All fields left in the snapshot_dict and unrecognized.
590+
unrecognized_fields = snapshot_dict
591+
return cls(*common_args, meta, unrecognized_fields)
563592

564593
def to_dict(self) -> Dict[str, Any]:
565594
"""Returns the dict representation of self. """
@@ -652,8 +681,11 @@ def __init__(
652681
expires: datetime,
653682
targets: Mapping[str, Any],
654683
delegations: Mapping[str, Any],
684+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
655685
) -> None:
656-
super().__init__(_type, version, spec_version, expires)
686+
super().__init__(
687+
_type, version, spec_version, expires, unrecognized_fields
688+
)
657689
# TODO: Add class for meta
658690
self.targets = targets
659691
self.delegations = delegations
@@ -664,7 +696,9 @@ def from_dict(cls, targets_dict: Mapping[str, Any]) -> "Targets":
664696
common_args = cls._common_fields_from_dict(targets_dict)
665697
targets = targets_dict.pop("targets")
666698
delegations = targets_dict.pop("delegations")
667-
return cls(*common_args, targets, delegations)
699+
# All fields left in the targets_dict and unrecognized.
700+
unrecognized_fields = targets_dict
701+
return cls(*common_args, targets, delegations, unrecognized_fields)
668702

669703
def to_dict(self) -> Dict[str, Any]:
670704
"""Returns the dict representation of self. """

0 commit comments

Comments
 (0)