Skip to content

Commit 5e17617

Browse files
committed
Add repository module
Plan for tuf.repository is: * provides useful functionality for TUF repository-side implementations (repository applications, developer tools, etc) * is minimalistic: only features that most implementations will use should be icluded * Only example implementations will be provided in python-tuf * As more repository implementations are built using tuf.repository we can evaluate what extended functionality is useful In this PR, a single abstract class is added that provides a framework for building repository-modifying tools. In subsequent commits some examples will be added that demonstrate how to use the class. Signed-off-by: Jussi Kukkonen <jkukkonen@google.com>
1 parent 4d99f78 commit 5e17617

2 files changed

Lines changed: 145 additions & 0 deletions

File tree

tuf/repository/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright 2021-2022 python-tuf contributors
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
"""Repository API: A library to help repository implementations"""
5+
6+
from tuf.repository._repository import AbortEdit, Repository

tuf/repository/_repository.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Copyright 2021-2022 python-tuf contributors
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
"""Repository Abstraction for metadata management"""
5+
6+
import logging
7+
from abc import ABC, abstractmethod
8+
from contextlib import contextmanager, suppress
9+
from typing import Dict, Generator, Optional, Tuple
10+
11+
from tuf.api.metadata import Metadata, MetaFile, Signed
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class AbortEdit(Exception):
17+
"""Raise to exit the edit() contextmanager without saving changes"""
18+
19+
20+
class Repository(ABC):
21+
"""Abstract class for metadata modifying implementations
22+
23+
This class is intended to be a base class used in any metadata editing
24+
application, whether it is a real repository server or a developer tool.
25+
26+
Implementations must implement open() and close(), and can then use the
27+
edit() contextmanager to implement actual operations.
28+
29+
A few operations (sign, snapshot and timestamp) are already implemented
30+
in this base class.
31+
"""
32+
33+
@abstractmethod
34+
def open(self, role: str, init: bool = False) -> Metadata:
35+
"""Load a roles metadata from storage or cache, return it
36+
37+
If 'init', then create metadata from scratch"""
38+
raise NotImplementedError
39+
40+
@abstractmethod
41+
def close(self, role: str, md: Metadata, sign_only: bool = False) -> None:
42+
"""Write roles metadata into storage
43+
44+
If sign_only, then just append signatures of all available keys.
45+
46+
If not sign_only, update expiry and version and replace signatures
47+
with ones from all available keys."""
48+
raise NotImplementedError
49+
50+
@contextmanager
51+
def edit(
52+
self, role: str, init: bool = False
53+
) -> Generator[Signed, None, None]:
54+
"""Context manager for editing a roles metadata
55+
56+
Context manager takes care of loading the roles metadata (or creating
57+
new metadata if 'init'), updating expiry and version. The caller can do
58+
other changes to the Signed object and when the context manager exits,
59+
a new version of the roles metadata is stored.
60+
61+
Context manager user can raise AbortEdit from inside the with-block to
62+
cancel the edit: in this case none of the changes are stored.
63+
"""
64+
md = self.open(role, init)
65+
with suppress(AbortEdit):
66+
yield md.signed
67+
self.close(role, md)
68+
69+
def sign(self, role: str) -> None:
70+
"""sign without modifying content, or removing existing signatures"""
71+
md = self.open(role)
72+
self.close(role, md, sign_only=True)
73+
74+
def snapshot(
75+
self, current_targets: Dict[str, MetaFile]
76+
) -> Tuple[Optional[int], Dict[str, MetaFile]]:
77+
"""Update snapshot meta information
78+
79+
Updates the meta information in snapshot according to input.
80+
81+
Arguments:
82+
current_targets: The new currently served targets roles.
83+
84+
Returns: Tuple of
85+
- New snapshot version or None if snapshot was not created
86+
- Meta information for targets metadata that were removed from repository
87+
"""
88+
89+
# Snapshot update is needed if
90+
# * any targets files are not yet in snapshot or
91+
# * any targets version is incorrect
92+
updated_snapshot = False
93+
removed: Dict[str, MetaFile] = {}
94+
95+
with self.edit("snapshot") as snapshot:
96+
for keyname, new_meta in current_targets.items():
97+
if keyname not in snapshot.meta:
98+
updated_snapshot = True
99+
snapshot.meta[keyname] = new_meta
100+
continue
101+
102+
old_meta = snapshot.meta[keyname]
103+
if new_meta.version < old_meta.version:
104+
raise ValueError(f"{keyname} version rollback")
105+
if new_meta.version > old_meta.version:
106+
updated_snapshot = True
107+
snapshot.meta[keyname] = new_meta
108+
removed[keyname] = old_meta
109+
110+
if not updated_snapshot:
111+
# prevent edit() from storing a new snapshot version
112+
raise AbortEdit("Skip snapshot: No targets version changes")
113+
114+
if not updated_snapshot:
115+
# This code is reacheable as edit() handles AbortEdit
116+
logger.debug("Snapshot update not needed") # type: ignore[unreachable]
117+
else:
118+
logger.debug(
119+
"Snapshot v%d, %d targets", snapshot.version, len(snapshot.meta)
120+
)
121+
122+
version = snapshot.version if updated_snapshot else None
123+
return version, removed
124+
125+
def timestamp(self, snapshot_meta: MetaFile) -> Optional[MetaFile]:
126+
"""Update timestamp meta information
127+
128+
Updates timestamp with given snapshot information.
129+
130+
Returns the snapshot that was removed from repository (if any).
131+
"""
132+
with self.edit("timestamp") as timestamp:
133+
old_snapshot_meta = timestamp.snapshot_meta
134+
timestamp.snapshot_meta = snapshot_meta
135+
136+
logger.debug("Timestamp v%d", timestamp.version)
137+
if old_snapshot_meta.version == snapshot_meta.version:
138+
return None
139+
return old_snapshot_meta

0 commit comments

Comments
 (0)