diff options
author | Alexander Sulfrian <alexander@sulfrian.net> | 2016-01-23 20:17:36 +0100 |
---|---|---|
committer | Alexander Sulfrian <alexander@sulfrian.net> | 2016-01-25 01:56:49 +0100 |
commit | 9f24d8bd26e7dd3b7e36294edee31be7a37fa650 (patch) | |
tree | 85f50a093f70d36fd58c52072ba2e5ccdaef91e7 | |
parent | ea3983d891bc6e34a827902ac8cf15734923e14c (diff) | |
download | web-9f24d8bd26e7dd3b7e36294edee31be7a37fa650.tar.gz web-9f24d8bd26e7dd3b7e36294edee31be7a37fa650.tar.bz2 web-9f24d8bd26e7dd3b7e36294edee31be7a37fa650.zip |
backend/user: Allow different backends for users
-rw-r--r-- | accounts/__init__.py | 26 | ||||
-rw-r--r-- | accounts/account.py | 310 | ||||
-rw-r--r-- | accounts/backend/user/__init__.py | 116 | ||||
-rw-r--r-- | accounts/backend/user/dummy.py | 104 | ||||
-rw-r--r-- | accounts/backend/user/ldap.py | 230 | ||||
-rw-r--r-- | accounts/default_settings.py | 1 | ||||
-rw-r--r-- | accounts/forms.py | 25 | ||||
-rw-r--r-- | accounts/utils.py | 6 | ||||
-rw-r--r-- | accounts/views/admin/__init__.py | 2 |
9 files changed, 476 insertions, 344 deletions
diff --git a/accounts/__init__.py b/accounts/__init__.py index 09aa875..d52a724 100644 --- a/accounts/__init__.py +++ b/accounts/__init__.py @@ -18,6 +18,7 @@ if 'SPLINE_ACCOUNT_WEB_SETTINGS' in os.environ: app.all_services = account.SERVICES #TODO: take that from our json file or so app.username_blacklist = list() +app.user_backend = get_backend(app.config['USER_BACKEND'], app) app.mail_backend = get_backend(app.config['MAIL_BACKEND'], app) @app.before_request @@ -28,11 +29,6 @@ def session_permanent(): session.permanent = False @app.before_request -def ldap_connect(): - g.ldap = account.AccountService(app.config['LDAP_HOST'], app.config['LDAP_BASE_DN'], - app.config['LDAP_ADMIN_USER'], app.config['LDAP_ADMIN_PASS'], app.all_services) - -@app.before_request def initialize_user(): g.user = None @@ -40,7 +36,7 @@ def initialize_user(): username = ensure_utf8(session['username']) password = ensure_utf8(decrypt_password(session['password'])) try: - g.user = g.ldap.auth(username, password) + g.user = current_app.user_backend.auth(username, password) except ldap.INVALID_CREDENTIALS: # we had crap in the session, delete it logout_user() @@ -100,9 +96,9 @@ def register_complete(token): username, mail = http_verify_confirmation('register', token.encode('ascii'), timeout=3*24*60*60) try: - g.ldap.get_by_uid(username) - g.ldap.get_by_mail(mail) - except account.NoSuchUserError: + app.user_backend.get_by_uid(username) + app.user_backend.get_by_mail(mail) + except app.user_backend.NoSuchUserError: pass else: flash(u'Du hast den Benutzer bereits angelegt! Du kannst dich jetzt einfach einloggen:') @@ -113,7 +109,7 @@ def register_complete(token): password = form.password.data user = account.Account(username, mail, password=form.password.data) - g.ldap.register(user) + app.user_backend.register(user) # populate request context and session assert login_user(user.uid, user.password) @@ -172,9 +168,9 @@ def lost_password_complete(token): form = RegisterCompleteForm(request.form, csrf_enabled=False) if request.method == 'POST' and form.validate(): - user = g.ldap.get_by_uid(username) + user = app.user_backend.get_by_uid(username) user.change_password(form.password.data) - g.ldap.update(user, as_admin=True) + app.user_backend.update(user, as_admin=True) session['username'] = username session['password'] = encrypt_password(form.password.data) @@ -235,7 +231,7 @@ def settings(): g.user.change_password(field.data, None, service.id) if changed: - g.ldap.update(g.user, as_admin=True) #XXX: as_admin wieder wegmachen sobald ACLs richtig gesetzt sind + app.user_backend.update(g.user, as_admin=True) #XXX: as_admin wieder wegmachen sobald ACLs richtig gesetzt sind return redirect(url_for('settings')) else: flash(u'Nichts geändert.') @@ -258,13 +254,13 @@ def change_mail(token): if g.user.uid != username: raise Forbidden(u'Bitte logge dich als der Benutzer ein, dessen E-Mail-Adresse du ändern willst.') - results = g.ldap.find_by_mail(mail) + results = app.user_backend.find_by_mail(mail) for user in results: if user.uid != g.user.uid: raise Forbidden(u'Diese E-Mail-Adresse wird schon von einem anderen account benutzt!') g.user.change_email(mail) - g.ldap.update(g.user) + app.user_backend.update(g.user) flash(u'E-Mail-Adresse geändert.', 'success') return redirect(url_for('settings')) diff --git a/accounts/account.py b/accounts/account.py index 4c522a0..fdfeba2 100644 --- a/accounts/account.py +++ b/accounts/account.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import ldap from utils import Service from uuid import uuid4 @@ -13,308 +12,6 @@ SERVICES = [ ] - -class AccountService: - """ - To simplify account management through ldap this class can be used. - The AccountService class is stateless. It means that every request needs - its own authentication request (bind). - - To test you stuff against our test setup use Port-Forwarding - ssh spline -L 5678:vm-account:389 -N - - * register a new user - >> service = AccountService(LDAP_HOST, LDAP_BASE_DN, ADMIN_USER, ADMIN_PW, SERVICES) - >> foo = Account('foo','foo@bar.de', password='bar') - >> service.register(foo) - - * authenticate a new user - >> service = AccountService(LDAP_HOST, LDAP_BASE_DN, ADMIN_USER, ADMIN_PW, SERVICES) - >> foo = service.auth('foo', 'bar') - - * updates an account - >> foo.change_mail('a@b.de') - >> foo.change_password('newpw','oldpw') # changes root password - >> foo.change_password('newpw','oldpw', 'gitlab') # changes password for gitlab - >> service.update(foo) # save changes in ldap backend - # save changes in ldap backend as admin user (no need for old password) - >> service.update(foo, as_admin=True) - - * delete an account - >> service = AccountService(LDAP_HOST, LDAP_BASE_DN, LDAP_ADMIN_USER, LDAP_ADMIN_PASS, SERVICES) - >> service.delete(Account) - >> service.delete('foo') - - * find accounts - >> service = AccountService(LDAP_HOST, LDAP_BASE_DN, LDAP_ADMIN_USER, LDAP_ADMIN_PASS, SERVICES) - >> all_accounts = service.find() # find all accounts - >> print([x.uid for x in all_accounts]) - >> service.find_by_uid('test') # find users by uid - >> service.get_by_uid('test') # same, raise NoSuchUserError if no match - >> service.find_by_mail('test@test.de') # find users by mail - >> service.find_by_uid('test*', wildcard=True) # find with wildcards - - """ - def __init__(self, ldap_host, base_dn, admin_user, admin_pass, services): - self.ldap_host = ldap_host - self.base_dn = base_dn - self.admin_user = admin_user - self.admin_pass = admin_pass - self.services = services - self.admin = False - self.binded = False - - - def auth(self, username, password): - """ - Tries to authenticate a user with a given password. If the - authentication is successful an Account object will be returned. - """ - - self._bind_anonymous() - dn = self._format_dn([('uid', username), ('ou','users')]) - dn_user, data_user = self.connection.search_s(dn, ldap.SCOPE_SUBTREE)[0] - - self._bind_as_user(username, password) - uid = data_user['uid'][0] - mail = data_user['mail'][0] - - dn = self._format_dn([('ou', 'services')]) - filterstr = '(uid=%s)' % self._escape(uid) - data_service = self.connection.search_s(dn, ldap.SCOPE_SUBTREE, filterstr) - - services = [] - for entry in data_service: - cn = filter(lambda x: x.startswith('cn='), entry[0].split(','))[0] - services.append(cn.split('=')[-1]) - - acc = Account(uid, mail, services, dn_user, password) - - self._unbind() - - return acc - - def get_by_uid(self, uid): - """ - Find a single user by uid. Unlike find_by_uid, don't return a list but - raise NoSuchUserError if there is no such user. - """ - users = self.find_by_uid(uid) - if len(users) == 0: - raise NoSuchUserError('No such user') - if len(users) > 1: - raise ShouldNotHappen('Several users for one uid returned.') - - return users[0] - - def get_by_mail(self, mail): - """ - Find a single user by mail. Unlike find_by_mail, don't return a list but - raise NoSuchUserError if there is no such user. - """ - users = self.find_by_mail(mail) - if len(users) == 0: - raise NoSuchUserError('No such user') - if len(users) > 1: - raise ShouldNotHappen('Several users for one mail returned.') - - return users[0] - - def find_by_uid(self, uid, wildcard=False): - return self.find({'uid': uid}, wildcard) - - def find_by_mail(self, mail, wildcard=False): - return self.find({'mail': mail}, wildcard) - - def find(self, filters={}, wildcard=False): - """ - Find accounts by a given filter with key:value semantic) - """ - self._bind_anonymous() - - filters['objectClass'] = 'inetOrgPerson' - - - filter_as_list = ['(%s=%s)' % (k,self._escape(v, wildcard)) for k,v in filters.items()] - filterstr = ''.join(filter_as_list) - if len(filter_as_list) > 1: - filterstr = '(&%s)' % filterstr - - dn = self._format_dn([('ou','users')]) - data = self.connection.search_s(dn, ldap.SCOPE_SUBTREE, filterstr) - - accounts = [] - for a in data: - accounts.append(Account(a[1]['uid'][0], a[1]['mail'][0])) - - self._unbind() - - return accounts - - - def register(self, account): - """ - Persists an account in the ldap backend - """ - self._bind_as_admin() - - dn = self._format_dn([('uid', account.uid),('ou','users')]) - uid = self._escape(account.uid) - attr = [ - ('objectClass', ['top','inetOrgPerson']), - ('uid', uid), ('sn', 'n/a'), ('cn', uid), - ('mail', account.attributes['mail']) - ] - self.connection.add_s(dn, attr) - account.dn = dn - - account.new_password_root = (None, account.password) - self._alter_passwords(account) - - self._unbind() - - - def update(self, account, as_admin=False): - """ - Updates account informations like passwords or email. - """ - if as_admin: - self._bind_as_admin() - else: - self._bind_as_user(account.uid, account.password) - - attr = [(ldap.MOD_REPLACE, k, v) for k, v in account.attributes.items()] - dn = self._format_dn([('uid',account.uid),('ou','users')]) - self.connection.modify_s(dn, attr) - self._alter_passwords(account, as_admin=as_admin) - - self._unbind() - - - def delete(self, account, password=None, as_admin=False): - """ - Deletes an account permanently. - """ - - if isinstance(account, basestring): - raise NotImplementedError() - else: - user = account.uid - password = account.password - - if as_admin: - self._bind_as_admin() - else: - self._bind_as_user(user, password) - - dn = [self._format_dn([('uid',user),('cn',s),('ou','services')]) for s.id in account.services] - dn.append(self._format_dn([('uid', user), ('ou','users')])) - - for x in dn: - self.connection.delete_s(x) - - self._unbind() - - def _format_dn(self, attr, with_base_dn = True): - if with_base_dn: - attr.extend(self.base_dn) - - dn = ['%s=%s' % (item[0], self._escape(item[1])) for item in attr] - - return ','.join(dn) - - def _bind(self, dn, password): - self.connection = ldap.initialize(self.ldap_host) - self.connection.version = ldap.VERSION3 - - self.connection.simple_bind_s(dn, password) - - def _bind_as_admin(self): - if self.binded: - self._unbind() - - dn = self._format_dn([('cn', self.admin_user)]) - self._bind(dn, self.admin_pass) - - self.admin = True - self.binded = True - - def _bind_as_user(self, username, password): - if self.binded: - self._unbind() - - dn = self._format_dn([('uid', username),('ou', 'users')]) - self._bind(dn, password) - - self.binded = True - self.admin = True - - def _bind_anonymous(self): - if self.binded: - self._unbind() - - self._bind('','') - - self.binded = True - - - def _unbind(self): - self.connection.unbind_s() - self.admin = False - self.binded = False - - - def _alter_passwords(self, account, as_admin=False): - if account.new_password_root: - dn = self._format_dn([('uid',account.uid),('ou','users')]) - old, new = account.new_password_root - if as_admin: - self.connection.passwd_s(dn, None, new) - else: - try: - self.connection.passwd_s(dn, old, new) - except ldap.UNWILLING_TO_PERFORM: - raise InvalidPasswordError() - - account.password = new - - account.new_password_root = None - - for service, passwords in account.new_password_services.items(): - dn = self._format_dn([('uid',account.uid),('cn',service),('ou','services')]) - old, new = passwords - - if new != None: - if service not in account.services: - attr = [('objectClass', ['top', 'servicePassword']), ('uid', account.uid)] - self.connection.add_s(dn, attr) - - if as_admin: - self.connection.passwd_s(dn, None, new) - else: - self.connection.passwd_s(dn, old, new) - - else: - s = service.lower() - if s in account.services: - self.connection.delete_s(dn) - account.services.remove(s) - - account.new_password_services = {} - - def _escape(self, s, wildcard=False): - chars_to_escape = ['\\',',','=','+','<','>',';','"','\'','#','(',')','\0'] - - if not wildcard: - chars_to_escape.append('*') - - escape = lambda x,y: x.replace(y,'\%02X' % ord(y)) - - return reduce(escape, chars_to_escape, s) - - - - class Account: """ An Account represents a complex ldap tree entry for spline users. @@ -374,10 +71,3 @@ class Account: raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) - - -class NoSuchUserError(ValueError): - pass - -class InvalidPasswordError(ldap.INVALID_CREDENTIALS): - pass diff --git a/accounts/backend/user/__init__.py b/accounts/backend/user/__init__.py new file mode 100644 index 0000000..749f284 --- /dev/null +++ b/accounts/backend/user/__init__.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + + +class NoSuchUserError(ValueError): + pass + + +class ShouldNotHappen(RuntimeError): + pass + + +class Backend(object): + """ + This is the backend class for the account management. It is + stateless, so every request needs the authentication data again. + + * register a new user + >> backend = Backend(app) + >> foo = Account('foo','foo@bar.de', password='bar') + >> backend.register(foo) + + * authenticate a new user + >> backend = Backend(app) + >> foo = backend.auth('foo', 'bar') + + * updates an account + >> foo.change_mail('a@b.de') + >> foo.change_password('newpw','oldpw') # changes root password + >> foo.change_password('newpw','oldpw', 'gitlab') # changes password for gitlab + >> backend.update(foo) # save changes in the backend + # save changes in the backend as admin user (no need for old password) + >> backend.update(foo, as_admin=True) + + * delete an account + >> backend = Backend(app) + >> backend.delete(Account) + >> backend.delete('foo') + + * find accounts + >> backend = Backend(app) + >> all_accounts = backend.find() # find all accounts + >> print([x.uid for x in all_accounts]) + >> backend.find_by_uid('test') # find users by uid + >> backend.get_by_uid('test') # same, raise NoSuchUserError if no match + >> backend.find_by_mail('test@test.de') # find users by mail + >> backend.find_by_uid('test*', wildcard=True) # find with wildcards + """ + + def __init__(self, app): + self.app = app + + #: Exception type, that is raised if no matching user was found. + self.NoSuchUserError = NoSuchUserError + + def auth(self, username, password): + """ + Tries to authenticate a user with a given password. If the + authentication is successful an Account object will be returned. + """ + raise NotImplementedError() + + def get_by_uid(self, uid): + """ + Find a single user by uid. Unlike find_by_uid, don't return a list but + raise NoSuchUserError if there is no such user. + """ + users = self.find_by_uid(uid) + if len(users) == 0: + raise NoSuchUserError('No such user') + if len(users) > 1: + raise ShouldNotHappen('Several users for one uid returned.') + + return users[0] + + def get_by_mail(self, mail): + """ + Find a single user by mail. Unlike find_by_mail, don't return a list but + raise NoSuchUserError if there is no such user. + """ + users = self.find_by_mail(mail) + if len(users) == 0: + raise NoSuchUserError('No such user') + if len(users) > 1: + raise ShouldNotHappen('Several users for one mail returned.') + + return users[0] + + def find_by_uid(self, uid, wildcard=False): + return self.find({'uid': uid}, wildcard) + + def find_by_mail(self, mail, wildcard=False): + return self.find({'mail': mail}, wildcard) + + def find(self, filters=None, wildcard=False): + """ + Find accounts by a given filter. + """ + raise NotImplementedError() + + def register(self, account): + """ + Persists an account in the backend. + """ + raise NotImplementedError() + + def update(self, account, as_admin=False): + """ + Updates account information like passwords or email. + """ + raise NotImplementedError() + + def delete(self, account, password=None, as_admin=False): + """ + Deletes an account permanently. + """ + raise NotImplementedError() diff --git a/accounts/backend/user/dummy.py b/accounts/backend/user/dummy.py new file mode 100644 index 0000000..c4925fb --- /dev/null +++ b/accounts/backend/user/dummy.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +from copy import deepcopy +from fnmatch import fnmatch + +from . import Backend +from accounts.models import Account + + +def _match_filter(account, filters, wildcard): + if filters is None: + return True + + for key in filters: + if not hasattr(account, key): + return False + + if wildcard: + if not fnmatch(getattr(account, key), filters[key]): + return False + else: + if getattr(account, key) != filters[key]: + return False + + return True + + +class DummyBackend(Backend): + """ + This is a simple user backend that persists the users in a simple list. + + The users are stored only in memory and during initialization two dummy + users (test and test2) are created. + """ + + def __init__(self, app): + super(DummyBackend, self).__init__(app) + + self._storage = [ + Account('test', 'test@accounts.spline.de', password='test'), + Account('test2', 'test2@accounts.spline.de', password='test2'), + ] + + def auth(self, username, password): + """ + Tries to authenticate a user with a given password. If the + authentication is successful an Account object will be returned. + """ + acc = self.get_by_uid(username) + if acc.password != password: + raise ValueError("Invalid password") + + return acc + + def find(self, filters=None, wildcard=False): + """ + Find accounts by a given filter. + """ + results = [] + for acc in self._storage: + if _match_filter(acc, filters, wildcard): + results.append(deepcopy(acc)) + + return results + + def register(self, account): + """ + Persists an account in the backend. + """ + if account.password is None: + raise ValueError("Password required for register") + + self._storage.append(deepcopy(account)) + + def update(self, account, as_admin=False): + """ + Updates account information like passwords or email. + """ + stored_account = self.get_by_uid(account.uid) + if not as_admin: + if stored_account.password != account.password: + raise ValueError("Invalid password") + + self._storage = [acc for acc in self._storage if acc.uid != account.uid] + new_acc = deepcopy(account) + + if account.new_password_root: + old, new = account.new_password_root + if old == stored_account.password: + new_acc.password = new + + self._storage.append(new_acc) + + def delete(self, account, password=None, as_admin=False): + """ + Deletes an account permanently. + """ + stored_account = self.get_by_uid(account.uid) + if not as_admin: + if stored_account.password != account.password: + raise ValueError("Invalid password") + + self._storage = [acc for acc in self._storage if acc.uid != account.uid] diff --git a/accounts/backend/user/ldap.py b/accounts/backend/user/ldap.py new file mode 100644 index 0000000..5472caf --- /dev/null +++ b/accounts/backend/user/ldap.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import ldap + +from . import Backend +from accounts.models import Account + + +class LdapBackend(Backend): + + def __init__(self, app): + super(LdapBackend, self).__init__(app) + + self.ldap_host = self.app.config['LDAP_HOST'] + self.base_dn = self.app.config['LDAP_BASE_DN'] + self.admin_user = self.app.config['LDAP_ADMIN_USER'] + self.admin_pass = self.app.config['LDAP_ADMIN_PASS'] + self.services = self.app.all_services + + self.admin = False + self.binded = False + + def auth(self, username, password): + """ + Tries to authenticate a user with a given password. If the + authentication is successful an Account object will be returned. + """ + self._bind_anonymous() + dn = self._format_dn([('uid', username), ('ou','users')]) + dn_user, data_user = self.connection.search_s(dn, ldap.SCOPE_SUBTREE)[0] + + self._bind_as_user(username, password) + uid = data_user['uid'][0] + mail = data_user['mail'][0] + + dn = self._format_dn([('ou', 'services')]) + filterstr = '(uid=%s)' % self._escape(uid) + data_service = self.connection.search_s(dn, ldap.SCOPE_SUBTREE, filterstr) + + services = [] + for entry in data_service: + cn = filter(lambda x: x.startswith('cn='), entry[0].split(','))[0] + services.append(cn.split('=')[-1]) + + acc = Account(uid, mail, services, dn_user, password) + + self._unbind() + + return acc + + def find(self, filters={}, wildcard=False): + """ + Find accounts by a given filter. + """ + self._bind_anonymous() + + filters['objectClass'] = 'inetOrgPerson' + + + filter_as_list = ['(%s=%s)' % (k,self._escape(v, wildcard)) for k,v in filters.items()] + filterstr = ''.join(filter_as_list) + if len(filter_as_list) > 1: + filterstr = '(&%s)' % filterstr + + dn = self._format_dn([('ou','users')]) + data = self.connection.search_s(dn, ldap.SCOPE_SUBTREE, filterstr) + + accounts = [] + for a in data: + accounts.append(Account(a[1]['uid'][0], a[1]['mail'][0])) + + self._unbind() + + return accounts + + def register(self, account): + """ + Persists an account in the backend. + """ + self._bind_as_admin() + + dn = self._format_dn([('uid', account.uid),('ou','users')]) + uid = self._escape(account.uid) + attr = [ + ('objectClass', ['top','inetOrgPerson']), + ('uid', uid), ('sn', 'n/a'), ('cn', uid), + ('mail', account.attributes['mail']) + ] + self.connection.add_s(dn, attr) + account.dn = dn + + account.new_password_root = (None, account.password) + self._alter_passwords(account) + + self._unbind() + + def update(self, account, as_admin=False): + """ + Updates account informations like passwords or email. + """ + if as_admin: + self._bind_as_admin() + else: + self._bind_as_user(account.uid, account.password) + + attr = [(ldap.MOD_REPLACE, k, v) for k, v in account.attributes.items()] + dn = self._format_dn([('uid',account.uid),('ou','users')]) + self.connection.modify_s(dn, attr) + self._alter_passwords(account, as_admin=as_admin) + + self._unbind() + + def delete(self, account, password=None, as_admin=False): + """ + Deletes an account permanently. + """ + if isinstance(account, basestring): + raise NotImplementedError() + else: + user = account.uid + password = account.password + + if as_admin: + self._bind_as_admin() + else: + self._bind_as_user(user, password) + + dn = [self._format_dn([('uid',user),('cn',s),('ou','services')]) for s.id in account.services] + dn.append(self._format_dn([('uid', user), ('ou','users')])) + + for x in dn: + self.connection.delete_s(x) + + self._unbind() + + def _format_dn(self, attr, with_base_dn = True): + if with_base_dn: + attr.extend(self.base_dn) + + dn = ['%s=%s' % (item[0], self._escape(item[1])) for item in attr] + + return ','.join(dn) + + def _bind(self, dn, password): + self.connection = ldap.initialize(self.ldap_host) + self.connection.version = ldap.VERSION3 + + self.connection.simple_bind_s(dn, password) + + def _bind_as_admin(self): + if self.binded: + self._unbind() + + dn = self._format_dn([('cn', self.admin_user)]) + self._bind(dn, self.admin_pass) + + self.admin = True + self.binded = True + + def _bind_as_user(self, username, password): + if self.binded: + self._unbind() + + dn = self._format_dn([('uid', username),('ou', 'users')]) + self._bind(dn, password) + + self.binded = True + self.admin = True + + def _bind_anonymous(self): + if self.binded: + self._unbind() + + self._bind('','') + + self.binded = True + + def _unbind(self): + self.connection.unbind_s() + self.admin = False + self.binded = False + + def _alter_passwords(self, account, as_admin=False): + if account.new_password_root: + dn = self._format_dn([('uid',account.uid),('ou','users')]) + old, new = account.new_password_root + if as_admin: + self.connection.passwd_s(dn, None, new) + else: + try: + self.connection.passwd_s(dn, old, new) + except ldap.UNWILLING_TO_PERFORM: + raise InvalidPasswordError() + + account.password = new + + account.new_password_root = None + + for service, passwords in account.new_password_services.items(): + dn = self._format_dn([('uid',account.uid),('cn',service),('ou','services')]) + old, new = passwords + + if new != None: + if service not in account.services: + attr = [('objectClass', ['top', 'servicePassword']), ('uid', account.uid)] + self.connection.add_s(dn, attr) + + if as_admin: + self.connection.passwd_s(dn, None, new) + else: + self.connection.passwd_s(dn, old, new) + + else: + s = service.lower() + if s in account.services: + self.connection.delete_s(dn) + account.services.remove(s) + + account.new_password_services = {} + + def _escape(self, s, wildcard=False): + chars_to_escape = ['\\',',','=','+','<','>',';','"','\'','#','(',')','\0'] + + if not wildcard: + chars_to_escape.append('*') + + escape = lambda x,y: x.replace(y,'\%02X' % ord(y)) + + return reduce(escape, chars_to_escape, s) diff --git a/accounts/default_settings.py b/accounts/default_settings.py index 538a8ed..09d81f5 100644 --- a/accounts/default_settings.py +++ b/accounts/default_settings.py @@ -23,4 +23,5 @@ LDAP_ADMIN_PASS = 'admin' PREFERRED_URL_SCHEME = 'https' +USER_BACKEND = 'accounts.backend.user.dummy' MAIL_BACKEND = 'accounts.backend.mail.dummy' diff --git a/accounts/forms.py b/accounts/forms.py index deaffa8..c374ef6 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from account import SERVICES, NoSuchUserError +from account import SERVICES from flask import g, current_app, session, Markup from flask.ext.wtf import Form from wtforms import TextField, PasswordField, ValidationError, BooleanField,\ @@ -25,8 +25,8 @@ class RegisterForm(Form): def validate_username(form, field): try: - g.ldap.get_by_uid(field.data) - except NoSuchUserError: + current_app.user_backend.get_by_uid(field.data) + except current_app.user_backend.NoSuchUserError: if current_app.username_blacklist: if field.data.lower() in current_app.username_blacklist: @@ -37,8 +37,8 @@ class RegisterForm(Form): def validate_mail(form, field): try: - g.ldap.get_by_mail(field.data) - except NoSuchUserError: + current_app.user_backend.get_by_mail(field.data) + except current_app.user_backend.NoSuchUserError: pass else: raise ValidationError(Markup(u'Ein Benutzername mit dieser Adresse existiert bereits. ' @@ -49,8 +49,8 @@ class RegisterForm(Form): class AdminCreateAccountForm(RegisterForm): def validate_username(form, field): try: - g.ldap.get_by_uid(field.data) - except NoSuchUserError: + current_app.user_backend.get_by_uid(field.data) + except current_app.user_backend.NoSuchUserError: return else: raise ValidationError(u'Dieser Benutzername ist schon vergeben') @@ -76,12 +76,12 @@ class LostPasswordForm(Form): def validate_username_or_mail(form, field): if '@' not in field.data: try: - form.user = g.ldap.get_by_uid(field.data) + form.user = current_app.user_backend.get_by_uid(field.data) except NoSuchUserError: raise ValidationError(u'Es gibt keinen Benutzer mit diesem Namen.') else: try: - form.user = g.ldap.get_by_mail(field.data) + form.user = current_app.user_backend.get_by_mail(field.data) except NoSuchUserError: raise ValidationError(u'Es gibt keinen Benutzer mit dieser Adresse.') @@ -103,7 +103,7 @@ class SettingsForm(Form): raise ValidationError(u'Altes Passwort ist falsch.') def validate_mail(form, field): - results = g.ldap.find_by_mail(field.data) + results = current_app.user_backend.find_by_mail(field.data) for user in results: if user.uid != g.user.uid: raise ValidationError(u'Diese E-Mail-Adresse wird schon von einem anderen Account benutzt!') @@ -121,12 +121,11 @@ class AdminDisableAccountForm(Form): def validate_username(form, field): try: - form.user = g.ldap.get_by_uid(field.data) - except NoSuchUserError: + form.user = current_app.user_backend.get_by_uid(field.data) + except current_app.user_backend.NoSuchUserError: raise ValidationError(u'Dieser Benutzername existiert nicht') - #TODO: find out how we can use app.all_services in that early state for service in SERVICES: setattr(SettingsForm, 'password_%s' % service.id, diff --git a/accounts/utils.py b/accounts/utils.py index 87a2ba8..3b313d8 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -71,7 +71,7 @@ def login_user(username, password): password = ensure_utf8(password) try: - g.user = g.ldap.auth(username, password) + g.user = current_app.user_backend.auth(username, password) except ldap.INVALID_CREDENTIALS: return False except ldap.NO_SUCH_OBJECT: @@ -242,10 +242,6 @@ def url_for(endpoint, **values): else: return u -# used when we encounter inconsistent data etc -class ShouldNotHappen(RuntimeError): - pass - def get_backend(path, app): module = path.rsplit(".", 1).pop() diff --git a/accounts/views/admin/__init__.py b/accounts/views/admin/__init__.py index 02a2da1..5564f93 100644 --- a/accounts/views/admin/__init__.py +++ b/accounts/views/admin/__init__.py @@ -72,7 +72,7 @@ def disable_account(): mail = current_app.config['DISABLED_ACCOUNT_MAILADDRESS_TEMPLATE'] % form.user.uid form.user.change_email(mail) - g.ldap.update(form.user, as_admin=True) + current_app.user_backend.update(form.user, as_admin=True) flash(u'Passwort auf ein zufälliges und Mailadresse auf %s ' u'gesetzt.' % mail, 'success') |