/solace/auth.py

https://bitbucket.org/muhuk/solace · Python · 326 lines · 159 code · 47 blank · 120 comment · 38 complexity · acf61c8377c19be2adb71e172a1860e6 MD5 · raw file

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