diff --git a/secure-access-token-authorizer.nix b/secure-access-token-authorizer.nix index be5fabe508d62a74a32234d1678709696a86c981..cb825d72a6899b9bbd278a033b9198dfdf16ac0f 100644 --- a/secure-access-token-authorizer.nix +++ b/secure-access-token-authorizer.nix @@ -9,13 +9,16 @@ buildPythonPackage rec { ]; propagatedBuildInputs = with pythonPackages; [ + attrs zope_interface twisted tahoe-lafs ]; checkInputs = with pythonPackages; [ + fixtures testtools + hypothesis ]; checkPhase = '' diff --git a/src/_secureaccesstokenauthorizer/_plugin.py b/src/_secureaccesstokenauthorizer/_plugin.py index ccede2047b78a7cd3a30ad19bbfbf27c265eb630..4b4019244fa4d4f0b34ec53f7e2b139fd31f4889 100644 --- a/src/_secureaccesstokenauthorizer/_plugin.py +++ b/src/_secureaccesstokenauthorizer/_plugin.py @@ -17,18 +17,33 @@ The Twisted plugin that glues the Secure Access Token system into Tahoe-LAFS. """ +import attr + from zope.interface import ( implementer, ) +from twisted.internet.defer import ( + succeed, +) + from allmydata.interfaces import ( IFoolscapStoragePlugin, + IAnnounceableStorageServer, ) from .api import ( SecureAccessTokenAuthorizerStorageServer, ) +@implementer(IAnnounceableStorageServer) +@attr.s +class AnnounceableStorageServer(object): + announcement = attr.ib() + storage_server = attr.ib() + + + @implementer(IFoolscapStoragePlugin) class SecureAccessTokenAuthorizer(object): """ @@ -38,10 +53,18 @@ class SecureAccessTokenAuthorizer(object): name = u"privatestorageio-satauthz-v1" def get_storage_server(self, configuration, get_anonymous_storage_server): - return SecureAccessTokenAuthorizerStorageServer( + announcement = {} + storage_server = SecureAccessTokenAuthorizerStorageServer( get_anonymous_storage_server(), **configuration ) + return succeed( + AnnounceableStorageServer( + announcement, + storage_server, + ), + ) + def get_storage_client(self, configuration, announcement): raise NotImplementedError() diff --git a/src/_secureaccesstokenauthorizer/_storage_server.py b/src/_secureaccesstokenauthorizer/_storage_server.py index 075a0fbf216b9f25c55b8d20015bf63892fdc903..79585222ff7af9274791eb260f820749f21b9b24 100644 --- a/src/_secureaccesstokenauthorizer/_storage_server.py +++ b/src/_secureaccesstokenauthorizer/_storage_server.py @@ -30,6 +30,7 @@ from foolscap.constraint import ( ) from foolscap.api import ( ListOf, + Referenceable, ) from foolscap.remoteinterface import ( RemoteMethodSchema, @@ -95,7 +96,7 @@ class RITokenAuthorizedStorageServer(RemoteInterface): @implementer(RITokenAuthorizedStorageServer) -class SecureAccessTokenAuthorizerStorageServer(proxyForInterface(RIStorageServer)): +class SecureAccessTokenAuthorizerStorageServer(proxyForInterface(RIStorageServer), Referenceable): def allocate_buckets(self, tokens, *a, **kw): self._validate_tokens(tokens) return super(SecureAccessTokenAuthorizerStorageServer, self).allocate_buckets(*a, **kw) diff --git a/src/_secureaccesstokenauthorizer/tests/matchers.py b/src/_secureaccesstokenauthorizer/tests/matchers.py new file mode 100644 index 0000000000000000000000000000000000000000..29dc8dd46b4070767ca0d896baaff19d0f61f12c --- /dev/null +++ b/src/_secureaccesstokenauthorizer/tests/matchers.py @@ -0,0 +1,40 @@ +# 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. + +""" +Testtools matchers useful for the test suite. +""" + +import attr + +from testtools.matchers import ( + Mismatch, +) + +@attr.s +class Provides(object): + """ + Match objects that provide one or more Zope Interface interfaces. + """ + interfaces = attr.ib() + + def match(self, obj): + missing = set() + for iface in self.interfaces: + if not iface.providedBy(obj): + missing.add(iface) + if missing: + return Mismatch("{} does not provide expected {}".format( + obj, ", ".join(str(iface) for iface in missing), + )) diff --git a/src/_secureaccesstokenauthorizer/tests/strategies.py b/src/_secureaccesstokenauthorizer/tests/strategies.py new file mode 100644 index 0000000000000000000000000000000000000000..eb008c7767d77edc061f550523a2475e30acdfe4 --- /dev/null +++ b/src/_secureaccesstokenauthorizer/tests/strategies.py @@ -0,0 +1,27 @@ +# 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. + +""" +Hypothesis strategies for property testing. +""" + +from hypothesis.strategies import ( + just, +) + +def configurations(): + """ + Build configuration values for the plugin. + """ + return just({}) diff --git a/src/_secureaccesstokenauthorizer/tests/test_plugin.py b/src/_secureaccesstokenauthorizer/tests/test_plugin.py index 2b9913bc58b4208cf1ed67e3a98258bc66d5e054..d57d304cb80a57490d9779a9eafe0af6c41252dd 100644 --- a/src/_secureaccesstokenauthorizer/tests/test_plugin.py +++ b/src/_secureaccesstokenauthorizer/tests/test_plugin.py @@ -25,10 +25,24 @@ from testtools import ( ) from testtools.matchers import ( Contains, + AfterPreprocessing, +) +from testtools.twistedsupport import ( + succeeded, +) + +from hypothesis import ( + given, +) + +from foolscap.ipb import ( + IReferenceable, + IRemotelyCallable, ) from allmydata.interfaces import ( IFoolscapStoragePlugin, + IAnnounceableStorageServer, ) from twisted.plugin import ( @@ -38,6 +52,17 @@ from twisted.plugins.secureaccesstokenauthorizer import ( storage_server, ) +from .strategies import ( + configurations, +) +from .matchers import ( + Provides, +) + +def get_anonymous_storage_server(): + return None + + class PluginTests(TestCase): """ Tests for ``twisted.plugins.secureaccesstokenauthorizer.storage_server``. @@ -57,3 +82,45 @@ class PluginTests(TestCase): ``storage_server`` provides ``IFoolscapStoragePlugin``. """ verifyObject(IFoolscapStoragePlugin, storage_server) + + + @given(configurations()) + def test_returns_announceable(self, configuration): + """ + ``storage_server.get_storage_server`` returns an instance which provides + ``IAnnounceableStorageServer``. + """ + storage_server_deferred = storage_server.get_storage_server( + configuration, + get_anonymous_storage_server, + ) + self.assertThat( + storage_server_deferred, + succeeded(Provides([IAnnounceableStorageServer])), + ) + + + @given(configurations()) + def test_returns_referenceable(self, configuration): + """ + The storage server attached to the result of + ``storage_server.get_storage_server`` provides ``IReferenceable`` and + ``IRemotelyCallable``. + """ + # XXX It's not clear what the actual Foolscap-imposed requirements on + # this object should be. Maybe the two above-mentioned interfaces are + # important ... or maybe not? + + storage_server_deferred = storage_server.get_storage_server( + configuration, + get_anonymous_storage_server, + ) + self.assertThat( + storage_server_deferred, + succeeded( + AfterPreprocessing( + lambda ann: ann.storage_server, + Provides([IReferenceable, IRemotelyCallable]), + ), + ), + )