[Zodb-checkins] CVS: ZODB3/ZEO - zeopasswd.py:1.2 runzeo.py:1.15 component.xml:1.4 StorageServer.py:1.97 ServerStub.py:1.16 Exceptions.py:1.9 ClientStorage.py:1.100

Jeremy Hylton jeremy at zope.com
Fri May 30 16:21:28 EDT 2003


Update of /cvs-repository/ZODB3/ZEO
In directory cvs.zope.org:/tmp/cvs-serv25334/ZEO

Modified Files:
	runzeo.py component.xml StorageServer.py ServerStub.py 
	Exceptions.py ClientStorage.py 
Added Files:
	zeopasswd.py 
Log Message:
Merge ZODB3-auth-branch and bump a few version numbers.

After the merge, I made several Python 2.1 compatibility changes for
the auth code.


=== ZODB3/ZEO/zeopasswd.py 1.1 => 1.2 ===
--- /dev/null	Fri May 30 15:21:28 2003
+++ ZODB3/ZEO/zeopasswd.py	Fri May 30 15:20:57 2003
@@ -0,0 +1,95 @@
+#!python
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Update a user's authentication tokens for a ZEO server.
+
+usage: python zeopasswd.py [options] username [password]
+
+-C/--configuration URL -- configuration file or URL
+-d/--delete -- delete user instead of updating password
+"""
+
+import getopt
+import getpass
+import sys
+
+import ZConfig
+import ZEO
+
+def usage(msg):
+    print msg
+    print __doc__
+    sys.exit(2)
+
+def options(args):
+    """Password-specific options loaded from regular ZEO config file."""
+
+    schema = ZConfig.loadSchema(os.path.join(os.path.dirname(ZEO.__file__),
+                                             "schema.xml"))
+
+    try:
+        options, args = getopt.getopt(args, "C:", ["configure="])
+    except getopt.error, msg:
+        usage(msg)
+    config = None
+    delete = False
+    for k, v in options:
+        if k == '-C' or k == '--configure':
+            config, nil = ZConfig.loadConfig(schema, v)
+        if k == '-d' or k == '--delete':
+            delete = True
+    if config is None:
+        usage("Must specifiy configuration file")
+
+    password = None
+    if delete:
+        if not args:
+            usage("Must specify username to delete")
+        elif len(args) > 1:
+            usage("Too many arguments")
+        username = args[0]
+    else:
+        if not args:
+            usage("Must specify username")
+        elif len(args) > 2:
+            usage("Too many arguments")
+        elif len(args) == 1:
+            username = args[0]
+        else:
+            username, password = args
+        
+    return config.zeo, delete, username, password
+
+def main(args=None):
+    options, delete, username, password = options(args)
+    p = options.authentication_protocol  
+    if p is None:
+        usage("ZEO configuration does not specify authentication-protocol")
+    if p == "digest":
+        from ZEO.auth.auth_digest import DigestDatabase as Database
+    elif p == "srp":
+        from ZEO.auth.auth_srp import SRPDatabase as Database
+    if options.authentication_database is None:
+        usage("ZEO configuration does not specify authentication-database")
+    db = Database(options.authentication_database)
+    if delete:
+        db.del_user(username)
+    else:
+        if password is None:
+            password = getpass.getpass("Enter password: ")
+        db.add_user(username, password)
+    db.save()
+
+if __name__ == "__main__":
+    main(sys.argv)


=== ZODB3/ZEO/runzeo.py 1.14 => 1.15 ===
--- ZODB3/ZEO/runzeo.py:1.14	Wed Apr 30 13:14:33 2003
+++ ZODB3/ZEO/runzeo.py	Fri May 30 15:20:57 2003
@@ -1,7 +1,7 @@
 #!python
 ##############################################################################
 #
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -89,7 +89,12 @@
                  "t:", "timeout=", float)
         self.add("monitor_address", "zeo.monitor_address", "m:", "monitor=",
                  self.handle_monitor_address)
-
+        self.add('auth_protocol', 'zeo.authentication_protocol',
+                 None, 'auth-protocol=', default=None)
+        self.add('auth_database', 'zeo.authentication_database',
+                 None, 'auth-database=')
+        self.add('auth_realm', 'zeo.authentication_realm',
+                 None, 'auth-realm=')
 
 class ZEOOptions(ZDOptions, ZEOOptionsMixin):
 
@@ -189,7 +194,10 @@
             read_only=self.options.read_only,
             invalidation_queue_size=self.options.invalidation_queue_size,
             transaction_timeout=self.options.transaction_timeout,
-            monitor_address=self.options.monitor_address)
+            monitor_address=self.options.monitor_address,
+            auth_protocol=self.options.auth_protocol,
+            auth_database=self.options.auth_database,
+            auth_realm=self.options.auth_realm)
 
     def loop_forever(self):
         import ThreadedAsync.LoopCallback


=== ZODB3/ZEO/component.xml 1.3 => 1.4 ===
--- ZODB3/ZEO/component.xml:1.3	Mon Jan 20 17:09:46 2003
+++ ZODB3/ZEO/component.xml	Fri May 30 15:20:57 2003
@@ -3,7 +3,7 @@
   <sectiontype name="zeo">
 
     <description>
-      The content of a "ZEO" section describe operational parameters
+      The content of a ZEO section describe operational parameters
       of a ZEO server except for the storage(s) to be served.
     </description>
 
@@ -68,6 +68,28 @@
         after acquiring the storage lock, specified in seconds.  If the
         transaction takes too long, the client connection will be closed
         and the transaction aborted.
+      </description>
+    </key>
+
+    <key name="authentication-protocol" required="no">
+      <description>
+        The name of the protocol used for authentication.  The
+        only protocol provided with ZEO is "digest," but extensions
+        may provide other protocols.
+      </description>
+    </key>
+
+    <key name="authentication-database" required="no">
+      <description>
+        The path of the database containing authentication credentials.
+      </description>
+    </key>
+
+    <key name="authentication-realm" required="no">
+      <description>
+        The authentication realm of the server.  Some authentication
+        schemes use a realm to identify the logic set of usernames
+        that are accepted by this server.
       </description>
     </key>
 


=== ZODB3/ZEO/StorageServer.py 1.96 => 1.97 ===
--- ZODB3/ZEO/StorageServer.py:1.96	Fri May 30 14:17:10 2003
+++ ZODB3/ZEO/StorageServer.py	Fri May 30 15:20:57 2003
@@ -1,6 +1,6 @@
 ##############################################################################
 #
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -35,6 +35,7 @@
 from ZEO.zrpc.server import Dispatcher
 from ZEO.zrpc.connection import ManagedServerConnection, Delay, MTDelay
 from ZEO.zrpc.trigger import trigger
+from ZEO.Exceptions import AuthError
 
 import zLOG
 from ZODB.ConflictResolution import ResolvedSerial
@@ -62,10 +63,13 @@
     """Proxy to underlying storage for a single remote client."""
 
     # Classes we instantiate.  A subclass might override.
-
     ClientStorageStubClass = ClientStub.ClientStorage
 
-    def __init__(self, server, read_only=0):
+    # A list of extension methods.  A subclass with extra methods
+    # should override.
+    extensions = []
+
+    def __init__(self, server, read_only=0, auth_realm=None):
         self.server = server
         # timeout and stats will be initialized in register()
         self.timeout = None
@@ -79,7 +83,22 @@
         self.locked = 0
         self.verifying = 0
         self.log_label = _label
+        self.authenticated = 0
+        self.auth_realm = auth_realm
+        # The authentication protocol may define extra methods.
+        self._extensions = {}
+        for func in self.extensions:
+            self._extensions[func.func_name] = None
+        
+    def finish_auth(self, authenticated):
+        if not self.auth_realm:
+            return 1
+        self.authenticated = authenticated
+        return authenticated
 
+    def set_database(self, database):
+        self.database = database
+        
     def notifyConnected(self, conn):
         self.connection = conn # For restart_other() below
         self.client = self.ClientStorageStubClass(conn)
@@ -133,9 +152,11 @@
             # can be removed
             pass
         else:
-            for name in fn().keys():
-                if not hasattr(self,name):
-                    setattr(self, name, getattr(self.storage, name))
+            d = fn()
+            self._extensions.update(d)
+            for name in d.keys():
+                assert not hasattr(self, name)
+                setattr(self, name, getattr(self.storage, name))
         self.lastTransaction = self.storage.lastTransaction
 
     def _check_tid(self, tid, exc=None):
@@ -159,11 +180,25 @@
                 return 0
         return 1
 
+    def getAuthProtocol(self):
+        """Return string specifying name of authentication module to use.
+
+        The module name should be auth_%s where %s is auth_protocol."""
+        protocol = self.server.auth_protocol
+        if not protocol or protocol == 'none':
+            return None
+        return protocol
+    
     def register(self, storage_id, read_only):
         """Select the storage that this client will use
 
         This method must be the first one called by the client.
+        For authenticated storages this method will be called by the client
+        immediately after authentication is finished.
         """
+        if self.auth_realm and not self.authenticated:
+            raise AuthError, "Client was never authenticated with server!"
+
         if self.storage is not None:
             self.log("duplicate register() call")
             raise ValueError, "duplicate register() call"
@@ -199,12 +234,7 @@
                 }
 
     def getExtensionMethods(self):
-        try:
-            e = self.storage.getExtensionMethods
-        except AttributeError:
-            return {}
-        else:
-            return e()
+        return self._extensions
 
     def zeoLoad(self, oid):
         self.stats.loads += 1
@@ -579,7 +609,10 @@
     def __init__(self, addr, storages, read_only=0,
                  invalidation_queue_size=100,
                  transaction_timeout=None,
-                 monitor_address=None):
+                 monitor_address=None,
+                 auth_protocol=None,
+                 auth_filename=None,
+                 auth_realm=None):
         """StorageServer constructor.
 
         This is typically invoked from the start.py script.
@@ -620,7 +653,22 @@
         monitor_address -- The address at which the monitor server
             should listen.  If specified, a monitor server is started.
             The monitor server provides server statistics in a simple
-            text format. 
+            text format.
+
+        auth_protocol -- The name of the authentication protocol to use.
+            Examples are "digest" and "srp".
+            
+        auth_filename -- The name of the password database filename.
+            It should be in a format compatible with the authentication
+            protocol used; for instance, "sha" and "srp" require different
+            formats.
+            
+            Note that to implement an authentication protocol, a server
+            and client authentication mechanism must be implemented in a
+            auth_* module, which should be stored inside the "auth"
+            subdirectory. This module may also define a DatabaseClass
+            variable that should indicate what database should be used
+            by the authenticator.
         """
 
         self.addr = addr
@@ -635,6 +683,12 @@
         for s in storages.values():
             s._waiting = []
         self.read_only = read_only
+        self.auth_protocol = auth_protocol
+        self.auth_filename = auth_filename
+        self.auth_realm = auth_realm
+        self.database = None
+        if auth_protocol:
+            self._setup_auth(auth_protocol)
         # A list of at most invalidation_queue_size invalidations
         self.invq = []
         self.invq_bound = invalidation_queue_size
@@ -656,7 +710,41 @@
             self.monitor = StatsServer(monitor_address, self.stats)
         else:
             self.monitor = None
+            
+    def _setup_auth(self, protocol):
+        # Can't be done in global scope, because of cyclic references
+        from ZEO.auth import get_module
+
+        name = self.__class__.__name__
+
+        module = get_module(protocol)
+        if not module:
+            log("%s: no such an auth protocol: %s" % (name, protocol))
+            return
+        
+        storage_class, client, db_class = module
+        
+        if not storage_class or not issubclass(storage_class, ZEOStorage):
+            log(("%s: %s isn't a valid protocol, must have a StorageClass" %
+                 (name, protocol)))
+            self.auth_protocol = None
+            return
+        self.ZEOStorageClass = storage_class
+
+        log("%s: using auth protocol: %s" % (name, protocol))
+        
+        # We create a Database instance here for use with the authenticator
+        # modules. Having one instance allows it to be shared between multiple
+        # storages, avoiding the need to bloat each with a new authenticator
+        # Database that would contain the same info, and also avoiding any
+        # possibly synchronization issues between them.
+        self.database = db_class(self.auth_filename)
+        if self.database.realm != self.auth_realm:
+            raise ValueError("password database realm %r "
+                             "does not match storage realm %r"
+                             % (self.database.realm, self.auth_realm))
 
+        
     def new_connection(self, sock, addr):
         """Internal: factory to create a new connection.
 
@@ -664,8 +752,14 @@
         whenever accept() returns a socket for a new incoming
         connection.
         """
-        z = self.ZEOStorageClass(self, self.read_only)
-        c = self.ManagedServerConnectionClass(sock, addr, z, self)
+        if self.auth_protocol and self.database:
+            zstorage = self.ZEOStorageClass(self, self.read_only,
+                                            auth_realm=self.auth_realm)
+            zstorage.set_database(self.database)
+        else:
+            zstorage = self.ZEOStorageClass(self, self.read_only)
+            
+        c = self.ManagedServerConnectionClass(sock, addr, zstorage, self)
         log("new connection %s: %s" % (addr, `c`))
         return c
 


=== ZODB3/ZEO/ServerStub.py 1.15 => 1.16 ===
--- ZODB3/ZEO/ServerStub.py:1.15	Fri May 30 14:17:10 2003
+++ ZODB3/ZEO/ServerStub.py	Fri May 30 15:20:57 2003
@@ -1,6 +1,6 @@
 ##############################################################################
 #
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -45,6 +45,9 @@
     def get_info(self):
         return self.rpc.call('get_info')
 
+    def getAuthProtocol(self):
+        return self.rpc.call('getAuthProtocol')
+    
     def lastTransaction(self):
         # Not in protocol version 2.0.0; see __init__()
         return self.rpc.call('lastTransaction')
@@ -147,5 +150,6 @@
     def __init__(self, rpc, name):
         self.rpc = rpc
         self.name = name
+        
     def call(self, *a, **kwa):
-        return apply(self.rpc.call, (self.name,)+a, kwa)
+        return self.rpc.call(self.name, *a, **kwa)


=== ZODB3/ZEO/Exceptions.py 1.8 => 1.9 ===
--- ZODB3/ZEO/Exceptions.py:1.8	Wed Apr 30 13:14:33 2003
+++ ZODB3/ZEO/Exceptions.py	Fri May 30 15:20:57 2003
@@ -24,3 +24,5 @@
 class ClientDisconnected(ClientStorageError):
     """The database storage is disconnected from the storage."""
 
+class AuthError(StorageError):
+    """The client provided invalid authentication credentials."""


=== ZODB3/ZEO/ClientStorage.py 1.99 => 1.100 ===
--- ZODB3/ZEO/ClientStorage.py:1.99	Fri May 30 14:17:10 2003
+++ ZODB3/ZEO/ClientStorage.py	Fri May 30 15:20:57 2003
@@ -1,6 +1,6 @@
 ##############################################################################
 #
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
 # All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -28,13 +28,14 @@
 
 from ZEO import ClientCache, ServerStub
 from ZEO.TransactionBuffer import TransactionBuffer
-from ZEO.Exceptions \
-     import ClientStorageError, UnrecognizedResult, ClientDisconnected
+from ZEO.Exceptions import ClientStorageError, UnrecognizedResult, \
+     ClientDisconnected, AuthError
+from ZEO.auth import get_module
 from ZEO.zrpc.client import ConnectionManager
 
 from ZODB import POSException
 from ZODB.TimeStamp import TimeStamp
-from zLOG import LOG, PROBLEM, INFO, BLATHER
+from zLOG import LOG, PROBLEM, INFO, BLATHER, ERROR
 
 def log2(type, msg, subsys="ZCS:%d" % os.getpid()):
     LOG(subsys, type, msg)
@@ -99,8 +100,8 @@
                  min_disconnect_poll=5, max_disconnect_poll=300,
                  wait_for_server_on_startup=None, # deprecated alias for wait
                  wait=None, # defaults to 1
-                 read_only=0, read_only_fallback=0):
-
+                 read_only=0, read_only_fallback=0,
+                 username='', password='', realm=None):
         """ClientStorage constructor.
 
         This is typically invoked from a custom_zodb.py file.
@@ -159,6 +160,17 @@
             writable storages are available.  Defaults to false.  At
             most one of read_only and read_only_fallback should be
             true.
+
+        username -- string with username to be used when authenticating.
+            These only need to be provided if you are connecting to an
+            authenticated server storage.
+ 
+        password -- string with plaintext password to be used
+            when authenticated.
+
+        Note that the authentication protocol is defined by the server
+        and is detected by the ClientStorage upon connecting (see
+        testConnection() and doAuth() for details).
         """
 
         log2(INFO, "%s (pid=%d) created %s/%s for storage: %r" %
@@ -217,6 +229,9 @@
         self._conn_is_read_only = 0
         self._storage = storage
         self._read_only_fallback = read_only_fallback
+        self._username = username
+        self._password = password
+        self._realm = realm
         # _server_addr is used by sortKey()
         self._server_addr = None
         self._tfile = None
@@ -347,6 +362,29 @@
         if cn is not None:
             cn.pending()
 
+    def doAuth(self, protocol, stub):
+        if not (self._username and self._password):
+            raise AuthError, "empty username or password"
+
+        module = get_module(protocol)
+        if not module:
+            log2(PROBLEM, "%s: no such an auth protocol: %s" %
+                 (self.__class__.__name__, protocol))
+            return
+
+        storage_class, client, db_class = module
+
+        if not client:
+            log2(PROBLEM,
+                 "%s: %s isn't a valid protocol, must have a Client class" %
+                 (self.__class__.__name__, protocol))
+            raise AuthError, "invalid protocol"
+        
+        c = client(stub)
+        
+        # Initiate authentication, returns boolean specifying whether OK
+        return c.start(self._username, self._realm, self._password)
+        
     def testConnection(self, conn):
         """Internal: test the given connection.
 
@@ -372,6 +410,16 @@
         # XXX Check the protocol version here?
         self._conn_is_read_only = 0
         stub = self.StorageServerStubClass(conn)
+
+        auth = stub.getAuthProtocol()
+        log2(INFO, "Server authentication protocol %r" % auth)
+        if auth:
+            if self.doAuth(auth, stub):
+                log2(INFO, "Client authentication successful")
+            else:
+                log2(ERROR, "Authentication failed")
+                raise AuthError, "Authentication failed"
+        
         try:
             stub.register(str(self._storage), self._is_read_only)
             return 1
@@ -416,14 +464,14 @@
         stub = self.StorageServerStubClass(conn)
         self._oids = []
         self._info.update(stub.get_info())
-        self._handle_extensions()
         self.verify_cache(stub)
         if not conn.is_async():
             log2(INFO, "Waiting for cache verification to finish")
             self._wait_sync()
+        self._handle_extensions()
 
     def _handle_extensions(self):
-        for name in self.getExtensionMethods():
+        for name in self.getExtensionMethods().keys():
             if not hasattr(self, name):
                 setattr(self, name, self._server.extensionMethod(name))
 




More information about the Zodb-checkins mailing list