diff --git a/src/_secureaccesstokenauthorizer/_storage_client.py b/src/_secureaccesstokenauthorizer/_storage_client.py index 0648ab190bfd665d00ff502c396beb57144f49bd..4560c06266646e19c7b2e14095789de000ff6dd6 100644 --- a/src/_secureaccesstokenauthorizer/_storage_client.py +++ b/src/_secureaccesstokenauthorizer/_storage_client.py @@ -67,3 +67,12 @@ class SecureAccessTokenAuthorizerStorageClient(object): allocated_size, canary, ) + + def get_buckets( + self, + storage_index, + ): + return self._rref.callRemote( + "get_buckets", + storage_index, + ) diff --git a/src/_secureaccesstokenauthorizer/_storage_server.py b/src/_secureaccesstokenauthorizer/_storage_server.py index 5593c289273d59c12c7f32a0e25f2c843008e439..7e4abcbccf5316e00972b2dc6c5b333cfd6c6ba3 100644 --- a/src/_secureaccesstokenauthorizer/_storage_server.py +++ b/src/_secureaccesstokenauthorizer/_storage_server.py @@ -20,14 +20,12 @@ This is the server part of a storage access protocol. The client part is implemented in ``_storage_client.py``. """ +import attr + from zope.interface import ( implementer_only, ) -from twisted.python.components import ( - proxyForInterface, -) - from foolscap.constraint import ( ByteStringConstraint, ) @@ -103,14 +101,23 @@ class RITokenAuthorizedStorageServer(RemoteInterface): @implementer_only(RITokenAuthorizedStorageServer, IReferenceable, IRemotelyCallable) -class SecureAccessTokenAuthorizerStorageServer(proxyForInterface(RIStorageServer), Referenceable): - def allocate_buckets(self, tokens, *a, **kw): +@attr.s +class SecureAccessTokenAuthorizerStorageServer(Referenceable): + _original = attr.ib() + + def _validate_tokens(self, tokens): + pass + + def remote_allocate_buckets(self, tokens, *a, **kw): self._validate_tokens(tokens) - return super(SecureAccessTokenAuthorizerStorageServer, self).allocate_buckets(*a, **kw) + return self._original.remote_allocate_buckets(*a, **kw) + + def remote_get_buckets(self, storage_index): + return self._original.remote_get_buckets(storage_index) - def add_lease(self, tokens, *a, **kw): + def remote_add_lease(self, tokens, *a, **kw): self._validate_tokens(tokens) - return super(SecureAccessTokenAuthorizerStorageServer, self).add_lease(*a, **kw) + return self._original.remote_allocate_buckets(*a, **kw) # I don't understand why this is required. # SecureAccessTokenAuthorizerStorageServer is-a Referenceable. It seems like diff --git a/src/_secureaccesstokenauthorizer/tests/strategies.py b/src/_secureaccesstokenauthorizer/tests/strategies.py index eb008c7767d77edc061f550523a2475e30acdfe4..945ad614e0a1ed8f3c4022008e641e3e983f74e3 100644 --- a/src/_secureaccesstokenauthorizer/tests/strategies.py +++ b/src/_secureaccesstokenauthorizer/tests/strategies.py @@ -18,6 +18,15 @@ Hypothesis strategies for property testing. from hypothesis.strategies import ( just, + binary, + integers, + sets, +) + +from allmydata.interfaces import ( + StorageIndex, + LeaseRenewSecret, + LeaseCancelSecret, ) def configurations(): @@ -25,3 +34,65 @@ def configurations(): Build configuration values for the plugin. """ return just({}) + + +def storage_indexes(): + """ + Build Tahoe-LAFS storage indexes. + """ + return binary( + min_size=StorageIndex.minLength, + max_size=StorageIndex.maxLength, + ) + + +def lease_renew_secrets(): + """ + Build Tahoe-LAFS lease renewal secrets. + """ + return binary( + min_size=LeaseRenewSecret.minLength, + max_size=LeaseRenewSecret.maxLength, + ) + + +def lease_cancel_secrets(): + """ + Build Tahoe-LAFS lease cancellation secrets. + """ + return binary( + min_size=LeaseCancelSecret.minLength, + max_size=LeaseCancelSecret.maxLength, + ) + + +def sharenums(): + """ + Build Tahoe-LAFS share numbers. + """ + return integers( + min_value=0, + max_value=255, + ) + + +def sharenum_sets(): + """ + Build sets of Tahoe-LAFS share numbers. + """ + return sets( + sharenums(), + min_size=1, + max_size=255, + ) + + +def sizes(): + """ + Build Tahoe-LAFS share sizes. + """ + return integers( + min_value=0, + # Just for practical purposes... + max_value=2 ** 16, + ) diff --git a/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py b/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..e19d67725d869fd770f702340f12e1bbbec5631f --- /dev/null +++ b/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py @@ -0,0 +1,168 @@ +# Copyright 2019 PrivateStorage.io, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for communication between the client and server components. +""" +import attr + +from fixtures import ( + Fixture, + TempDir, +) +from testtools import ( + TestCase, +) +from testtools.matchers import ( + Equals, +) +from testtools.twistedsupport._deferred import ( + # I'd rather use https://twistedmatrix.com/trac/ticket/8900 but efforts + # there appear to have stalled. + extract_result, +) + +from hypothesis import ( + given, +) + +from twisted.internet.defer import ( + execute, +) + +from foolscap.referenceable import ( + LocalReferenceable, +) + +from allmydata.storage.server import ( + StorageServer, +) + +from .strategies import ( + storage_indexes, + lease_renew_secrets, + lease_cancel_secrets, + sharenum_sets, + sizes, +) + +from ..api import ( + SecureAccessTokenAuthorizerStorageServer, + SecureAccessTokenAuthorizerStorageClient, +) + +def bytes_for_share(sharenum, size): + """ + Generate marginally distinctive bytes of a certain length for the given + share number. + """ + if 0 <= sharenum <= 255: + return (unichr(sharenum) * size).encode("latin-1") + raise ValueError("Sharenum must be between 0 and 255 inclusive.") + + +class AnonymousStorageServer(Fixture): + def _setUp(self): + self.tempdir = self.useFixture(TempDir()).join(b"storage") + self.storage_server = StorageServer( + self.tempdir, + b"x" * 20, + ) + + +@attr.s +class LocalRemote(object): + _referenceable = attr.ib() + + def callRemote(self, methname, *args, **kwargs): + return execute( + getattr(self._referenceable, "remote_" + methname), + *args, + **kwargs + ) + + +class ImmutableTests(TestCase): + """ + Tests for interaction with immutable shares. + """ + @given( + storage_index=storage_indexes(), + renew_secret=lease_renew_secrets(), + cancel_secret=lease_cancel_secrets(), + sharenums=sharenum_sets(), + size=sizes(), + ) + def test_create(self, storage_index, renew_secret, cancel_secret, sharenums, size): + """ + Immutable share data created using *allocate_buckets* and methods of the + resulting buckets can be read back using *get_buckets* and methods of + those resulting buckets. + """ + anonymous_storage_server = self.useFixture(AnonymousStorageServer()).storage_server + + def get_tokens(): + return [u"x"] + + server = SecureAccessTokenAuthorizerStorageServer( + anonymous_storage_server, + ) + local_remote_server = LocalRemote(server) + client = SecureAccessTokenAuthorizerStorageClient( + get_rref=lambda: local_remote_server, + get_tokens=get_tokens, + ) + + alreadygot, allocated = extract_result( + client.allocate_buckets( + storage_index, + renew_secret, + cancel_secret, + sharenums, + size, + canary=LocalReferenceable(None), + ), + ) + self.expectThat( + alreadygot, + Equals(set()), + u"fresh server somehow already had shares", + ) + self.expectThat( + set(allocated.keys()), + Equals(sharenums), + u"fresh server refused to allocate all requested buckets", + ) + + for sharenum, bucket in allocated.items(): + # returns None, nothing to extract + bucket.remote_write(0, bytes_for_share(sharenum, size)), + # returns None, nothing to extract + bucket.remote_close() + + readers = extract_result(client.get_buckets(storage_index)) + + self.expectThat( + set(readers.keys()), + Equals(sharenums), + u"server did not return all buckets we wrote", + ) + for (sharenum, bucket) in readers.items(): + self.expectThat( + bucket.remote_read(0, size), + Equals(bytes_for_share(sharenum, size)), + u"server returned wrong bytes for share number {}".format( + sharenum, + ), + )