/solace/auth.py
Python | 326 lines | 159 code | 47 blank | 120 comment | 38 complexity | acf61c8377c19be2adb71e172a1860e6 MD5 | raw file
Possible License(s): BSD-3-Clause
1# -*- coding: utf-8 -*-
2"""
3 solace.auth
4 ~~~~~~~~~~~
5
6 This module implements the auth system.
7
8 :copyright: (c) 2009 by Plurk Inc., see AUTHORS for more details.
9 :license: BSD, see LICENSE for more details.
10"""
11from __future__ import with_statement
12from threading import Lock
13from werkzeug import import_string, redirect
14from werkzeug.contrib.securecookie import SecureCookie
15from datetime import datetime
16
17from solace import settings
18from solace.i18n import lazy_gettext
19from solace.utils.support import UIException
20from solace.utils.mail import send_email
21
22
23_auth_system = None
24_auth_select_lock = Lock()
25
26
27def get_auth_system():
28 """Return the auth system."""
29 global _auth_system
30 with _auth_select_lock:
31 if _auth_system is None:
32 _auth_system = import_string(settings.AUTH_SYSTEM)()
33 return _auth_system
34
35
36def refresh_auth_system():
37 """Tears down the auth system after a config change."""
38 global _auth_system
39 with _auth_system_lock:
40 _auth_system = None
41
42
43def check_used_openids(identity_urls, ignored_owner=None):
44 """Returns a set of all the identity URLs from the list of identity
45 URLs that are already associated on the system. If a owner is given,
46 items that are owned by the given user will not show up in the result
47 list.
48 """
49 query = _OpenIDUserMapping.query.filter(
50 _OpenIDUserMapping.identity_url.in_(identity_urls)
51 )
52 if ignored_owner:
53 query = query.filter(_OpenIDUserMapping.user != ignored_owner)
54 return set([x.identity_url for x in query.all()])
55
56
57class LoginUnsucessful(UIException):
58 """Raised if the login failed."""
59
60
61class AuthSystemBase(object):
62 """The base auth system.
63
64 Most functionality is described in the methods and properties you have
65 to override for subclasses. A special notice applies for user
66 registration.
67
68 Different auth systems may create users at different stages (first login,
69 register etc.). At that point (where the user is created in the
70 database) the system has to call `after_register` and pass it the user
71 (and request) object. That method handles the confirmation mails and
72 whatever else is required. If you do not want your auth system to send
73 confirmation mails you still have to call the method but tell the user
74 of your class to disable registration activation in the configuration.
75
76 `after_register` should *not* be called if the registration process
77 should happen transparently for the user. eg, the user has already
78 registered somewhere else and the Solace account is created based on the
79 already existing account on first login.
80 """
81
82 #: for auth systems that are managing the email externally this
83 #: attributes has to set to `True`. In that case the user will
84 #: be unable to change the email from the profile. (True for
85 #: the plurk auth, possible OpenID support and more.)
86 email_managed_external = False
87
88 #: like `email_managed_external` but for the password
89 password_managed_external = False
90
91 #: set to True to indicate that this login system does not use
92 #: a password. This will also affect the standard login form
93 #: and the standard profile form.
94 passwordless = False
95
96 #: if you don't want to see a register link in the user interface
97 #: for this auth system, you can disable it here.
98 show_register_link = True
99
100 @property
101 def can_reset_password(self):
102 """You can either override this property or leave the default
103 implementation that should work most of the time. By default
104 the auth system can reset the password if the password is not
105 externally managed and not passwordless.
106 """
107 return not (self.passwordless or self.password_managed_external)
108
109 def reset_password(self, request, user):
110 if settings.REGISTRATION_REQUIRES_ACTIVATION:
111 user.is_active = False
112 confirmation_url = url_for('core.activate_user', email=user.email,
113 key=user.activation_key, _external=True)
114 send_email(_(u'Registration Confirmation'),
115 render_template('mails/activate_user.txt', user=user,
116 confirmation_url=confirmation_url),
117 user.email)
118 request.flash(_(u'A mail was sent to %s with a link to finish the '
119 u'registration.') % user.email)
120 else:
121 request.flash(_(u'You\'re registered. You can login now.'))
122
123 def before_register(self, request):
124 """Invoked before the standard register form processing. This is
125 intended to be used to redirect to an external register URL if
126 if the syncronization is only one-directional. If this function
127 returns a response object, Solace will abort standard registration
128 handling.
129 """
130
131 def register(self, request):
132 """Called like a view function with only the request. Has to do the
133 register heavy-lifting. Auth systems that only use the internal
134 database do not have to override this method. Implementers that
135 override this function *have* to call `after_register` to finish
136 the registration of the new user. If `before_register` is unnused
137 it does not have to be called, otherwise as documented.
138 """
139 rv = self.before_register(request)
140 if rv is not None:
141 return rv
142
143 form = RegistrationForm()
144 if request.method == 'POST' and form.validate():
145 user = User(form['username'], form['email'], form['password'])
146 self.after_register(request, user)
147 session.commit()
148 if rv is not None:
149 return rv
150 return form.redirect('kb.overview')
151
152 return render_template('core/register.html', form=form.as_widget())
153
154 def after_register(self, request, user):
155 """Handles activation."""
156 if settings.REGISTRATION_REQUIRES_ACTIVATION:
157 user.is_active = False
158 confirmation_url = url_for('core.activate_user', email=user.email,
159 key=user.activation_key, _external=True)
160 send_email(_(u'Registration Confirmation'),
161 render_template('mails/activate_user.txt', user=user,
162 confirmation_url=confirmation_url),
163 user.email)
164 request.flash(_(u'A mail was sent to %s with a link to finish the '
165 u'registration.') % user.email)
166 else:
167 request.flash(_(u'You\'re registered. You can login now.'))
168
169 def get_login_form(self):
170 """Return the login form to be used by `login`."""
171 return StandardLoginForm()
172
173 def before_login(self, request):
174 """If this login system uses an external login URL, this function
175 has to return a redirect response, otherwise None. This is called
176 before the standard form handling to allow redirecting to an
177 external login URL. This function is called by the default
178 `login` implementation.
179
180 If the actual login happens here because of a back-redirect the
181 system might raise a `LoginUnsucessful` exception.
182 """
183
184 def login(self, request):
185 """Like `register` just for login."""
186 form = self.get_login_form()
187
188 # some login systems require an external login URL. For example
189 # the one we use as Plurk.
190 try:
191 rv = self.before_login(request)
192 if rv is not None:
193 return rv
194 except LoginUnsucessful, e:
195 form.add_error(unicode(e))
196
197 # only validate if the before_login handler did not already cause
198 # an error. In that case there is not much win in validating
199 # twice, it would clear the error added.
200 if form.is_valid and request.method == 'POST' and form.validate():
201 try:
202 rv = self.perform_login(request, **form.data)
203 except LoginUnsucessful, e:
204 form.add_error(unicode(e))
205 else:
206 session.commit()
207 if rv is not None:
208 return rv
209 request.flash(_(u'You are now logged in.'))
210 return form.redirect('kb.overview')
211
212 return self.render_login_template(request, form)
213
214 def perform_login(self, request, **form_data):
215 """If `login` is not overridden, this is called with the submitted
216 form data and might raise `LoginUnsucessful` so signal a login
217 error.
218 """
219 raise NotImplementedError()
220
221 def render_login_template(self, request, form):
222 """Renders the login template"""
223 return render_template('core/login.html', form=form.as_widget())
224
225 def get_edit_profile_form(self, user):
226 """Returns the profile form to be used by the auth system."""
227 return StandardProfileEditForm(user)
228
229 def edit_profile(self, request):
230 """Invoked like a view and does the profile handling."""
231 form = self.get_edit_profile_form(request.user)
232
233 if request.method == 'POST' and form.validate():
234 request.flash(_(u'Your profile was updated'))
235 form.apply_changes()
236 session.commit()
237 return form.redirect(form.user)
238
239 return self.render_edit_profile_template(request, form)
240
241 def render_edit_profile_template(self, request, form):
242 """Renders the template for the profile edit page."""
243 return render_template('users/edit_profile.html',
244 form=form.as_widget())
245
246 def logout(self, request):
247 """This has to logout the user again. This method must not fail.
248 If the logout requires the redirect to an external resource it
249 might return a redirect response. That resource then should not
250 redirect back to the logout page, but instead directly to the
251 **current** `request.next_url`.
252
253 Most auth systems do not have to implement this method. The
254 default one calls `set_user(request, None)`.
255 """
256 self.set_user(request, None)
257
258 def get_user(self, request):
259 """If the user is logged in this method has to return the user
260 object for the user that is logged in. Beware: the request
261 class provides some attributes such as `user` and `is_logged_in`
262 you may never use from this function to avoid recursion. The
263 request object will call this function for those two attributes.
264
265 If the user is not logged in, the return value has to be `None`.
266 This method also has to check if the user was not banned. If the
267 user is banned, it has to ensure that `None` is returned and
268 should ensure that future requests do not trigger this method.
269
270 Most auth systems do not have to implement this method.
271 """
272 user_id = request.session.get('user_id')
273 if user_id is not None:
274 user = User.query.get(user_id)
275 if user is not None and user.is_banned:
276 del request.session['user_id']
277 else:
278 return user
279
280 def set_user(self, request, user):
281 """Can be used by the login function to set the user. This function
282 should only be used for auth systems internally if they are not using
283 an external session.
284 """
285 if user is None:
286 request.session.pop('user_id', None)
287 else:
288 user.last_login = datetime.utcnow()
289 request.session['user_id'] = user.id
290
291
292class InternalAuth(AuthSystemBase):
293 """Authenticate against the internal database."""
294
295 def perform_login(self, request, username, password):
296 user = User.query.filter_by(username=username).first()
297 if user is None:
298 raise LoginUnsucessful(_(u'No user named %s') % username)
299 if not user.is_active:
300 raise LoginUnsucessful(_(u'The user is not yet activated.'))
301 if not user.check_password(password):
302 raise LoginUnsucessful(_(u'Invalid password'))
303 if user.is_banned:
304 raise LoginUnsucessful(_(u'The user got banned from the system.'))
305 self.set_user(request, user)
306
307
308# the openid support will be only available if the openid library is installed.
309# otherwise we create a dummy auth system that fails upon usage.
310try:
311 from solace._openid_auth import OpenIDAuth
312except ImportError:
313 class OpenIDAuth(AuthSystemBase):
314 def __init__(self):
315 raise RuntimeError('python-openid library not installed but '
316 'required for openid support.')
317
318
319# circular dependencies
320from solace.application import url_for
321from solace.models import User, _OpenIDUserMapping
322from solace.database import session
323from solace.i18n import _
324from solace.forms import StandardLoginForm, RegistrationForm, \
325 StandardProfileEditForm
326from solace.templating import render_template