71b0abed0babc35d0a992ea946f4bd575271ba49
[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 import messages
39 from django.core.urlresolvers import reverse
40 from django.utils.translation import ugettext as _
41 from django.db import transaction
42
43 from urlparse import urljoin
44
45 from astakos.im.models import AstakosUser, Invitation
46 from astakos.im.forms import *
47 from astakos.im.util import get_invitation
48 from astakos.im.functions import send_verification, send_activation, \
49     send_admin_notification, activate, SendMailError
50 from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, \
51     DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
52
53 import socket
54 import logging
55 import re
56
57 logger = logging.getLogger(__name__)
58
59 def get_backend(request):
60     """
61     Returns an instance of an activation backend,
62     according to the INVITATIONS_ENABLED setting
63     (if True returns ``astakos.im.activation_backends.InvitationsBackend`` and if False
64     returns ``astakos.im.activation_backends.SimpleBackend``).
65
66     If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
67     is raised.
68     """
69     module = 'astakos.im.activation_backends'
70     prefix = 'Invitations' if INVITATIONS_ENABLED else 'Simple'
71     backend_class_name = '%sBackend' %prefix
72     try:
73         mod = import_module(module)
74     except ImportError, e:
75         raise ImproperlyConfigured('Error loading activation backend %s: "%s"' % (module, e))
76     try:
77         backend_class = getattr(mod, backend_class_name)
78     except AttributeError:
79         raise ImproperlyConfigured('Module "%s" does not define a activation backend named "%s"' % (module, attr))
80     return backend_class(request)
81
82 class ActivationBackend(object):
83     def _is_preaccepted(self, user):
84         # return True if user email matches specific patterns
85         for pattern in RE_USER_EMAIL_PATTERNS:
86             if re.match(pattern, user.email):
87                 return True
88         return False
89     
90     def get_signup_form(self, provider='local', instance=None):
91         """
92         Returns a form instance of the relevant class
93         """
94         main = provider.capitalize() if provider == 'local' else 'ThirdParty'
95         suffix  = 'UserCreationForm'
96         formclass = '%s%s' % (main, suffix)
97         request = self.request
98         initial_data = None
99         if request.method == 'POST':
100             if provider == request.POST.get('provider', ''):
101                 initial_data = request.POST
102         return globals()[formclass](initial_data, instance=instance, request=request)
103     
104     def handle_activation(self, user, \
105                           activation_template_name='im/activation_email.txt', \
106                           greeting_template_name='im/welcome_email.txt', \
107                           admin_email_template_name='im/admin_notification.txt', \
108                           switch_accounts_email_template_name='im/switch_accounts_email.txt'):
109         """
110         If the user is already active returns immediately.
111         If the user is not active and there is another account associated with
112         the specific email, it sends an informative email to the user whether
113         wants to switch to this account.
114         If the user is preaccepted and the email is verified, the account is
115         activated automatically. Otherwise, if the email is not verified,
116         it sends a verification email to the user.
117         If the user is not preaccepted, it sends an email to the administrators
118         and informs the user that the account is pending activation.
119         """
120         try:
121             if user.is_active:
122                 return RegistationCompleted()
123             if user.conflicting_email():
124                 send_verification(user, switch_accounts_email_template_name)
125                 return SwitchAccountsVerificationSent(user.email)
126             
127             if self._is_preaccepted(user):
128                 if user.email_verified:
129                     activate(user, greeting_template_name)
130                     return RegistationCompleted()
131                 else:
132                     send_activation(user, activation_template_name)
133                     return VerificationSent()
134             else:
135                 send_admin_notification(user, admin_email_template_name)
136                 return NotificationSent()
137         except BaseException, e:
138             logger.exception(e)
139             raise e
140
141 class InvitationsBackend(ActivationBackend):
142     """
143     A activation backend which implements the following workflow: a user
144     supplies the necessary registation information, if the request contains a valid
145     inivation code the user is automatically activated otherwise an inactive user
146     account is created and the user is going to receive an email as soon as an
147     administrator activates his/her account.
148     """
149     def __init__(self, request):
150         self.request = request
151         super(InvitationsBackend, self).__init__()
152
153     def get_signup_form(self, provider='local', instance=None):
154         """
155         Returns a form instance of the relevant class
156         
157         raises Invitation.DoesNotExist and ValueError if invitation is consumed
158         or invitation username is reserved.
159         """
160         self.invitation = get_invitation(self.request)
161         invitation = self.invitation
162         initial_data = self.get_signup_initial_data(provider)
163         prefix = 'Invited' if invitation else ''
164         main = provider.capitalize()
165         suffix  = 'UserCreationForm'
166         formclass = '%s%s%s' % (prefix, main, suffix)
167         return globals()[formclass](initial_data, instance=instance, request=self.request)
168
169     def get_signup_initial_data(self, provider):
170         """
171         Returns the necassary activation form depending the user is invited or not
172
173         Throws Invitation.DoesNotExist in case ``code`` is not valid.
174         """
175         request = self.request
176         invitation = self.invitation
177         initial_data = None
178         if request.method == 'GET':
179             if invitation:
180                 # create a tmp user with the invitation realname
181                 # to extract first and last name
182                 u = AstakosUser(realname = invitation.realname)
183                 initial_data = {'email':invitation.username,
184                                 'inviter':invitation.inviter.realname,
185                                 'first_name':u.first_name,
186                                 'last_name':u.last_name,
187                                 'provider':provider}
188         else:
189             if provider == request.POST.get('provider', ''):
190                 initial_data = request.POST
191         return initial_data
192
193     def _is_preaccepted(self, user):
194         """
195         If there is a valid, not-consumed invitation code for the specific user
196         returns True else returns False.
197         """
198         if super(InvitationsBackend, self)._is_preaccepted(user):
199             return True
200         invitation = self.invitation
201         if not invitation:
202             return False
203         if invitation.username == user.email and not invitation.is_consumed:
204             invitation.consume()
205             return True
206         return False
207
208 class SimpleBackend(ActivationBackend):
209     """
210     A activation backend which implements the following workflow: a user
211     supplies the necessary registation information, an incative user account is
212     created and receives an email in order to activate his/her account.
213     """
214     def __init__(self, request):
215         self.request = request
216         super(SimpleBackend, self).__init__()
217     
218     def _is_preaccepted(self, user):
219         if super(SimpleBackend, self)._is_preaccepted(user):
220             return True
221         if MODERATION_ENABLED:
222             return False
223         return True
224
225 class ActivationResult(object):
226     def __init__(self, message):
227         self.message = message
228
229 class VerificationSent(ActivationResult):
230     def __init__(self):
231         message = _('Verification sent.')
232         super(VerificationSent, self).__init__(message)
233
234 class SwitchAccountsVerificationSent(ActivationResult):
235     def __init__(self, email):
236         message = _('This email is already associated with another \
237                     local account. To change this account to a shibboleth \
238                     one follow the link in the verification email sent \
239                     to %s. Otherwise just ignore it.' % email)
240         super(SwitchAccountsVerificationSent, self).__init__(message)
241
242 class NotificationSent(ActivationResult):
243     def __init__(self):
244         message = _('Your request for an account was successfully received and is now pending \
245                     approval. You will be notified by email in the next few days. Thanks for \
246                     your interest in ~okeanos! The GRNET team.')
247         super(NotificationSent, self).__init__(message)
248
249 class RegistationCompleted(ActivationResult):
250     def __init__(self):
251         message = _('Registration completed. You can now login.')
252         super(RegistationCompleted, self).__init__(message)