diff --git a/authserv/ldap_model.py b/authserv/ldap_model.py index 49c827fad1327b89d5030459ec333ceb5d0bf0d7..af6f7a2f19f2a8d29abf5b12753b9b0f6af79594 100644 --- a/authserv/ldap_model.py +++ b/authserv/ldap_model.py @@ -11,6 +11,13 @@ class Error(Exception): pass +def _expandvars(s, vars, quotefn): + if not s: + return s + qvars = dict((k, quotefn(str(v))) for k, v in vars.iteritems()) + return s % qvars + + class UserDb(model.UserDb): ldap_attrs = [ @@ -33,7 +40,7 @@ class UserDb(model.UserDb): yield c c.unbind_s() - def _query_user(self, username, service): + def _query_user(self, username, service, shard): # Allow referencing a service to another, by specifying a # string rather than a dictionary as the value. If you build # infinite loops this way, it's your fault. @@ -44,6 +51,11 @@ class UserDb(model.UserDb): logging.error('unknown service "%s"', service) return None + # Arguments used for variable substitution in the LDAP filters. + ldap_vars = {'user': username, + 'shard': shard, + 'service': service} + with self._conn() as c: # LDAP queries can be built in two ways: # @@ -56,12 +68,13 @@ class UserDb(model.UserDb): # 'filter' is required. # if 'dn' in ldap_params: - base = ldap_params['dn'].replace('%s', escape_dn_chars(username)) - filt = ldap_params.get('filter', '(objectClass=*)').replace('%s', escape_filter_chars(username)) + base = _expandvars(ldap_params['dn'], ldap_vars, escape_dn_chars) + filt = _expandvars(ldap_params.get('filter', '(objectClass=*)'), + ldap_vars, escape_filter_chars) scope = ldap.SCOPE_BASE else: - base = ldap_params['base'].replace('%s', escape_dn_chars(username)) - filt = ldap_params['filter'].replace('%s', escape_filter_chars(username)) + base = _expandvars(ldap_params['base'], ldap_vars, escape_dn_chars) + filt = _expandvars(ldap_params['filter'], ldap_vars, escape_filter_chars) scope = ldap.SCOPE_SUBTREE logging.debug('ldap search: base=%s, scope=%s, filt=%s', base, scope, filt) result = c.search_s(base, scope, filt, self.ldap_attrs) @@ -73,9 +86,9 @@ class UserDb(model.UserDb): return User(username, result[0][0], result[0][1]) - def get_user(self, username, service): + def get_user(self, username, service, shard): try: - return self._query_user(username, service) + return self._query_user(username, service, shard) except (Error, ldap.LDAPError), e: logging.error('userdb error: %s', e) return None diff --git a/authserv/model.py b/authserv/model.py index ed394f8106a5b795f881ffde673d372c1345e160..b5962038856da4a96fe0b5766b50736f1040d04f 100644 --- a/authserv/model.py +++ b/authserv/model.py @@ -5,7 +5,7 @@ class UserDb(object): __metaclass__ = abc.ABCMeta - def get_user(self, username, service): + def get_user(self, username, service, shard): pass diff --git a/authserv/server.py b/authserv/server.py index 353bb57d7b748294306157376791641ec7b5b6eb..b0fe69407696edd4d0551b69ae602d4b459fb461 100644 --- a/authserv/server.py +++ b/authserv/server.py @@ -12,8 +12,8 @@ from flask import Flask, request, abort, make_response @blacklist_on_auth_failure(key_from_args(0), count=5, period=600, ttl=43200) @blacklist_on_auth_failure(key_from_args(4), count=5, period=600, ttl=43200, check_wl=True) -def _auth(username, service, password, otp_token, source_ip): - user = app.userdb.get_user(username, service) +def _auth(username, service, shard, password, otp_token, source_ip): + user = app.userdb.get_user(username, service, shard) if not user: return protocol.ERR_AUTHENTICATION_FAILURE return auth.authenticate(user, service, password, otp_token) @@ -30,12 +30,16 @@ def do_auth(): password = request.form.get('password') otp_token = request.form.get('otp') source_ip = request.form.get('source_ip') + try: + shard = int(request.form.get('shard')) + except: + shard = -1 if not service or not username: abort(400) try: - result = _auth(username, service, password, otp_token, source_ip) + result = _auth(username, service, shard, password, otp_token, source_ip) except Exception, e: app.logger.exception('Unexpected exception in authenticate()') abort(500) diff --git a/authserv/test/__init__.py b/authserv/test/__init__.py index 8ef8f3674f631ac56a26fdb68d552fae1a3f0737..810ffe45ab85c83105051be8b3a492621d9ed56f 100644 --- a/authserv/test/__init__.py +++ b/authserv/test/__init__.py @@ -71,7 +71,7 @@ class FakeUserDb(model.UserDb): def __init__(self, users): self.users = users - def get_user(self, username, service): + def get_user(self, username, service, shard): return self.users.get(username) diff --git a/authserv/test/test_auth_ldap.py b/authserv/test/test_auth_ldap.py index 08fe57ef4bc3755997205ced10b3d27585d33895..e491b1843fc13cbb20a4e1bc5a522b518a2ab642 100644 --- a/authserv/test/test_auth_ldap.py +++ b/authserv/test/test_auth_ldap.py @@ -10,13 +10,17 @@ class LdapAuthTestBase(LdapTestBase): SERVICE_MAP = { 'mail': { 'base': 'ou=People,dc=investici,dc=org,o=Anarchy', - 'filter': '(&(status=active)(mail=%s))', - }, + 'filter': '(&(status=active)(mail=%(user)s))', + }, + 'sharded': { + 'base': 'ou=People,dc=investici,dc=org,o=Anarchy', + 'filter': '(&(status=active)(mail=%(user)s)(host=%(shard)s))', + }, 'account': { - 'dn': 'uid=%s,ou=People,dc=investici,dc=org,o=Anarchy', - }, + 'dn': 'uid=%(user)s,ou=People,dc=investici,dc=org,o=Anarchy', + }, 'aliased-service': 'account', - } + } def setUp(self): self.userdb = ldap_model.UserDb( @@ -34,25 +38,35 @@ class LdapAuthTest(LdapAuthTestBase): def test_userdb_get_user(self): self.assertTrue( - self.userdb.get_user('test@investici.org', 'account')) + self.userdb.get_user('test@investici.org', 'account', -1)) + self.assertTrue( + self.userdb.get_user('test@investici.org', 'account', 'whatever')) + + def test_userdb_get_user_sharded(self): + self.assertTrue( + self.userdb.get_user('test@investici.org', 'sharded', 'latitanza')) + self.assertFalse( + self.userdb.get_user('test@investici.org', 'sharded', 'contumacia')) + self.assertFalse( + self.userdb.get_user('test@investici.org', 'sharded', -1)) def test_userdb_unknown_service(self): self.assertFalse( - self.userdb.get_user('test@investici.org', 'unknownservice')) + self.userdb.get_user('test@investici.org', 'unknownservice', -1)) def test_userdb_service_alias(self): self.assertTrue( - self.userdb.get_user('test@investici.org', 'aliased-service')) + self.userdb.get_user('test@investici.org', 'aliased-service', -1)) def test_auth_password_ok(self): - u = self.userdb.get_user('test@investici.org', 'mail') + u = self.userdb.get_user('test@investici.org', 'mail', -1) self.assertTrue(u) self.assertEquals( protocol.OK, authenticate(u, 'mail', 'password', None)) def test_auth_password_fail(self): - u = self.userdb.get_user('test@investici.org', 'mail') + u = self.userdb.get_user('test@investici.org', 'mail', -1) self.assertTrue(u) self.assertEquals( protocol.ERR_AUTHENTICATION_FAILURE, @@ -66,21 +80,21 @@ class LdapOtpTest(LdapAuthTestBase): ] def test_auth_password_requires_otp(self): - u = self.userdb.get_user('test@investici.org', 'account') + u = self.userdb.get_user('test@investici.org', 'account', -1) self.assertTrue(u) self.assertEquals( protocol.ERR_OTP_REQUIRED, authenticate(u, 'account', 'password', None)) def test_auth_bad_password_requires_otp(self): - u = self.userdb.get_user('test@investici.org', 'account') + u = self.userdb.get_user('test@investici.org', 'account', -1) self.assertTrue(u) self.assertEquals( protocol.ERR_OTP_REQUIRED, authenticate(u, 'account', 'wrong password', None)) def test_auth_otp_ok(self): - u = self.userdb.get_user('test@investici.org', 'account') + u = self.userdb.get_user('test@investici.org', 'account', -1) self.assertTrue(u) secret= '089421' token = totp(secret, format='dec6', period=30) @@ -89,7 +103,7 @@ class LdapOtpTest(LdapAuthTestBase): authenticate(u, 'account', 'password', str(token))) def test_auth_otp_ok_bad_password(self): - u = self.userdb.get_user('test@investici.org', 'account') + u = self.userdb.get_user('test@investici.org', 'account', -1) self.assertTrue(u) secret= '089421' token = totp(secret, format='dec6', period=30) @@ -98,7 +112,7 @@ class LdapOtpTest(LdapAuthTestBase): authenticate(u, 'account', 'wrong password', str(token))) def test_auth_bad_otp(self): - u = self.userdb.get_user('test@investici.org', 'account') + u = self.userdb.get_user('test@investici.org', 'account', -1) self.assertTrue(u) self.assertEquals( protocol.ERR_AUTHENTICATION_FAILURE, @@ -112,21 +126,21 @@ class LdapASPTest(LdapAuthTestBase): ] def test_app_specific_password_ok(self): - u = self.userdb.get_user('test@investici.org', 'mail') + u = self.userdb.get_user('test@investici.org', 'mail', -1) self.assertTrue(u) self.assertEquals( protocol.OK, authenticate(u, 'mail', 'veryspecificpassword', None)) def test_plain_password_fails(self): - u = self.userdb.get_user('test@investici.org', 'mail') + u = self.userdb.get_user('test@investici.org', 'mail', -1) self.assertTrue(u) self.assertEquals( protocol.ERR_AUTHENTICATION_FAILURE, authenticate(u, 'mail', 'password', None)) def test_plain_password_and_otp_fails(self): - u = self.userdb.get_user('test@investici.org', 'mail') + u = self.userdb.get_user('test@investici.org', 'mail', -1) self.assertTrue(u) self.assertEquals( protocol.ERR_AUTHENTICATION_FAILURE, diff --git a/debian/ai-auth-server.conf b/debian/ai-auth-server.conf index 9603a079cca5dc2cc9a1001d078d6b0f5ffcf216..b04ca756fe484c694873968c5ca53748992b1e01 100644 --- a/debian/ai-auth-server.conf +++ b/debian/ai-auth-server.conf @@ -12,18 +12,18 @@ LDAP_SERVICE_MAP = { # Mail accounts (dovecot, nginx-mail-mapper). 'mail': { 'base': 'ou=People, dc=investici, dc=org, o=Anarchy', - 'filter': '(&(objectClass=virtualMailUser)(status=active)(mail=%s))', + 'filter': '(&(objectClass=virtualMailUser)(status=active)(mail=%(user)s))', }, # DAV access (webdav fcgi handler). 'dav': { 'base': 'ou=People, dc=investici, dc=org, o=Anarchy', - 'filter': '(&(objectClass=ftpAccount)(status=active)(host=%s)(ftpname=%%s))' % host, + 'filter': '(&(objectClass=ftpAccount)(status=active)(host=%(shard)s)(ftpname=%%(user)s))' % host, }, # Main account (pannello). 'account': { - 'dn': 'uid=%s, ou=People, dc=investici, dc=org, o=Anarchy', + 'dn': 'uid=%(user)s, ou=People, dc=investici, dc=org, o=Anarchy', }, }