[Zope-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@zope.com
Fri, 30 May 2003 15:21:28 -0400
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))