mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-19 13:07:56 +00:00
Merge pull request #1182 from tahoe-lafs/3876-per-bucket-upload-secret
Allow per-bucket upload secret Fixes ticket:3876
This commit is contained in:
commit
5be7cbc171
@ -493,8 +493,8 @@ Handling repeat calls:
|
|||||||
* If the same API call is repeated with the same upload secret, the response is the same and no change is made to server state.
|
* If the same API call is repeated with the same upload secret, the response is the same and no change is made to server state.
|
||||||
This is necessary to ensure retries work in the face of lost responses from the server.
|
This is necessary to ensure retries work in the face of lost responses from the server.
|
||||||
* If the API calls is with a different upload secret, this implies a new client, perhaps because the old client died.
|
* If the API calls is with a different upload secret, this implies a new client, perhaps because the old client died.
|
||||||
In order to prevent storage servers from being able to mess with each other, this API call will fail, because the secret doesn't match.
|
Or it may happen because the client wants to upload a different share number than a previous client.
|
||||||
The use case of restarting upload from scratch if the client dies can be implemented by having the client persist the upload secret.
|
New shares will be created, existing shares will be unchanged, regardless of whether the upload secret matches or not.
|
||||||
|
|
||||||
Discussion
|
Discussion
|
||||||
``````````
|
``````````
|
||||||
|
0
newsfragments/3876.minor
Normal file
0
newsfragments/3876.minor
Normal file
@ -203,18 +203,13 @@ class HTTPServer(object):
|
|||||||
upload_secret = authorization[Secrets.UPLOAD]
|
upload_secret = authorization[Secrets.UPLOAD]
|
||||||
info = loads(request.content.read())
|
info = loads(request.content.read())
|
||||||
|
|
||||||
if storage_index in self._uploads:
|
# We do NOT validate the upload secret for existing bucket uploads.
|
||||||
for share_number in info["share-numbers"]:
|
# Another upload may be happening in parallel, with a different upload
|
||||||
in_progress = self._uploads[storage_index]
|
# key. That's fine! If a client tries to _write_ to that upload, they
|
||||||
# For pre-existing upload, make sure password matches.
|
# need to have an upload key. That does mean we leak the existence of
|
||||||
if (
|
# these parallel uploads, but if you know storage index you can
|
||||||
share_number in in_progress.upload_secrets
|
# download them once upload finishes, so it's not a big deal to leak
|
||||||
and not timing_safe_compare(
|
# that information.
|
||||||
in_progress.upload_secrets[share_number], upload_secret
|
|
||||||
)
|
|
||||||
):
|
|
||||||
request.setResponseCode(http.UNAUTHORIZED)
|
|
||||||
return b""
|
|
||||||
|
|
||||||
already_got, sharenum_to_bucket = self._storage_server.allocate_buckets(
|
already_got, sharenum_to_bucket = self._storage_server.allocate_buckets(
|
||||||
storage_index,
|
storage_index,
|
||||||
|
@ -1102,18 +1102,15 @@ class _HTTPBucketReader(object):
|
|||||||
class _HTTPStorageServer(object):
|
class _HTTPStorageServer(object):
|
||||||
"""
|
"""
|
||||||
Talk to remote storage server over HTTP.
|
Talk to remote storage server over HTTP.
|
||||||
|
|
||||||
The same upload key is used for all communication.
|
|
||||||
"""
|
"""
|
||||||
_http_client = attr.ib(type=StorageClient)
|
_http_client = attr.ib(type=StorageClient)
|
||||||
_upload_secret = attr.ib(type=bytes)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_http_client(http_client): # type: (StorageClient) -> _HTTPStorageServer
|
def from_http_client(http_client): # type: (StorageClient) -> _HTTPStorageServer
|
||||||
"""
|
"""
|
||||||
Create an ``IStorageServer`` from a HTTP ``StorageClient``.
|
Create an ``IStorageServer`` from a HTTP ``StorageClient``.
|
||||||
"""
|
"""
|
||||||
return _HTTPStorageServer(http_client=http_client, upload_secret=urandom(20))
|
return _HTTPStorageServer(http_client=http_client)
|
||||||
|
|
||||||
def get_version(self):
|
def get_version(self):
|
||||||
return StorageClientGeneral(self._http_client).get_version()
|
return StorageClientGeneral(self._http_client).get_version()
|
||||||
@ -1128,9 +1125,10 @@ class _HTTPStorageServer(object):
|
|||||||
allocated_size,
|
allocated_size,
|
||||||
canary,
|
canary,
|
||||||
):
|
):
|
||||||
|
upload_secret = urandom(20)
|
||||||
immutable_client = StorageClientImmutables(self._http_client)
|
immutable_client = StorageClientImmutables(self._http_client)
|
||||||
result = immutable_client.create(
|
result = immutable_client.create(
|
||||||
storage_index, sharenums, allocated_size, self._upload_secret, renew_secret,
|
storage_index, sharenums, allocated_size, upload_secret, renew_secret,
|
||||||
cancel_secret
|
cancel_secret
|
||||||
)
|
)
|
||||||
result = yield result
|
result = yield result
|
||||||
@ -1140,7 +1138,7 @@ class _HTTPStorageServer(object):
|
|||||||
client=immutable_client,
|
client=immutable_client,
|
||||||
storage_index=storage_index,
|
storage_index=storage_index,
|
||||||
share_number=share_num,
|
share_number=share_num,
|
||||||
upload_secret=self._upload_secret
|
upload_secret=upload_secret
|
||||||
))
|
))
|
||||||
for share_num in result.allocated
|
for share_num in result.allocated
|
||||||
})
|
})
|
||||||
|
@ -474,41 +474,77 @@ class ImmutableHTTPAPITests(SyncTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(downloaded, expected_data[offset : offset + length])
|
self.assertEqual(downloaded, expected_data[offset : offset + length])
|
||||||
|
|
||||||
def test_allocate_buckets_second_time_wrong_upload_key(self):
|
|
||||||
"""
|
|
||||||
If allocate buckets endpoint is called second time with wrong upload
|
|
||||||
key on the same shares, the result is an error.
|
|
||||||
"""
|
|
||||||
# Create a upload:
|
|
||||||
(upload_secret, lease_secret, storage_index, _) = self.create_upload(
|
|
||||||
{1, 2, 3}, 100
|
|
||||||
)
|
|
||||||
with self.assertRaises(ClientException) as e:
|
|
||||||
result_of(
|
|
||||||
self.imm_client.create(
|
|
||||||
storage_index, {2, 3}, 100, b"x" * 32, lease_secret, lease_secret
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertEqual(e.exception.args[0], http.UNAUTHORIZED)
|
|
||||||
|
|
||||||
def test_allocate_buckets_second_time_different_shares(self):
|
def test_allocate_buckets_second_time_different_shares(self):
|
||||||
"""
|
"""
|
||||||
If allocate buckets endpoint is called second time with different
|
If allocate buckets endpoint is called second time with different
|
||||||
upload key on different shares, that creates the buckets.
|
upload key on potentially different shares, that creates the buckets on
|
||||||
|
those shares that are different.
|
||||||
"""
|
"""
|
||||||
# Create a upload:
|
# Create a upload:
|
||||||
(upload_secret, lease_secret, storage_index, created) = self.create_upload(
|
(upload_secret, lease_secret, storage_index, created) = self.create_upload(
|
||||||
{1, 2, 3}, 100
|
{1, 2, 3}, 100
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add same shares:
|
# Write half of share 1
|
||||||
|
result_of(
|
||||||
|
self.imm_client.write_share_chunk(
|
||||||
|
storage_index,
|
||||||
|
1,
|
||||||
|
upload_secret,
|
||||||
|
0,
|
||||||
|
b"a" * 50,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add same shares with a different upload key share 1 overlaps with
|
||||||
|
# existing shares, this call shouldn't overwrite the existing
|
||||||
|
# work-in-progress.
|
||||||
|
upload_secret2 = b"x" * 2
|
||||||
created2 = result_of(
|
created2 = result_of(
|
||||||
self.imm_client.create(
|
self.imm_client.create(
|
||||||
storage_index, {4, 6}, 100, b"x" * 2, lease_secret, lease_secret
|
storage_index,
|
||||||
|
{1, 4, 6},
|
||||||
|
100,
|
||||||
|
upload_secret2,
|
||||||
|
lease_secret,
|
||||||
|
lease_secret,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(created2.allocated, {4, 6})
|
self.assertEqual(created2.allocated, {4, 6})
|
||||||
|
|
||||||
|
# Write second half of share 1
|
||||||
|
self.assertTrue(
|
||||||
|
result_of(
|
||||||
|
self.imm_client.write_share_chunk(
|
||||||
|
storage_index,
|
||||||
|
1,
|
||||||
|
upload_secret,
|
||||||
|
50,
|
||||||
|
b"b" * 50,
|
||||||
|
)
|
||||||
|
).finished
|
||||||
|
)
|
||||||
|
|
||||||
|
# The upload of share 1 succeeded, demonstrating that second create()
|
||||||
|
# call didn't overwrite work-in-progress.
|
||||||
|
downloaded = result_of(
|
||||||
|
self.imm_client.read_share_chunk(storage_index, 1, 0, 100)
|
||||||
|
)
|
||||||
|
self.assertEqual(downloaded, b"a" * 50 + b"b" * 50)
|
||||||
|
|
||||||
|
# We can successfully upload the shares created with the second upload secret.
|
||||||
|
self.assertTrue(
|
||||||
|
result_of(
|
||||||
|
self.imm_client.write_share_chunk(
|
||||||
|
storage_index,
|
||||||
|
4,
|
||||||
|
upload_secret2,
|
||||||
|
0,
|
||||||
|
b"x" * 100,
|
||||||
|
)
|
||||||
|
).finished
|
||||||
|
)
|
||||||
|
|
||||||
def test_list_shares(self):
|
def test_list_shares(self):
|
||||||
"""
|
"""
|
||||||
Once a share is finished uploading, it's possible to list it.
|
Once a share is finished uploading, it's possible to list it.
|
||||||
|
Loading…
Reference in New Issue
Block a user