Skip to content

Commit 8e4d3b9

Browse files
committed
Add DelegatedRole 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. DelegatedRole 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 <mvrachev@vmware.com>
1 parent a261d49 commit 8e4d3b9

File tree

2 files changed

+140
-33
lines changed

2 files changed

+140
-33
lines changed

tests/test_api.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,15 @@ def test_support_for_unrecognized_fields(self):
477477
dict1["signed"]["keys"][keyid]["d"] = "c"
478478
for role_str in dict1["signed"]["roles"].keys():
479479
dict1["signed"]["roles"][role_str]["e"] = "g"
480+
elif metadata == "targets" and dict1["signed"].get("delegations"):
481+
for keyid in dict1["signed"]["delegations"]["keys"].keys():
482+
dict1["signed"]["delegations"]["keys"][keyid]["d"] = "c"
483+
new_roles = []
484+
for role in dict1["signed"]["delegations"]["roles"]:
485+
role["e"] = "g"
486+
new_roles.append(role)
487+
dict1["signed"]["delegations"]["roles"] = new_roles
488+
dict1["signed"]["delegations"]["foo"] = "bar"
480489

481490
temp_copy = copy.deepcopy(dict1)
482491
metadata_obj = Metadata.from_dict(temp_copy)

tuf/api/metadata.py

Lines changed: 131 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,131 @@ def update(
738738
self.meta[metadata_fn]["hashes"] = hashes
739739

740740

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

@@ -804,7 +900,7 @@ def __init__(
804900
spec_version: str,
805901
expires: datetime,
806902
targets: Dict[str, Any],
807-
delegations: Optional[Dict[str, Any]] = None,
903+
delegations: Optional[Delegations] = None,
808904
unrecognized_fields: Optional[Mapping[str, Any]] = None,
809905
) -> None:
810906
super().__init__(version, spec_version, expires, unrecognized_fields)
@@ -818,6 +914,8 @@ def from_dict(cls, targets_dict: Dict[str, Any]) -> "Targets":
818914
common_args = cls._common_fields_from_dict(targets_dict)
819915
targets = targets_dict.pop("targets")
820916
delegations = targets_dict.pop("delegations", None)
917+
if delegations:
918+
delegations = Delegations.from_dict(delegations)
821919
# All fields left in the targets_dict are unrecognized.
822920
return cls(*common_args, targets, delegations, targets_dict)
823921

@@ -826,7 +924,7 @@ def to_dict(self) -> Dict[str, Any]:
826924
targets_dict = self._common_fields_to_dict()
827925
targets_dict["targets"] = self.targets
828926
if self.delegations:
829-
targets_dict["delegations"] = self.delegations
927+
targets_dict["delegations"] = self.delegations.to_dict()
830928
return targets_dict
831929

832930
# Modification.

0 commit comments

Comments
 (0)