Commit 7e1fe222 authored by John Dahlin's avatar John Dahlin

Initial checkin of AuthZEO (without SRP)

parent 7e7f38b2
############################################################################## ##############################################################################
# #
# Copyright (c) 2001, 2002 Zope Corporation and Contributors. # Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -29,7 +29,8 @@ import types ...@@ -29,7 +29,8 @@ import types
from ZEO import ClientCache, ServerStub from ZEO import ClientCache, ServerStub
from ZEO.TransactionBuffer import TransactionBuffer from ZEO.TransactionBuffer import TransactionBuffer
from ZEO.Exceptions \ from ZEO.Exceptions \
import ClientStorageError, UnrecognizedResult, ClientDisconnected import ClientStorageError, UnrecognizedResult, ClientDisconnected, \
AuthError
from ZEO.zrpc.client import ConnectionManager from ZEO.zrpc.client import ConnectionManager
from ZODB import POSException from ZODB import POSException
...@@ -99,7 +100,8 @@ class ClientStorage: ...@@ -99,7 +100,8 @@ class ClientStorage:
min_disconnect_poll=5, max_disconnect_poll=300, min_disconnect_poll=5, max_disconnect_poll=300,
wait_for_server_on_startup=None, # deprecated alias for wait wait_for_server_on_startup=None, # deprecated alias for wait
wait=None, # defaults to 1 wait=None, # defaults to 1
read_only=0, read_only_fallback=0): read_only=0, read_only_fallback=0,
username='', password=''):
"""ClientStorage constructor. """ClientStorage constructor.
...@@ -159,6 +161,17 @@ class ClientStorage: ...@@ -159,6 +161,17 @@ class ClientStorage:
writable storages are available. Defaults to false. At writable storages are available. Defaults to false. At
most one of read_only and read_only_fallback should be most one of read_only and read_only_fallback should be
true. 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 scheme 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" % log2(INFO, "%s (pid=%d) created %s/%s for storage: %r" %
...@@ -217,6 +230,8 @@ class ClientStorage: ...@@ -217,6 +230,8 @@ class ClientStorage:
self._conn_is_read_only = 0 self._conn_is_read_only = 0
self._storage = storage self._storage = storage
self._read_only_fallback = read_only_fallback self._read_only_fallback = read_only_fallback
self._username = username
self._password = password
# _server_addr is used by sortKey() # _server_addr is used by sortKey()
self._server_addr = None self._server_addr = None
self._tfile = None self._tfile = None
...@@ -347,6 +362,34 @@ class ClientStorage: ...@@ -347,6 +362,34 @@ class ClientStorage:
if cn is not None: if cn is not None:
cn.pending() cn.pending()
def doAuth(self, protocol, stub):
if self._username == '' and self._password == '':
raise AuthError, "empty username or password"
# import the auth module
# XXX: Should we validate the client module that is being specified
# by the server? A malicious server could cause any auth_*.py file
# to be loaded according to Python import semantics.
fullname = 'ZEO.auth.auth_' + protocol
try:
module = __import__(fullname, globals(), locals(), protocol)
except ImportError:
log("%s: no such an auth protocol: %s" %
(self.__class__.__name__, protocol))
# And setup ZEOStorageClass
Client = getattr(module, 'Client', None)
if not Client:
log("%s: %s is not a valid auth protocol, must have a " + \
"Client class" % (self.__class__.__name__, protocol))
raise AuthError, "invalid protocol"
c = Client(stub)
# Initiate authentication, return boolean specifying whether OK or not
return c.start(self._username, self._password)
def testConnection(self, conn): def testConnection(self, conn):
"""Internal: test the given connection. """Internal: test the given connection.
...@@ -372,6 +415,12 @@ class ClientStorage: ...@@ -372,6 +415,12 @@ class ClientStorage:
# XXX Check the protocol version here? # XXX Check the protocol version here?
self._conn_is_read_only = 0 self._conn_is_read_only = 0
stub = self.StorageServerStubClass(conn) stub = self.StorageServerStubClass(conn)
# XXX: Verify return value
auth = stub.getAuthProtocol()
if auth and not self.doAuth(auth, stub):
raise AuthError, "Authentication failed"
try: try:
stub.register(str(self._storage), self._is_read_only) stub.register(str(self._storage), self._is_read_only)
return 1 return 1
......
...@@ -24,3 +24,5 @@ class UnrecognizedResult(ClientStorageError): ...@@ -24,3 +24,5 @@ class UnrecognizedResult(ClientStorageError):
class ClientDisconnected(ClientStorageError): class ClientDisconnected(ClientStorageError):
"""The database storage is disconnected from the storage.""" """The database storage is disconnected from the storage."""
class AuthError(StorageError):
"""The client provided invalid authentication credentials."""
############################################################################## ##############################################################################
# #
# Copyright (c) 2001, 2002 Zope Corporation and Contributors. # Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -45,6 +45,9 @@ class StorageServer: ...@@ -45,6 +45,9 @@ class StorageServer:
def get_info(self): def get_info(self):
return self.rpc.call('get_info') return self.rpc.call('get_info')
def getAuthProtocol(self):
return self.rpc.call('getAuthProtocol')
def lastTransaction(self): def lastTransaction(self):
# Not in protocol version 2.0.0; see __init__() # Not in protocol version 2.0.0; see __init__()
return self.rpc.call('lastTransaction') return self.rpc.call('lastTransaction')
......
############################################################################## ##############################################################################
# #
# Copyright (c) 2001, 2002 Zope Corporation and Contributors. # Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -31,6 +31,7 @@ import time ...@@ -31,6 +31,7 @@ import time
from ZEO import ClientStub from ZEO import ClientStub
from ZEO.CommitLog import CommitLog from ZEO.CommitLog import CommitLog
from ZEO.auth.database import Database
from ZEO.monitor import StorageStats, StatsServer from ZEO.monitor import StorageStats, StatsServer
from ZEO.zrpc.server import Dispatcher from ZEO.zrpc.server import Dispatcher
from ZEO.zrpc.connection import ManagedServerConnection, Delay, MTDelay from ZEO.zrpc.connection import ManagedServerConnection, Delay, MTDelay
...@@ -161,6 +162,8 @@ class ZEOStorage: ...@@ -161,6 +162,8 @@ class ZEOStorage:
"""Select the storage that this client will use """Select the storage that this client will use
This method must be the first one called by the client. 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.storage is not None: if self.storage is not None:
self.log("duplicate register() call") self.log("duplicate register() call")
...@@ -410,6 +413,15 @@ class ZEOStorage: ...@@ -410,6 +413,15 @@ class ZEOStorage:
else: else:
return self._wait(lambda: self._vote()) return self._wait(lambda: self._vote())
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 abortVersion(self, src, id): def abortVersion(self, src, id):
self._check_tid(id, exc=StorageTransactionError) self._check_tid(id, exc=StorageTransactionError)
if self.locked: if self.locked:
...@@ -577,7 +589,9 @@ class StorageServer: ...@@ -577,7 +589,9 @@ class StorageServer:
def __init__(self, addr, storages, read_only=0, def __init__(self, addr, storages, read_only=0,
invalidation_queue_size=100, invalidation_queue_size=100,
transaction_timeout=None, transaction_timeout=None,
monitor_address=None): monitor_address=None,
auth_protocol=None,
auth_filename=None):
"""StorageServer constructor. """StorageServer constructor.
This is typically invoked from the start.py script. This is typically invoked from the start.py script.
...@@ -619,6 +633,21 @@ class StorageServer: ...@@ -619,6 +633,21 @@ class StorageServer:
should listen. If specified, a monitor server is started. should listen. If specified, a monitor server is started.
The monitor server provides server statistics in a simple 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 "plaintext", "sha" 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 self.addr = addr
...@@ -633,6 +662,10 @@ class StorageServer: ...@@ -633,6 +662,10 @@ class StorageServer:
for s in storages.values(): for s in storages.values():
s._waiting = [] s._waiting = []
self.read_only = read_only self.read_only = read_only
self.auth_protocol = auth_protocol
self.auth_filename = auth_filename
if auth_protocol:
self._setup_auth(auth_protocol)
# A list of at most invalidation_queue_size invalidations # A list of at most invalidation_queue_size invalidations
self.invq = [] self.invq = []
self.invq_bound = invalidation_queue_size self.invq_bound = invalidation_queue_size
...@@ -655,6 +688,42 @@ class StorageServer: ...@@ -655,6 +688,42 @@ class StorageServer:
else: else:
self.monitor = None self.monitor = None
def _setup_auth(self, protocol):
# Load the auth protocol
fullname = 'ZEO.auth.auth_' + protocol
try:
module = __import__(fullname, globals(), locals(), protocol)
except ImportError:
log("%s: no such an auth protocol: %s" %
(self.__class__.__name__, protocol))
self.auth_protocol = None
return
from ZEO.auth.storage import AuthZEOStorage
# And set up ZEOStorageClass
klass = getattr(module, 'StorageClass', None)
if not klass or not issubclass(klass, AuthZEOStorage):
log(("%s: %s is not a valid auth protocol, must have a " + \
"StorageClass class") % (self.__class__.__name__, protocol))
self.auth_protocol = None
return
self.ZEOStorageClass = klass
log("%s: using auth protocol: %s" % \
(self.__class__.__name__, protocol))
dbklass = getattr(module, 'DatabaseClass', None)
if not dbklass:
dbklass = Database
# 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 = dbklass(self.auth_filename)
def new_connection(self, sock, addr): def new_connection(self, sock, addr):
"""Internal: factory to create a new connection. """Internal: factory to create a new connection.
...@@ -663,6 +732,8 @@ class StorageServer: ...@@ -663,6 +732,8 @@ class StorageServer:
connection. connection.
""" """
z = self.ZEOStorageClass(self, self.read_only) z = self.ZEOStorageClass(self, self.read_only)
if self.auth_protocol:
z.set_database(self.database)
c = self.ManagedServerConnectionClass(sock, addr, z, self) c = self.ManagedServerConnectionClass(sock, addr, z, self)
log("new connection %s: %s" % (addr, `c`)) log("new connection %s: %s" % (addr, `c`))
return c return c
......
#!python #!python
############################################################################## ##############################################################################
# #
# Copyright (c) 2001, 2002 Zope Corporation and Contributors. # Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -89,7 +89,9 @@ class ZEOOptionsMixin: ...@@ -89,7 +89,9 @@ class ZEOOptionsMixin:
"t:", "timeout=", float) "t:", "timeout=", float)
self.add("monitor_address", "zeo.monitor_address", "m:", "monitor=", self.add("monitor_address", "zeo.monitor_address", "m:", "monitor=",
self.handle_monitor_address) self.handle_monitor_address)
self.add('auth_protocol', 'zeo.auth_protocol', None,
'auth-protocol=', default=None)
self.add('auth_filename', 'zeo.auth_filename', None, 'auth-filename=')
class ZEOOptions(ZDOptions, ZEOOptionsMixin): class ZEOOptions(ZDOptions, ZEOOptionsMixin):
...@@ -189,7 +191,9 @@ class ZEOServer: ...@@ -189,7 +191,9 @@ class ZEOServer:
read_only=self.options.read_only, read_only=self.options.read_only,
invalidation_queue_size=self.options.invalidation_queue_size, invalidation_queue_size=self.options.invalidation_queue_size,
transaction_timeout=self.options.transaction_timeout, 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_filename=self.options.auth_filename)
def loop_forever(self): def loop_forever(self):
import ThreadedAsync.LoopCallback import ThreadedAsync.LoopCallback
......
##############################################################################
#
# Copyright (c) 2001, 2002 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
#
##############################################################################
"""Test suite for AuthZEO."""
import glob
import os
import time
import unittest
from ThreadedAsync import LoopCallback
from ZEO.auth.database import Database
#from ZEO.auth.auth_srp import SRPDatabase
from ZEO.ClientStorage import ClientStorage
from ZEO.StorageServer import StorageServer
from ZODB.FileStorage import FileStorage
storage = FileStorage('auth-test.fs')
SOCKET='auth-test-socket'
STORAGES={'1': storage}
class BaseTest(unittest.TestCase):
def createDB(self, name):
if os.path.exists(name):
os.remove(self.database)
if name.endswith('srp'):
db = SRPDatabase(name)
else:
db = Database(name)
db.add_user('foo', 'bar')
db.save()
def setUp(self):
self.createDB(self.database)
self.pid = os.fork()
if not self.pid:
self.server = StorageServer(SOCKET, STORAGES,
auth_protocol=self.protocol,
auth_filename=self.database)
LoopCallback.loop()
def tearDown(self):
os.kill(self.pid, 9)
os.remove(self.database)
os.remove(SOCKET)
for file in glob.glob('auth-test.fs*'):
os.remove(file)
def check(self):
# Sleep for 0.2 seconds to give the server some time to start up
# seems to be needed before and after creating the storage
time.sleep(0.2)
cs = ClientStorage(SOCKET, wait=0, username='foo', password='bar')
time.sleep(0.2)
if cs._connection == None:
raise AssertionError, \
"authentication for %s failed" % self.protocol
cs._connection.poll()
if not cs.is_connected():
raise AssertionError, \
"authentication for %s failed" % self.protocol
class PlainTextAuth(BaseTest):
protocol = 'plaintext'
database = 'authdb.sha'
class SHAAuth(BaseTest):
protocol = 'sha'
database = 'authdb.sha'
#class SRPAuth(BaseTest):
# protocol = 'srp'
# database = 'authdb.srp'
test_classes = [PlainTextAuth, SHAAuth] # SRPAuth
def test_suite():
suite = unittest.TestSuite()
for klass in test_classes:
sub = unittest.makeSuite(klass, 'check')
suite.addTest(sub)
return suite
if __name__ == "__main__":
unittest.main(defaultTest='test_suite')
##############################################################################
#
# 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
#
##############################################################################
"""Usage:
zpasswd [-cd] passwordfile username
zpasswd -b[cd] passwordfile username password
zpasswd -n[d] username
zpasswd -nb[d] username password
-c Create a new file.
-d Delete user
-n Don't update file; display results on stdout.
-b Use the password from the command line rather than prompting for it."""
import sys
import getopt
import getpass
from ZEO.auth.database import Database
#from ZEO.auth.srp import SRPDatabase
try:
opts, args = getopt.getopt(sys.argv[1:], 'cdnbs')
except getopt.GetoptError:
# print help information and exit:
print __doc__
sys.exit(2)
stdout = 0
create = 0
delete = 0
prompt = 1
#srp = 0
for opt, arg in opts:
if opt in ("-h", "--help"):
print __doc__
sys.exit()
if opt == "-n":
stdout = 1
if opt == "-c":
create = 1
if opt == "-d":
delete = 1
if opt == "b":
prompt = 0
# if opt == "-s":
# srp = 1
if create and delete:
print "Can't create and delete at the same time"
sys.exit(3)
if len(args) < 2:
print __doc__
sys.exit()
output = args[0]
username = args[1]
if not delete:
if len(args) > 3:
print __doc__
sys.exit()
if prompt:
password = getpass.getpass('Enter passphrase: ')
else:
password = args[2]
#if srp:
# db = SRPDatabase(output)
#else:
db = Database(output)
if create:
try:
db.add_user(username, password)
except LookupError:
print 'The username already exists'
sys.exit(4)
if stdout:
db.save(fd=sys.stdout)
else:
db.save()
if delete:
try:
db.del_user(username)
except LockupError:
print 'The username doesn\'t exist'
sys.exit(5)
if stdout:
db.save(fd=sys.stdout)
else:
db.save()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment