summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexander Sulfrian <alexander@sulfrian.net>2016-01-23 20:17:36 +0100
committerAlexander Sulfrian <alexander@sulfrian.net>2016-01-25 01:56:49 +0100
commit9f24d8bd26e7dd3b7e36294edee31be7a37fa650 (patch)
tree85f50a093f70d36fd58c52072ba2e5ccdaef91e7
parentea3983d891bc6e34a827902ac8cf15734923e14c (diff)
downloadweb-9f24d8bd26e7dd3b7e36294edee31be7a37fa650.tar.gz
web-9f24d8bd26e7dd3b7e36294edee31be7a37fa650.tar.bz2
web-9f24d8bd26e7dd3b7e36294edee31be7a37fa650.zip
backend/user: Allow different backends for users
-rw-r--r--accounts/__init__.py26
-rw-r--r--accounts/account.py310
-rw-r--r--accounts/backend/user/__init__.py116
-rw-r--r--accounts/backend/user/dummy.py104
-rw-r--r--accounts/backend/user/ldap.py230
-rw-r--r--accounts/default_settings.py1
-rw-r--r--accounts/forms.py25
-rw-r--r--accounts/utils.py6
-rw-r--r--accounts/views/admin/__init__.py2
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')