Skip to content

Commit

Permalink
Add a Role class and integrate it into Root
Browse files Browse the repository at this point in the history
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.

Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
  • Loading branch information
MVrachev committed Apr 21, 2021
1 parent f73ac31 commit 26982f7
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 15 deletions.
9 changes: 6 additions & 3 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,21 +306,21 @@ def test_metadata_root(self):
root_key2['keytype'], root_key2['scheme'], root_key2['keyval'])

# Assert that root does not contain the new key
self.assertNotIn(keyid, root.signed.roles['root']['keyids'])
self.assertNotIn(keyid, root.signed.roles['root'].keyids)
self.assertNotIn(keyid, root.signed.keys)

# Add new root key
root.signed.add_key('root', keyid, key_metadata)

# Assert that key is added
self.assertIn(keyid, root.signed.roles['root']['keyids'])
self.assertIn(keyid, root.signed.roles['root'].keyids)
self.assertIn(keyid, root.signed.keys)

# Remove the key
root.signed.remove_key('root', keyid)

# Assert that root does not contain the new key anymore
self.assertNotIn(keyid, root.signed.roles['root']['keyids'])
self.assertNotIn(keyid, root.signed.roles['root'].keyids)
self.assertNotIn(keyid, root.signed.keys)


Expand Down Expand Up @@ -369,6 +369,9 @@ def test_support_for_unrecognized_fields(self):
if metadata == "root":
for keyid in dict1["signed"]["keys"].keys():
dict1["signed"]["keys"][keyid]["d"] = "c"
for role_str in dict1["signed"]["roles"].keys():
dict1["signed"]["roles"][role_str]["e"] = "g"

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

Expand Down
65 changes: 53 additions & 12 deletions tuf/api/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,41 @@ def get_public_val(self):
return self.keyval["public"]


class Role:
"""A container class containing the set of keyids and threshold associated
with a particular role.
Attributes:
keyids: A set of strings each of which represents a given key.
threshold: An integer representing the required number of keys for that
particular role.
unrecognized_fields: Dictionary of all unrecognized fields.
"""

def __init__(
self,
keyids: set,
threshold: int,
unrecognized_fields: Optional[Mapping[str, Any]] = None,
) -> None:
self.keyids = keyids
self.threshold = threshold
if unrecognized_fields is None:
unrecognized_fields = {}
self.unrecognized_fields = unrecognized_fields

def to_dict(self) -> Dict:
"""Returns the dictionary representation of self."""
res_dict = {
"keyids": self.keyids,
"threshold": self.threshold,
**self.unrecognized_fields,
}

return res_dict


class Root(Signed):
"""A container for the signed part of root metadata.
Expand All @@ -465,10 +500,7 @@ class Root(Signed):
roles: A dictionary that contains a list of signing keyids and
a signature threshold for each top level role::
{
'<ROLE>': {
'keyids': ['<SIGNING KEY KEYID>', ...],
'threshold': <SIGNATURE THRESHOLD>,
},
'<ROLE>': <Role istance>,
...
}
Expand All @@ -486,13 +518,12 @@ def __init__(
expires: datetime,
consistent_snapshot: bool,
keys: Mapping[str, Key],
roles: Mapping[str, Any],
roles: Mapping[str, Role],
unrecognized_fields: Optional[Mapping[str, Any]] = None,
) -> None:
super().__init__(
_type, version, spec_version, expires, unrecognized_fields
)
# TODO: Add a class for roles
self.consistent_snapshot = consistent_snapshot
self.keys = keys
self.roles = roles
Expand All @@ -513,6 +544,13 @@ def from_dict(cls, root_dict: Mapping[str, Any]) -> "Root":
unrecognized_key_fields = key
keys[keyid] = Key(keytype, scheme, keyval, unrecognized_key_fields)

for role_str, role_dict in roles.items():
keyids = role_dict.pop("keyids")
threshold = role_dict.pop("threshold")
# All fields left in the role_dict are unrecognized.
unrecognized_role_fields = role_dict
roles[role_str] = Role(keyids, threshold, unrecognized_role_fields)

# All fields left in the root_dict are unrecognized.
unrecognized_fields = root_dict
return cls(
Expand All @@ -525,12 +563,15 @@ def to_dict(self) -> Dict[str, Any]:
keys = {}
for keyid, key in self.keys.items():
keys[keyid] = key.to_dict()
roles = {}
for role_str, role in self.roles.items():
roles[role_str] = role.to_dict()

root_dict.update(
{
"consistent_snapshot": self.consistent_snapshot,
"keys": keys,
"roles": self.roles,
"roles": roles,
}
)
return root_dict
Expand All @@ -540,17 +581,17 @@ def add_key(
self, role: str, keyid: str, key_metadata: Mapping[str, Any]
) -> None:
"""Adds new key for 'role' and updates the key store. """
if keyid not in self.roles[role]["keyids"]:
self.roles[role]["keyids"].append(keyid)
if keyid not in self.roles[role].keyids:
self.roles[role].keyids.append(keyid)
self.keys[keyid] = key_metadata

# Remove key for a role.
def remove_key(self, role: str, keyid: str) -> None:
"""Removes key for 'role' and updates the key store. """
if keyid in self.roles[role]["keyids"]:
self.roles[role]["keyids"].remove(keyid)
if keyid in self.roles[role].keyids:
self.roles[role].keyids.remove(keyid)
for keyinfo in self.roles.values():
if keyid in keyinfo["keyids"]:
if keyid in keyinfo.keyids:
return

del self.keys[keyid]
Expand Down

0 comments on commit 26982f7

Please sign in to comment.