[ZODB-Dev] SecureServerStorage and SecureClientStorage

Johan Dahlin jdahlin@telia.com
03 Oct 2002 15:52:47 -0300


--=-jVffnJ4Qa0jsF1qhwbLp
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

Hi folks

For the past week i have played around a bit with ServerStorage and
ClientStorage and added basic authentication.

Usernames and passwords are sent in clear text to not give a false sense
of security, in other words you have to trust your local network.

No changes to ZODB/ZEO directly, instead everything is subclassed.
It seems to work quite well, however, there are a few problems.

1. When authentication fails everything just locks up. Should this be
considered as a feature?
2. It's authenticated per connection id (storage_id). I don't know if
this is optimal. Isn't it possible that an attacker can send bogus
connection ids and access the storage unauthenticated (this is only
possible under a few microsecond on a relatively fast machine, since
it's dropped just after it's checked).
3. Thread safe? I have no experience with threads, am i doing something
completely wrong?
4. I am modifying the ServerStub class directly at the moment, could
this be solved in a different way?

Attaching secure.py (in which the classes are) and myserver.py/myclient.py which is a small set of
tests.

(cc: me eventual replies, since i'm not on the list (yet)(
--
Johan Dahlin

--=-jVffnJ4Qa0jsF1qhwbLp
Content-Disposition: attachment; filename=myclient.py
Content-Transfer-Encoding: quoted-printable
Content-Type: text/x-python; name=myclient.py; charset=ISO-8859-1

import os
import signal

import ZODB

from ZODB import DB
from secure import SecureClientStorage
from ZEO.ClientStorage import ClientStorage

def open_db (pwd=3D'p4ssw0rd'):
    storage =3D SecureClientStorage(('nachocano', 4000),
                                  username=3D'mrjoe',
                                  password=3Dpwd)
    try:
        database =3D DB (storage)
    except ClientStorage.ClientDisconnected:
        print 'Could not connect to database, is it running?'
        os.kill (os.getpid(), signal.SIGABRT)
       =20
    return database

if len(sys.argv) > 1:
    database =3D open_db (sys.argv[1])
else:
    database =3D open_db ()


conn =3D database.open ()
root =3D conn.root ()
print root
#get_transaction().commit()

--=-jVffnJ4Qa0jsF1qhwbLp
Content-Disposition: attachment; filename=myserver.py
Content-Transfer-Encoding: quoted-printable
Content-Type: text/x-python; name=myserver.py; charset=ISO-8859-1

import asyncore
from signal import signal, SIGTERM, SIGINT, SIGHUP

import ZODB.FileStorage
def shutdown (storages):
    import asyncore

    # Do this twice, in case we got some more connections
    # while going through the loop.  This is really sort of
    # unnecessary, since we now use so_reuseaddr.
    for ignored in 1,2:
        for socket in asyncore.socket_map.values():
            try: socket.close()
            except: pass

    for storage in storages.values():
        try: storage.close()
        finally: pass

    raise SystemExit
   =20
storages =3D {}
storages['1'] =3D ZODB.FileStorage.FileStorage('db/Data.fs')

signal (SIGINT,
        lambda g,f,s=3Dstorages: shutdown(s))

# hostname to listen to, port
unix =3D ('nachocano', 4000)

from secure import SecureStorageServer

serv =3D SecureStorageServer (unix, storages)
serv.add_user ('mrjoe', 'p4ssw0rd')

print serv

asyncore.loop ()

--=-jVffnJ4Qa0jsF1qhwbLp
Content-Disposition: attachment; filename=secure.py
Content-Transfer-Encoding: quoted-printable
Content-Type: text/x-python; name=secure.py; charset=ISO-8859-1

import os

from zLOG                import INFO, LOG
from ZEO                 import ServerStub
from ZEO.zrpc.connection import ManagedServerConnection
from ZEO.ClientStorage   import ClientStorage
from ZEO.StorageServer   import StorageServer, ZEOStorage
from ZODB                import POSException
   =20
def authUser(self, storage_id, username, password):
    return self.rpc.call('authUser', storage_id, username, password)
ServerStub.StorageServer.authUser =3D authUser

class AuthError(Exception): pass

class SecureZEOStorage(ZEOStorage):
    __super_register =3D ZEOStorage.register
    auth =3D {}
    def authUser(self, storage_id, username, password):
        users =3D self.server.users

        if not users.has_key(username):
            return 0
        elif users[username] !=3D password:
            return 0
       =20
        self.auth[storage_id] =3D None=20
        return 1

    def register(self, storage_id, read_only):
        auth =3D self.auth
        if not auth.has_key(storage_id):
            raise AuthError, 'must authenticate'
        del auth[storage_id]

        self.__super_register(storage_id, read_only)
       =20
class SecureStorageServer(StorageServer):
    users =3D {}
    def add_user(self, username, password=3D''):
        self.users[username] =3D password
       =20
    def new_connection(self, sock, addr):
        c =3D ManagedServerConnection(sock, addr,
                                    SecureZEOStorage(self), self)
        LOG('SecureServerStorage', INFO,
            "new connection %s: %s" % (addr, `c`))
        return c

class SecureClientStorage(ClientStorage):
    __super_init =3D ClientStorage.__init__
    def __init__(self, *args, **kwargs):
        self._username =3D kwargs.get('username', '')
        self._password =3D kwargs.get('password', '')
        try:
            del kwargs['username']
            del kwargs['password']=20
        except KeyError: pass
        self.__super_init(*args, **kwargs)
       =20
    def testConnection(self, conn):
        LOG("ClientStorage", INFO, "Testing connection %r" % conn)
        stub =3D ServerStub.StorageServer(conn)
        storage_id =3D str(self._storage)
        retval =3D stub.authUser(storage_id,
                               self._username,
                               self._password)
        if not retval:
            raise AuthError
       =20
        try:
            stub.register(storage_id, self._is_read_only)
            return 1
        except POSException.ReadOnlyError:
            if not self._read_only_fallback:
                raise
            LOG("ClientStorage", INFO,
                "Got ReadOnlyError; trying again with read_only=3D1")
            stub.register(storage_id, read_only=3D1)
            return 0

--=-jVffnJ4Qa0jsF1qhwbLp--