Skip to content
Snippets Groups Projects
Select Git revision
  • 7a6eb5edd766c309c1059865949970e11756a6cb
  • main default protected
  • restricted-sqlite-strategies
  • compare-structured-sql-dumps
  • 235.backup-and-recovery.recovery
  • mypy
  • real-spender
  • github/fork/tp-la/real-spender
  • 260.metric-rejected-zkaps
  • implicit-lease-renewal-problems
  • mach-nix
  • github/fork/tp-la/deep-traverse
  • v0.0
13 results

eliot.py

Blame
  • _storage_server.py 9.06 KiB
    # 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.
    
    """
    A Tahoe-LAFS RIStorageServer-alike which authorizes writes and lease
    updates using a per-call token.
    
    This is the server part of a storage access protocol.  The client part is
    implemented in ``_storage_client.py``.
    """
    
    import attr
    from attr.validators import (
        provides,
    )
    
    from zope.interface import (
        implementer_only,
    )
    
    from foolscap.constraint import (
        ByteStringConstraint,
    )
    from foolscap.api import (
        ListOf,
        Referenceable,
    )
    from foolscap.ipb import (
        IReferenceable,
        IRemotelyCallable,
    )
    from foolscap.remoteinterface import (
        RemoteMethodSchema,
        RemoteInterface,
    )
    
    from allmydata.interfaces import (
        RIStorageServer,
    )
    
    # The Foolscap convention seems to be to try to constrain inputs to valid
    # values.  So we'll try to limit the number of tokens a client can supply.
    # Foolscap may be moving away from this so we may eventually drop this as
    # well.  Though it may still make sense on a non-Foolscap protocol (eg HTTP)
    # which Tahoe-LAFS may eventually support.
    #
    # In any case, for now, pick some fairly arbitrary value.  I am deliberately
    # picking a small number here and expect to have to raise.  However, ideally,
    # a client could accomplish a lot with a few tokens while also not wasting a
    # lot of value.
    MAXIMUM_TOKENS_PER_CALL = 10
    
    # This is the length of a serialized PrivacyPass pass (there's a lot of
    # confusion between "tokens" and "passes" here, sadly).
    TOKEN_LENGTH = 97
    
    # Take those values and turn them into the appropriate Foolscap constraint
    # objects.  Foolscap seems to have a convention of representing these as
    # CamelCase module-level values so I replicate that here.
    Token = ByteStringConstraint(maxLength=TOKEN_LENGTH, minLength=TOKEN_LENGTH)
    TokenList = ListOf(Token, maxLength=MAXIMUM_TOKENS_PER_CALL)
    
    
    def add_tokens(schema):
        """
        Add a ``tokens`` parameter to the given method schema.
    
        :param foolscap.remoteinterface.RemoteMethodSchema schema: An existing
            method schema to modify.
    
        :return foolscap.remoteinterface.RemoteMethodSchema: A schema like
            ``schema`` but with one additional required argument.
        """
        return add_arguments(schema, [(b"tokens", TokenList)])
    
    
    def add_arguments(schema, kwargs):
        """
        Create a new schema like ``schema`` but with the arguments given by
        ``kwargs`` prepended to the signature.
    
        :param foolscap.remoteinterface.RemoteMethodSchema schema: The existing
            schema.
    
        :param list[(bytes, foolscap.IConstraint)] kwargs: The arguments to
            prepend to the signature of ``schema``.
    
        :return foolscap.remoteinterface.RemoteMethodSchema: The new schema
            object.
        """
        new_kwargs = dict(schema.argConstraints)
        new_kwargs.update(kwargs)
        modified_schema = RemoteMethodSchema(**new_kwargs)
        # Initialized from **new_kwargs, RemoteMethodSchema.argumentNames is in
        # some arbitrary, probably-incorrect order.  This breaks user code which
        # tries to use positional arguments.  Put them back in the order they were
        # in originally (in the input ``schema``), prepended with the newly added
        # arguments.
        modified_schema.argumentNames = (
            # The new arguments
            list(argName for (argName, _) in kwargs) +
            # The original arguments in the original order
            schema.argumentNames
        )
        return modified_schema
    
    
    
    class RITokenAuthorizedStorageServer(RemoteInterface):
        """
        An object which can store and retrieve shares, subject to token-based
        authorization.
    
        This is much the same as ``allmydata.interfaces.RIStorageServer`` but
        several of its methods take an additional ``tokens`` parameter.  Clients
        are expected to supply suitable tokens and only after the tokens have been
        validated is service provided.
        """
        __remote_name__ = (
            "RITokenAuthorizedStorageServer.tahoe.privatestorage.io"
        )
    
        get_version = RIStorageServer["get_version"]
    
        allocate_buckets = add_tokens(RIStorageServer["allocate_buckets"])
    
        add_lease = add_tokens(RIStorageServer["add_lease"])
    
        renew_lease = add_tokens(RIStorageServer["renew_lease"])
    
        get_buckets = RIStorageServer["get_buckets"]
    
        slot_readv = RIStorageServer["slot_readv"]
    
        slot_testv_and_readv_and_writev = add_tokens(
            RIStorageServer["slot_testv_and_readv_and_writev"],
        )
    
        advise_corrupt_share = RIStorageServer["advise_corrupt_share"]
    
    
    
    @implementer_only(RITokenAuthorizedStorageServer, IReferenceable, IRemotelyCallable)
    # It would be great to use `frozen=True` (value-based hashing) instead of
    # `cmp=False` (identity based hashing) but Referenceable wants to set some
    # attributes on self and it's hard to avoid that.
    @attr.s(cmp=False)
    class ZKAPAuthorizerStorageServer(Referenceable):
        """
        A class which wraps an ``RIStorageServer`` to insert token validity checks
        before allowing certain functionality.
        """
        _original = attr.ib(validator=provides(RIStorageServer))
    
        def _validate_tokens(self, tokens):
            """
            Check that all of the given tokens are valid.
    
            :raise InvalidToken: If any token in ``tokens`` is not valid.
    
            :return NoneType: If all of the tokens in ``tokens`` are valid.
    
            :note: This is yet to be implemented so it always returns ``None``.
            """
            return None
    
        def remote_get_version(self):
            """
            Pass through without token check to allow clients to learn about our
            version and configuration in case it helps them decide how to behave.
            """
            return self._original.remote_get_version()
    
        def remote_allocate_buckets(self, tokens, *a, **kw):
            """
            Pass through after a token check to ensure that clients can only allocate
            storage for immutable shares if they present valid tokens.
            """
            self._validate_tokens(tokens)
            return self._original.remote_allocate_buckets(*a, **kw)
    
        def remote_get_buckets(self, storage_index):
            """
            Pass through without token check to let clients read immutable shares as
            long as those shares exist.
            """
            return self._original.remote_get_buckets(storage_index)
    
        def remote_add_lease(self, tokens, *a, **kw):
            """
            Pass through after a token check to ensure clients can only extend the
            duration of share storage if they present valid tokens.
            """
            self._validate_tokens(tokens)
            return self._original.remote_add_lease(*a, **kw)
    
        def remote_renew_lease(self, tokens, *a, **kw):
            """
            Pass through after a token check to ensure clients can only extend the
            duration of share storage if they present valid tokens.
            """
            self._validate_tokens(tokens)
            return self._original.remote_renew_lease(*a, **kw)
    
        def remote_advise_corrupt_share(self, *a, **kw):
            """
            Pass through without a token check to let clients inform us of possible
            issues with the system without incurring any cost to themselves.
            """
            return self._original.remote_advise_corrupt_share(*a, **kw)
    
        def remote_slot_testv_and_readv_and_writev(self, tokens, *a, **kw):
            """
            Pass through after a token check to ensure clients can only allocate
            storage for mutable shares if they present valid tokens.
    
            :note: This method can be used both to allocate storage and to rewrite
                data in already-allocated storage.  These cases may not be the
                same from the perspective of token validation.
            """
            self._validate_tokens(tokens)
            # Skip over the remotely exposed method and jump to the underlying
            # implementation which accepts one additional parameter that we know
            # about (and don't expose over the network): renew_leases.  We always
            # pass False for this because we want to manage leases completely
            # separately from writes.
            kw["renew_leases"] = False
            return self._original.slot_testv_and_readv_and_writev(*a, **kw)
    
        def remote_slot_readv(self, *a, **kw):
            """
            Pass through without a token check to let clients read mutable shares as
            long as those shares exist.
            """
            return self._original.remote_slot_readv(*a, **kw)
    
    # I don't understand why this is required.
    # ZKAPAuthorizerStorageServer is-a Referenceable.  It seems like
    # the built in adapter should take care of this case.
    from twisted.python.components import (
        registerAdapter,
    )
    from foolscap.referenceable import (
        ReferenceableSlicer,
    )
    from foolscap.ipb import (
        ISlicer,
    )
    registerAdapter(ReferenceableSlicer, ZKAPAuthorizerStorageServer, ISlicer)