source: trunk/src/allmydata/storage/lease.py

Last change on this file was 1cfe843d, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-22T23:40:25Z

more python2 removal

  • Property mode set to 100644
File size: 12.5 KB
Line 
1"""
2Ported to Python 3.
3"""
4
5import struct, time
6
7import attr
8
9from zope.interface import (
10    Interface,
11    implementer,
12)
13
14from twisted.python.components import (
15    proxyForInterface,
16)
17
18from allmydata.util.hashutil import timing_safe_compare
19from allmydata.util import base32
20
21# struct format for representation of a lease in an immutable share
22IMMUTABLE_FORMAT = ">L32s32sL"
23
24# struct format for representation of a lease in a mutable share
25MUTABLE_FORMAT = ">LL32s32s20s"
26
27
28class ILeaseInfo(Interface):
29    """
30    Represent a marker attached to a share that indicates that share should be
31    retained for some amount of time.
32
33    Typically clients will create and renew leases on their shares as a way to
34    inform storage servers that there is still interest in those shares.  A
35    share may have more than one lease.  If all leases on a share have
36    expiration times in the past then the storage server may take this as a
37    strong hint that no one is interested in the share anymore and therefore
38    the share may be deleted to reclaim the space.
39    """
40    def renew(new_expire_time):
41        """
42        Create a new ``ILeaseInfo`` with the given expiration time.
43
44        :param Union[int, float] new_expire_time: The expiration time the new
45            ``ILeaseInfo`` will have.
46
47        :return: The new ``ILeaseInfo`` provider with the new expiration time.
48        """
49
50    def get_expiration_time():
51        """
52        :return Union[int, float]: this lease's expiration time
53        """
54
55    def get_grant_renew_time_time():
56        """
57        :return Union[int, float]: a guess about the last time this lease was
58            renewed
59        """
60
61    def get_age():
62        """
63        :return Union[int, float]: a guess about how long it has been since this
64            lease was renewed
65        """
66
67    def to_immutable_data():
68        """
69        :return bytes: a serialized representation of this lease suitable for
70            inclusion in an immutable container
71        """
72
73    def to_mutable_data():
74        """
75        :return bytes: a serialized representation of this lease suitable for
76            inclusion in a mutable container
77        """
78
79    def immutable_size():
80        """
81        :return int: the size of the serialized representation of this lease in an
82            immutable container
83        """
84
85    def mutable_size():
86        """
87        :return int: the size of the serialized representation of this lease in a
88            mutable container
89        """
90
91    def is_renew_secret(candidate_secret):
92        """
93        :return bool: ``True`` if the given byte string is this lease's renew
94            secret, ``False`` otherwise
95        """
96
97    def present_renew_secret():
98        """
99        :return str: Text which could reasonably be shown to a person representing
100            this lease's renew secret.
101        """
102
103    def is_cancel_secret(candidate_secret):
104        """
105        :return bool: ``True`` if the given byte string is this lease's cancel
106            secret, ``False`` otherwise
107        """
108
109    def present_cancel_secret():
110        """
111        :return str: Text which could reasonably be shown to a person representing
112            this lease's cancel secret.
113        """
114
115
116@implementer(ILeaseInfo)
117@attr.s(frozen=True)
118class LeaseInfo(object):
119    """
120    Represent the details of one lease, a marker which is intended to inform
121    the storage server how long to store a particular share.
122    """
123    owner_num = attr.ib(default=None)
124
125    # Don't put secrets into the default string representation.  This makes it
126    # slightly less likely the secrets will accidentally be leaked to
127    # someplace they're not meant to be.
128    renew_secret = attr.ib(default=None, repr=False)
129    cancel_secret = attr.ib(default=None, repr=False)
130
131    _expiration_time = attr.ib(default=None)
132
133    nodeid = attr.ib(default=None)
134
135    @nodeid.validator
136    def _validate_nodeid(self, attribute, value):
137        if value is not None:
138            if not isinstance(value, bytes):
139                raise ValueError(
140                    "nodeid value must be bytes, not {!r}".format(value),
141                )
142            if len(value) != 20:
143                raise ValueError(
144                    "nodeid value must be 20 bytes long, not {!r}".format(value),
145                )
146        return None
147
148    def get_expiration_time(self):
149        # type: () -> float
150        """
151        Retrieve a POSIX timestamp representing the time at which this lease is
152        set to expire.
153        """
154        return self._expiration_time
155
156    def renew(self, new_expire_time):
157        # type: (float) -> LeaseInfo
158        """
159        Create a new lease the same as this one but with a new expiration time.
160
161        :param new_expire_time: The new expiration time.
162
163        :return: The new lease info.
164        """
165        return attr.assoc(
166            self,
167            # MyPy is unhappy with this; long-term solution is likely switch to
168            # new @frozen attrs API, with type annotations.
169            _expiration_time=new_expire_time,  # type: ignore[call-arg]
170        )
171
172    def is_renew_secret(self, candidate_secret):
173        # type: (bytes) -> bool
174        """
175        Check a string to see if it is the correct renew secret.
176
177        :return: ``True`` if it is the correct renew secret, ``False``
178            otherwise.
179        """
180        return timing_safe_compare(self.renew_secret, candidate_secret)
181
182    def present_renew_secret(self):
183        # type: () -> str
184        """
185        Return the renew secret, base32-encoded.
186        """
187        return str(base32.b2a(self.renew_secret), "utf-8")
188
189    def is_cancel_secret(self, candidate_secret):
190        # type: (bytes) -> bool
191        """
192        Check a string to see if it is the correct cancel secret.
193
194        :return: ``True`` if it is the correct cancel secret, ``False``
195            otherwise.
196        """
197        return timing_safe_compare(self.cancel_secret, candidate_secret)
198
199    def present_cancel_secret(self):
200        # type: () -> str
201        """
202        Return the cancel secret, base32-encoded.
203        """
204        return str(base32.b2a(self.cancel_secret), "utf-8")
205
206    def get_grant_renew_time_time(self):
207        # hack, based upon fixed 31day expiration period
208        return self._expiration_time - 31*24*60*60
209
210    def get_age(self):
211        return time.time() - self.get_grant_renew_time_time()
212
213    @classmethod
214    def from_immutable_data(cls, data):
215        """
216        Create a new instance from the encoded data given.
217
218        :param data: A lease serialized using the immutable-share-file format.
219        """
220        names = [
221            "owner_num",
222            "renew_secret",
223            "cancel_secret",
224            "expiration_time",
225        ]
226        values = struct.unpack(IMMUTABLE_FORMAT, data)
227        return cls(nodeid=None, **dict(zip(names, values)))
228
229    def immutable_size(self):
230        """
231        :return int: The size, in bytes, of the representation of this lease in an
232            immutable share file.
233        """
234        return struct.calcsize(IMMUTABLE_FORMAT)
235
236    def mutable_size(self):
237        """
238        :return int: The size, in bytes, of the representation of this lease in a
239            mutable share file.
240        """
241        return struct.calcsize(MUTABLE_FORMAT)
242
243    def to_immutable_data(self):
244        return struct.pack(IMMUTABLE_FORMAT,
245                           self.owner_num,
246                           self.renew_secret, self.cancel_secret,
247                           int(self._expiration_time))
248
249    def to_mutable_data(self):
250        return struct.pack(MUTABLE_FORMAT,
251                           self.owner_num,
252                           int(self._expiration_time),
253                           self.renew_secret, self.cancel_secret,
254                           self.nodeid)
255
256    @classmethod
257    def from_mutable_data(cls, data):
258        """
259        Create a new instance from the encoded data given.
260
261        :param data: A lease serialized using the mutable-share-file format.
262        """
263        names = [
264            "owner_num",
265            "expiration_time",
266            "renew_secret",
267            "cancel_secret",
268            "nodeid",
269        ]
270        values = struct.unpack(MUTABLE_FORMAT, data)
271        return cls(**dict(zip(names, values)))
272
273
274@attr.s(frozen=True)
275class HashedLeaseInfo(proxyForInterface(ILeaseInfo, "_lease_info")): # type: ignore # unsupported dynamic base class
276    """
277    A ``HashedLeaseInfo`` wraps lease information in which the secrets have
278    been hashed.
279    """
280    _lease_info = attr.ib()
281    _hash = attr.ib()
282
283    # proxyForInterface will take care of forwarding all methods on ILeaseInfo
284    # to `_lease_info`.  Here we override a few of those methods to adjust
285    # their behavior to make them suitable for use with hashed secrets.
286
287    def renew(self, new_expire_time):
288        # Preserve the HashedLeaseInfo wrapper around the renewed LeaseInfo.
289        return attr.assoc(
290            self,
291            _lease_info=super(HashedLeaseInfo, self).renew(new_expire_time),
292        )
293
294    def is_renew_secret(self, candidate_secret):
295        # type: (bytes) -> bool
296        """
297        Hash the candidate secret and compare the result to the stored hashed
298        secret.
299        """
300        return super(HashedLeaseInfo, self).is_renew_secret(self._hash(candidate_secret))
301
302    def present_renew_secret(self):
303        # type: () -> str
304        """
305        Present the hash of the secret with a marker indicating it is a hash.
306        """
307        return u"hash:" + super(HashedLeaseInfo, self).present_renew_secret()
308
309    def is_cancel_secret(self, candidate_secret):
310        # type: (bytes) -> bool
311        """
312        Hash the candidate secret and compare the result to the stored hashed
313        secret.
314        """
315        if isinstance(candidate_secret, _HashedCancelSecret):
316            # Someone read it off of this object in this project - probably
317            # the lease crawler - and is just trying to use it to identify
318            # which lease it wants to operate on.  Avoid re-hashing the value.
319            #
320            # It is important that this codepath is only availably internally
321            # for this process to talk to itself.  If it were to be exposed to
322            # clients over the network, they could just provide the hashed
323            # value to avoid having to ever learn the original value.
324            hashed_candidate = candidate_secret.hashed_value
325        else:
326            # It is not yet hashed so hash it.
327            hashed_candidate = self._hash(candidate_secret)
328
329        return super(HashedLeaseInfo, self).is_cancel_secret(hashed_candidate)
330
331    def present_cancel_secret(self):
332        # type: () -> str
333        """
334        Present the hash of the secret with a marker indicating it is a hash.
335        """
336        return u"hash:" + super(HashedLeaseInfo, self).present_cancel_secret()
337
338    @property
339    def owner_num(self):
340        return self._lease_info.owner_num
341
342    @property
343    def nodeid(self):
344        return self._lease_info.nodeid
345
346    @property
347    def cancel_secret(self):
348        """
349        Give back an opaque wrapper around the hashed cancel secret which can
350        later be presented for a succesful equality comparison.
351        """
352        # We don't *have* the cancel secret.  We hashed it and threw away the
353        # original.  That's good.  It does mean that some code that runs
354        # in-process with the storage service (LeaseCheckingCrawler) runs into
355        # some difficulty.  That code wants to cancel leases and does so using
356        # the same interface that faces storage clients (or would face them,
357        # if lease cancellation were exposed).
358        #
359        # Since it can't use the hashed secret to cancel a lease (that's the
360        # point of the hashing) and we don't have the unhashed secret to give
361        # it, instead we give it a marker that `cancel_lease` will recognize.
362        # On recognizing it, if the hashed value given matches the hashed
363        # value stored it is considered a match and the lease can be
364        # cancelled.
365        #
366        # This isn't great.  Maybe the internal and external consumers of
367        # cancellation should use different interfaces.
368        return _HashedCancelSecret(self._lease_info.cancel_secret)
369
370
371@attr.s(frozen=True)
372class _HashedCancelSecret(object):
373    """
374    ``_HashedCancelSecret`` is a marker type for an already-hashed lease
375    cancel secret that lets internal lease cancellers bypass the hash-based
376    protection that's imposed on external lease cancellers.
377
378    :ivar bytes hashed_value: The already-hashed secret.
379    """
380    hashed_value = attr.ib()
Note: See TracBrowser for help on using the repository browser.