Skip to content

Commit 5d5e093

Browse files
committed
New metadata API: add support for ADR 0008
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 5d5e093

File tree

2 files changed

+121
-6
lines changed

2 files changed

+121
-6
lines changed

tests/test_api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
from securesystemslib.signer import (
5151
SSlibSigner
5252
)
53+
from securesystemslib.storage import(
54+
FilesystemBackend
55+
)
5356

5457
logger = logging.getLogger(__name__)
5558

@@ -330,6 +333,32 @@ def test_metadata_targets(self):
330333
# Verify that data is updated
331334
self.assertEqual(targets.signed.targets[filename], fileinfo)
332335

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

tuf/api/metadata.py

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,20 @@ def bump_version(self) -> None:
361361
self.version += 1
362362

363363

364+
def _get_unrecognized_fields(
365+
all_fields: Mapping[str, Any], recognized_fields: list
366+
) -> Mapping[str, Any]:
367+
"""Utility function to get the additional unrecognized fields from a dict.
368+
Read ADR 0008."""
369+
370+
unrecognized_fields = {}
371+
for key, value in all_fields.items():
372+
if key not in recognized_fields:
373+
unrecognized_fields[key] = value
374+
375+
return unrecognized_fields
376+
377+
364378
class Root(Signed):
365379
"""A container for the signed part of root metadata.
366380
@@ -393,6 +407,9 @@ class Root(Signed):
393407
},
394408
...
395409
}
410+
unrecognized_fields: An optional dictionary used to store any
411+
unrecognized fields. Needed for backward compatibility.
412+
Read ADR 0008.
396413
397414
"""
398415

@@ -409,12 +426,14 @@ def __init__(
409426
consistent_snapshot: bool,
410427
keys: Mapping[str, Any],
411428
roles: Mapping[str, Any],
429+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
412430
) -> None:
413431
super().__init__(_type, version, spec_version, expires)
414432
# TODO: Add classes for keys and roles
415433
self.consistent_snapshot = consistent_snapshot
416434
self.keys = keys
417435
self.roles = roles
436+
self.unrecognized_fields = unrecognized_fields
418437

419438
@classmethod
420439
def from_dict(cls, root_dict: Mapping[str, Any]) -> "Root":
@@ -423,7 +442,22 @@ def from_dict(cls, root_dict: Mapping[str, Any]) -> "Root":
423442
consistent_snapshot = root_dict.pop("consistent_snapshot")
424443
keys = root_dict.pop("keys")
425444
roles = root_dict.pop("roles")
426-
return cls(*common_args, consistent_snapshot, keys, roles)
445+
# Store unregonized fields. Read ADR 0008.
446+
expected_fields = [
447+
"_type",
448+
"version",
449+
"spec_version",
450+
"expires",
451+
"consistent_snapshot",
452+
"keys",
453+
"roles",
454+
]
455+
unrecognized_fields = _get_unrecognized_fields(
456+
root_dict, expected_fields
457+
)
458+
return cls(
459+
*common_args, consistent_snapshot, keys, roles, unrecognized_fields
460+
)
427461

428462
def to_dict(self) -> Dict[str, Any]:
429463
"""Returns the dict representation of self. """
@@ -433,6 +467,7 @@ def to_dict(self) -> Dict[str, Any]:
433467
"consistent_snapshot": self.consistent_snapshot,
434468
"keys": self.keys,
435469
"roles": self.roles,
470+
**self.unrecognized_fields,
436471
}
437472
)
438473
return root_dict
@@ -475,6 +510,9 @@ class Timestamp(Signed):
475510
}
476511
}
477512
}
513+
unrecognized_fields: An optional dictionary used to store any
514+
unrecognized fields. Needed for backward compatibility.
515+
Read ADR 0008.
478516
479517
"""
480518

@@ -485,22 +523,35 @@ def __init__(
485523
spec_version: str,
486524
expires: datetime,
487525
meta: Mapping[str, Any],
526+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
488527
) -> None:
489528
super().__init__(_type, version, spec_version, expires)
490529
# TODO: Add class for meta
491530
self.meta = meta
531+
self.unrecognized_fields = unrecognized_fields
492532

493533
@classmethod
494534
def from_dict(cls, timestamp_dict: Mapping[str, Any]) -> "Timestamp":
495535
"""Creates Timestamp object from its dict representation. """
496536
common_args = cls._common_fields_from_dict(timestamp_dict)
497537
meta = timestamp_dict.pop("meta")
498-
return cls(*common_args, meta)
538+
# Store unregonized fields. Read ADR 0008.
539+
expected_fields = [
540+
"_type",
541+
"version",
542+
"spec_version",
543+
"expires",
544+
"meta",
545+
]
546+
unrecognized_fields = _get_unrecognized_fields(
547+
timestamp_dict, expected_fields
548+
)
549+
return cls(*common_args, meta, unrecognized_fields)
499550

500551
def to_dict(self) -> Dict[str, Any]:
501552
"""Returns the dict representation of self. """
502553
timestamp_dict = self._common_fields_to_dict()
503-
timestamp_dict.update({"meta": self.meta})
554+
timestamp_dict.update({"meta": self.meta, **self.unrecognized_fields})
504555
return timestamp_dict
505556

506557
# Modification.
@@ -539,6 +590,9 @@ class Snapshot(Signed):
539590
},
540591
...
541592
}
593+
unrecognized_fields: An optional dictionary used to store any
594+
unrecognized fields. Needed for backward compatibility.
595+
Read ADR 0008.
542596
543597
"""
544598

@@ -549,22 +603,35 @@ def __init__(
549603
spec_version: str,
550604
expires: datetime,
551605
meta: Mapping[str, Any],
606+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
552607
) -> None:
553608
super().__init__(_type, version, spec_version, expires)
554609
# TODO: Add class for meta
555610
self.meta = meta
611+
self.unrecognized_fields = unrecognized_fields
556612

557613
@classmethod
558614
def from_dict(cls, snapshot_dict: Mapping[str, Any]) -> "Snapshot":
559615
"""Creates Snapshot object from its dict representation. """
560616
common_args = cls._common_fields_from_dict(snapshot_dict)
561617
meta = snapshot_dict.pop("meta")
562-
return cls(*common_args, meta)
618+
# Store unregonized fields. Read ADR 0008.
619+
expected_fields = [
620+
"_type",
621+
"version",
622+
"spec_version",
623+
"expires",
624+
"meta",
625+
]
626+
unrecognized_fields = _get_unrecognized_fields(
627+
snapshot_dict, expected_fields
628+
)
629+
return cls(*common_args, meta, unrecognized_fields)
563630

564631
def to_dict(self) -> Dict[str, Any]:
565632
"""Returns the dict representation of self. """
566633
snapshot_dict = self._common_fields_to_dict()
567-
snapshot_dict.update({"meta": self.meta})
634+
snapshot_dict.update({"meta": self.meta, **self.unrecognized_fields})
568635
return snapshot_dict
569636

570637
# Modification.
@@ -638,6 +705,10 @@ class Targets(Signed):
638705
]
639706
}
640707
708+
unrecognized_fields: An optional dictionary used to store any
709+
unrecognized fields. Needed for backward compatibility.
710+
Read ADR 0008.
711+
641712
"""
642713

643714
# TODO: determine an appropriate value for max-args and fix places where
@@ -652,19 +723,33 @@ def __init__(
652723
expires: datetime,
653724
targets: Mapping[str, Any],
654725
delegations: Mapping[str, Any],
726+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
655727
) -> None:
656728
super().__init__(_type, version, spec_version, expires)
657729
# TODO: Add class for meta
658730
self.targets = targets
659731
self.delegations = delegations
732+
self.unrecognized_fields = unrecognized_fields
660733

661734
@classmethod
662735
def from_dict(cls, targets_dict: Mapping[str, Any]) -> "Targets":
663736
"""Creates Targets object from its dict representation. """
664737
common_args = cls._common_fields_from_dict(targets_dict)
665738
targets = targets_dict.pop("targets")
666739
delegations = targets_dict.pop("delegations")
667-
return cls(*common_args, targets, delegations)
740+
# Store unregonized fields. Read ADR 0008.
741+
expected_fields = [
742+
"_type",
743+
"version",
744+
"spec_version",
745+
"expires",
746+
"meta",
747+
["targets", "delegations"],
748+
]
749+
unrecognized_fields = _get_unrecognized_fields(
750+
targets_dict, expected_fields
751+
)
752+
return cls(*common_args, targets, delegations, unrecognized_fields)
668753

669754
def to_dict(self) -> Dict[str, Any]:
670755
"""Returns the dict representation of self. """
@@ -673,6 +758,7 @@ def to_dict(self) -> Dict[str, Any]:
673758
{
674759
"targets": self.targets,
675760
"delegations": self.delegations,
761+
**self.unrecognized_fields,
676762
}
677763
)
678764
return targets_dict

0 commit comments

Comments
 (0)