Skip to content

Commit 78e5323

Browse files
committed
Add DelegationRole and Delegations
In the top level metadata classes, there are complex attributes such as "meta" in Targets and Snapshot, "key" and "roles" in Root etc. We want to represent those complex attributes with a class to allow easier verification and support for metadata with unrecognized fields. For more context read ADR 0004 and ADR 0008 in the docs/adr folder. DelegationRole shares a couple of fields with the Role class and that's why it inherits it. I decided to use a separate Delegations class because I thought it will make it easier to read, verify and add additional helper functions. Also, I tried to make sure that I test each level of the delegations representation for support of storing unrecognized fields. Signed-off-by: Martin Vrachev <[email protected]>
1 parent 1ce94b9 commit 78e5323

File tree

2 files changed

+145
-36
lines changed

2 files changed

+145
-36
lines changed

tests/test_api.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,15 @@ def test_support_for_unrecognized_fields(self):
441441
dict1["signed"]["keys"][keyid]["d"] = "c"
442442
for role_str in dict1["signed"]["roles"].keys():
443443
dict1["signed"]["roles"][role_str]["e"] = "g"
444+
elif metadata == "targets" and dict1["signed"].get("delegations"):
445+
for keyid in dict1["signed"]["delegations"]["keys"].keys():
446+
dict1["signed"]["delegations"]["keys"][keyid]["d"] = "c"
447+
new_roles = []
448+
for role in dict1["signed"]["delegations"]["roles"]:
449+
role["e"] = "g"
450+
new_roles.append(role)
451+
dict1["signed"]["delegations"]["roles"] = new_roles
452+
dict1["signed"]["delegations"]["foo"] = "bar"
444453

445454
temp_copy = copy.deepcopy(dict1)
446455
metadata_obj = Metadata.from_dict(temp_copy)

tuf/api/metadata.py

Lines changed: 136 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,135 @@ def update(
728728
self.meta[metadata_fn]["hashes"] = hashes
729729

730730

731+
class DelegationRole(Role):
732+
"""A container with information about particular delegated role.
733+
734+
Attributes:
735+
name: A string giving the name of the delegated role.
736+
keyids: A set of strings each of which represents a given key.
737+
threshold: An integer representing the required number of keys for that
738+
particular role.
739+
terminating: A boolean indicating whether subsequent delegations
740+
should be considered.
741+
paths: An optional list of strings, where each string describes
742+
a path that the role is trusted to provide.
743+
path_hash_prefixes: An optional list of HEX_DIGESTs used to succinctly
744+
describe a set of target paths. Only one of the attributes "paths"
745+
and "path_hash_prefixes" is allowed to be set.
746+
unrecognized_fields: Dictionary of all unrecognized fields.
747+
748+
"""
749+
750+
def __init__(
751+
self,
752+
name: str,
753+
keyids: list,
754+
threshold: int,
755+
terminating: bool,
756+
paths: Optional[list] = None,
757+
path_hash_prefixes: Optional[list] = None,
758+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
759+
) -> None:
760+
super().__init__(keyids, threshold, unrecognized_fields)
761+
self.name = name
762+
self.terminating = terminating
763+
if paths and path_hash_prefixes:
764+
raise ValueError(
765+
f"Only one of the attributes 'paths' and"
766+
f"'path_hash_prefixes' can be set!"
767+
)
768+
if paths:
769+
self.paths = paths
770+
elif path_hash_prefixes:
771+
self.path_hash_prefixes = path_hash_prefixes
772+
773+
@classmethod
774+
def from_dict(cls, role_dict: Mapping[str, Any]) -> "Role":
775+
"""Creates DelegationRole object from its dict representation."""
776+
name = role_dict.pop("name")
777+
keyids = role_dict.pop("keyids")
778+
threshold = role_dict.pop("threshold")
779+
terminating = role_dict.pop("terminating")
780+
paths = paths = role_dict.pop("paths", None)
781+
path_hash_prefixes = role_dict.pop("path_hash_prefixes", None)
782+
# All fields left in the role_dict are unrecognized.
783+
return cls(
784+
name,
785+
keyids,
786+
threshold,
787+
terminating,
788+
paths,
789+
path_hash_prefixes,
790+
role_dict,
791+
)
792+
793+
def to_dict(self) -> Dict[str, Any]:
794+
"""Returns the dict representation of self. """
795+
base_role_dict = super().to_dict()
796+
res_dict = {
797+
"name": self.name,
798+
"terminating": self.terminating,
799+
**base_role_dict,
800+
}
801+
if self.paths:
802+
res_dict["paths"] = self.paths
803+
elif self.path_hash_prefixes:
804+
res_dict["path_hash_prefixes"] = self.path_hash_prefixes
805+
return res_dict
806+
807+
808+
class Delegations:
809+
"""A container object storing information about all delegations.
810+
811+
Attributes:
812+
keys: A dictionary of keyids and key objects containing information
813+
about the corresponding key.
814+
roles: A list of DelegationRole instances containing information about
815+
all delegated roles.
816+
unrecognized_fields: Dictionary of all unrecognized fields.
817+
818+
"""
819+
820+
def __init__(
821+
self,
822+
keys: Mapping[str, Key],
823+
roles: list,
824+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
825+
) -> None:
826+
self.keys = keys
827+
self.roles = roles
828+
self.unrecognized_fields = unrecognized_fields or {}
829+
830+
@classmethod
831+
def from_dict(cls, delegations_dict: Mapping[str, Any]) -> "Delegations":
832+
"""Creates Delegations object from its dict representation."""
833+
keys = delegations_dict.pop("keys")
834+
for keyid, key_dict in keys.items():
835+
keys[keyid] = Key.from_dict(key_dict.copy())
836+
roles = delegations_dict.pop("roles")
837+
roles_res = []
838+
for role_dict in roles:
839+
new_role = DelegationRole.from_dict(role_dict.copy())
840+
roles_res.append(new_role)
841+
# All fields left in the delegations_dict are unrecognized.
842+
return cls(keys, roles_res, delegations_dict)
843+
844+
def to_dict(self) -> Dict[str, Any]:
845+
"""Returns the dict representation of self. """
846+
keys = {}
847+
for keyid, key in self.keys.items():
848+
keys[keyid] = key.to_dict()
849+
roles = []
850+
for role_obj in self.roles:
851+
roles.append(role_obj.to_dict())
852+
res_dict = {
853+
"keys": keys,
854+
"roles": roles,
855+
**self.unrecognized_fields,
856+
}
857+
return res_dict
858+
859+
731860
class Targets(Signed):
732861
"""A container for the signed part of targets metadata.
733862
@@ -747,38 +876,9 @@ class Targets(Signed):
747876
...
748877
}
749878
750-
delegations: A dictionary that contains a list of delegated target
879+
delegations: An optional object containing a list of delegated target
751880
roles and public key store used to verify their metadata
752-
signatures::
753-
754-
{
755-
'keys' : {
756-
'<KEYID>': {
757-
'keytype': '<KEY TYPE>',
758-
'scheme': '<KEY SCHEME>',
759-
'keyid_hash_algorithms': [
760-
'<HASH ALGO 1>',
761-
'<HASH ALGO 2>'
762-
...
763-
],
764-
'keyval': {
765-
'public': '<PUBLIC KEY HEX REPRESENTATION>'
766-
}
767-
},
768-
...
769-
},
770-
'roles': [
771-
{
772-
'name': '<ROLENAME>',
773-
'keyids': ['<SIGNING KEY KEYID>', ...],
774-
'threshold': <SIGNATURE THRESHOLD>,
775-
'terminating': <TERMINATING BOOLEAN>,
776-
'path_hash_prefixes': ['<HEX DIGEST>', ... ], // or
777-
'paths' : ['PATHPATTERN', ... ],
778-
},
779-
...
780-
]
781-
}
881+
signatures.
782882
783883
"""
784884

@@ -793,13 +893,12 @@ def __init__(
793893
spec_version: str,
794894
expires: datetime,
795895
targets: Mapping[str, Any],
796-
delegations: Mapping[str, Any],
896+
delegations: Optional[Delegations] = None,
797897
unrecognized_fields: Optional[Mapping[str, Any]] = None,
798898
) -> None:
799899
super().__init__(
800900
_type, version, spec_version, expires, unrecognized_fields
801901
)
802-
# TODO: Add class for meta
803902
self.targets = targets
804903
self.delegations = delegations
805904

@@ -808,17 +907,18 @@ def from_dict(cls, targets_dict: Mapping[str, Any]) -> "Targets":
808907
"""Creates Targets object from its dict representation."""
809908
common_args = cls._common_fields_from_dict(targets_dict)
810909
targets = targets_dict.pop("targets")
811-
delegations = targets_dict.pop("delegations")
910+
delegations_dict = targets_dict.pop("delegations")
911+
delegations_obj = Delegations.from_dict(delegations_dict)
812912
# All fields left in the targets_dict are unrecognized.
813-
return cls(*common_args, targets, delegations, targets_dict)
913+
return cls(*common_args, targets, delegations_obj, targets_dict)
814914

815915
def to_dict(self) -> Dict[str, Any]:
816916
"""Returns the dict representation of self."""
817917
targets_dict = self._common_fields_to_dict()
818918
targets_dict.update(
819919
{
820920
"targets": self.targets,
821-
"delegations": self.delegations,
921+
"delegations": self.delegations.to_dict(),
822922
}
823923
)
824924
return targets_dict

0 commit comments

Comments
 (0)