PageRenderTime 52ms CodeModel.GetById 29ms app.highlight 19ms RepoModel.GetById 0ms app.codeStats 0ms

/solace/auth.py

https://bitbucket.org/muhuk/solace
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