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.
#
# This software is subject to the provisions of the Zope Public License,
......@@ -29,7 +29,8 @@ import types
from ZEO import ClientCache, ServerStub
from ZEO.TransactionBuffer import TransactionBuffer
from ZEO.Exceptions \
import ClientStorageError, UnrecognizedResult, ClientDisconnected
import ClientStorageError, UnrecognizedResult, ClientDisconnected, \
AuthError
from ZEO.zrpc.client import ConnectionManager
from ZODB import POSException
......@@ -99,7 +100,8 @@ class ClientStorage:
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=''):
"""ClientStorage constructor.
......@@ -159,6 +161,17 @@ class ClientStorage:
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 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" %
......@@ -217,6 +230,8 @@ class ClientStorage:
self._conn_is_read_only = 0
self._storage = storage
self._read_only_fallback = read_only_fallback
self._username = username
self._password = password
# _server_addr is used by sortKey()
self._server_addr = None
self._tfile = None
......@@ -347,6 +362,34 @@ class ClientStorage:
if cn is not None:
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):
"""Internal: test the given connection.
......@@ -372,6 +415,12 @@ class ClientStorage:
# XXX Check the protocol version here?
self._conn_is_read_only = 0
stub = self.StorageServerStubClass(conn)
# XXX: Verify return value
auth = stub.getAuthProtocol()
if auth and not self.doAuth(auth, stub):
raise AuthError, "Authentication failed"
try:
stub.register(str(self._storage), self._is_read_only)
return 1
......
......@@ -24,3 +24,5 @@ class UnrecognizedResult(ClientStorageError):
class ClientDisconnected(ClientStorageError):
"""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.
#
# This software is subject to the provisions of the Zope Public License,
......@@ -45,6 +45,9 @@ class StorageServer:
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')
......
##############################################################################
#
# 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,
......@@ -31,6 +31,7 @@ import time
from ZEO import ClientStub
from ZEO.CommitLog import CommitLog
from ZEO.auth.database import Database
from ZEO.monitor import StorageStats, StatsServer
from ZEO.zrpc.server import Dispatcher
from ZEO.zrpc.connection import ManagedServerConnection, Delay, MTDelay
......@@ -161,6 +162,8 @@ class ZEOStorage:
"""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.storage is not None:
self.log("duplicate register() call")
......@@ -410,6 +413,15 @@ class ZEOStorage:
else:
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):
self._check_tid(id, exc=StorageTransactionError)
if self.locked:
......@@ -577,7 +589,9 @@ class StorageServer:
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):
"""StorageServer constructor.
This is typically invoked from the start.py script.
......@@ -618,7 +632,22 @@ class StorageServer:
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 "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
......@@ -633,6 +662,10 @@ class StorageServer:
for s in storages.values():
s._waiting = []
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
self.invq = []
self.invq_bound = invalidation_queue_size
......@@ -654,7 +687,43 @@ class StorageServer:
self.monitor = StatsServer(monitor_address, self.stats)
else:
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):
"""Internal: factory to create a new connection.
......@@ -663,6 +732,8 @@ class StorageServer:
connection.
"""
z = self.ZEOStorageClass(self, self.read_only)
if self.auth_protocol:
z.set_database(self.database)
c = self.ManagedServerConnectionClass(sock, addr, z, self)
log("new connection %s: %s" % (addr, `c`))
return c
......
#!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,9 @@ class ZEOOptionsMixin:
"t:", "timeout=", float)
self.add("monitor_address", "zeo.monitor_address", "m:", "monitor=",
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):
......@@ -189,7 +191,9 @@ class ZEOServer:
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_filename=self.options.auth_filename)
def loop_forever(self):
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