947f620512454eac5b79cc28e739e27b766d96c9
[astakos] / snf-astakos-app / astakos / im / activation_backends.py
1 # Copyright 2011 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 from django.utils.importlib import import_module
35 from django.core.exceptions import ImproperlyConfigured
36 from django.core.mail import send_mail
37 from django.template.loader import render_to_string
38 from django.contrib.sites.models import Site
39 from django.contrib import messages
40 from django.core.urlresolvers import reverse
41 from django.utils.translation import ugettext as _
42 from django.db import transaction
43
44 from urlparse import urljoin
45
46 from astakos.im.models import AstakosUser, Invitation
47 from astakos.im.forms import *
48 from astakos.im.util import get_invitation
49 from astakos.im.functions import send_verification, send_admin_notification, activate, SendMailError
50 from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
51
52 import socket
53 import logging
54 import re
55
56 logger = logging.getLogger(__name__)
57
58 def get_backend(request):
59     """
60     Returns an instance of an activation backend,
61     according to the INVITATIONS_ENABLED setting
62     (if True returns ``astakos.im.activation_backends.InvitationsBackend`` and if False
63     returns ``astakos.im.activation_backends.SimpleBackend``).
64
65     If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
66     is raised.
67     """
68     module = 'astakos.im.activation_backends'
69     prefix = 'Invitations' if INVITATIONS_ENABLED else 'Simple'
70     backend_class_name = '%sBackend' %prefix
71     try:
72         mod = import_module(module)
73     except ImportError, e:
74         raise ImproperlyConfigured('Error loading activation backend %s: "%s"' % (module, e))
75     try:
76         backend_class = getattr(mod, backend_class_name)
77     except AttributeError:
78         raise ImproperlyConfigured('Module "%s" does not define a activation backend named "%s"' % (module, attr))
79     return backend_class(request)
80
81 class ActivationBackend(object):
82     def _is_preaccepted(self, user):
83         # return True if user email matches specific patterns
84         for pattern in RE_USER_EMAIL_PATTERNS:
85             if re.match(pattern, user.email):
86                 return True
87         return False
88     
89     def get_signup_form(self, provider='local', instance=None):
90         """
91         Returns the form class name
92         """
93         main = provider.capitalize() if provider == 'local' else 'ThirdParty'
94         suffix  = 'UserCreationForm'
95         formclass = '%s%s' % (main, suffix)
96         request = self.request
97         initial_data = None
98         if request.method == 'POST':
99             if provider == request.POST.get('provider', ''):
100                 initial_data = request.POST
101         return globals()[formclass](initial_data, instance=instance, request=request)
102     
103     def handle_activation(self, user, \
104                           verification_template_name='im/activation_email.txt', \
105                           greeting_template_name='im/welcome_email.txt', \
106                           admin_email_template_name='im/admin_notification.txt', \
107                           switch_accounts_email_template_name='im/switch_accounts_email.txt'):
108         """
109         If the user is already active returns immediately.
110         If the user is not active and there is another account associated with
111         the specific email, it sends an informative email to the user whether
112         wants to switch to this account.
113         If the user is preaccepted and the email is verified, the account is
114         activated automatically. Otherwise, if the email is not verified,
115         it sends a verification email to the user.
116         If the user is not preaccepted, it sends an email to the administrators
117         and informs the user that the account is pending activation.
118         """
119         try:
120             if user.is_active:
121                 return RegistationCompleted()
122             if user.conflicting_email():
123                 send_verification(user, switch_accounts_email_template_name)
124                 return SwitchAccountsVerificationSent(user.email)
125             
126             if self._is_preaccepted(user):
127                 if user.email_verified:
128                     activate(user, greeting_template_name)
129                     return RegistationCompleted()
130                 else:
131                     send_verification(user, verification_template_name)
132                     return VerificationSent()
133             else:
134                 send_admin_notification(user, admin_email_template_name)
135                 return NotificationSent()
136         except BaseException, e:
137             logger.exception(e)
138             raise e
139
140 class InvitationsBackend(ActivationBackend):
141     """
142     A activation backend which implements the following workflow: a user
143     supplies the necessary registation information, if the request contains a valid
144     inivation code the user is automatically activated otherwise an inactive user
145     account is created and the user is going to receive an email as soon as an
146     administrator activates his/her account.
147     """
148     def __init__(self, request):
149         self.request = request
150         super(InvitationsBackend, self).__init__()
151
152     def get_signup_form(self, provider='local', instance=None):
153         """
154         Returns the form class
155         
156         raises Invitation.DoesNotExist and ValueError if invitation is consumed
157         or invitation username is reserved.
158         """
159         self.invitation = get_invitation(self.request)
160         invitation = self.invitation
161         initial_data = self.get_signup_initial_data(provider)
162         prefix = 'Invited' if invitation else ''
163         main = provider.capitalize()
164         suffix  = 'UserCreationForm'
165         formclass = '%s%s%s' % (prefix, main, suffix)
166         return globals()[formclass](initial_data, instance=instance, request=self.request)
167
168     def get_signup_initial_data(self, provider):
169         """
170         Returns the necassary activation form depending the user is invited or not
171
172         Throws Invitation.DoesNotExist in case ``code`` is not valid.
173         """
174         request = self.request
175         invitation = self.invitation
176         initial_data = None
177         if request.method == 'GET':
178             if invitation:
179                 # create a tmp user with the invitation realname
180                 # to extract first and last name
181                 u = AstakosUser(realname = invitation.realname)
182                 initial_data = {'email':invitation.username,
183                                 'inviter':invitation.inviter.realname,
184                                 'first_name':u.first_name,
185                                 'last_name':u.last_name,
186                                 'provider':provider}
187         else:
188             if provider == request.POST.get('provider', ''):
189                 initial_data = request.POST
190         return initial_data
191
192     def _is_preaccepted(self, user):
193         """
194         If there is a valid, not-consumed invitation code for the specific user
195         returns True else returns False.
196         """
197         if super(InvitationsBackend, self)._is_preaccepted(user):
198             return True
199         invitation = self.invitation
200         if not invitation:
201             return False
202         if invitation.username == user.email and not invitation.is_consumed:
203             invitation.consume()
204             return True
205         return False
206
207 class SimpleBackend(ActivationBackend):
208     """
209     A activation backend which implements the following workflow: a user
210     supplies the necessary registation information, an incative user account is
211     created and receives an email in order to activate his/her account.
212     """
213     def __init__(self, request):
214         self.request = request
215         super(SimpleBackend, self).__init__()
216     
217     def _is_preaccepted(self, user):
218         if super(SimpleBackend, self)._is_preaccepted(user):
219             return True
220         if MODERATION_ENABLED:
221             return False
222         return True
223
224 class ActivationResult(object):
225     def __init__(self, message):
226         self.message = message
227
228 class VerificationSent(ActivationResult):
229     def __init__(self):
230         message = _('Verification sent.')
231         super(VerificationSent, self).__init__(message)
232
233 class SwitchAccountsVerificationSent(ActivationResult):
234     def __init__(self, email):
235         message = _('This email is already associated with another \
236                     local account. To change this account to a shibboleth \
237                     one follow the link in the verification email sent \
238                     to %s. Otherwise just ignore it.' % email)
239         super(SwitchAccountsVerificationSent, self).__init__(message)
240
241 class NotificationSent(ActivationResult):
242     def __init__(self):
243         message = _('Your request for an account was successfully received and is now pending \
244                     approval. You will be notified by email in the next few days. Thanks for \
245                     your interest in ~okeanos! The GRNET team.')
246         super(NotificationSent, self).__init__(message)
247
248 class RegistationCompleted(ActivationResult):
249     def __init__(self):
250         message = _('Registration completed. You can now login.')
251         super(RegistationCompleted, self).__init__(message)