Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / activation_backends.py @ f72ba65d

History | View | Annotate | Download (20 kB)

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.utils.translation import ugettext as _
37

    
38
from astakos.im.models import AstakosUser
39
from astakos.im import functions
40
from astakos.im import settings
41
from astakos.im import forms
42

    
43
from astakos.im.quotas import qh_sync_new_user
44

    
45
import astakos.im.messages as astakos_messages
46

    
47
import datetime
48
import logging
49
import re
50
import json
51

    
52
logger = logging.getLogger(__name__)
53

    
54

    
55
def get_backend():
56
    """
57
    Returns an instance of an activation backend,
58
    according to the INVITATIONS_ENABLED setting
59
    (if True returns ``astakos.im.activation_backends.InvitationsBackend``
60
    and if False
61
    returns ``astakos.im.activation_backends.SimpleBackend``).
62

63
    If the backend cannot be located
64
    ``django.core.exceptions.ImproperlyConfigured`` is raised.
65
    """
66
    module = 'astakos.im.activation_backends'
67
    prefix = 'Invitations' if settings.INVITATIONS_ENABLED else 'Simple'
68
    backend_class_name = '%sBackend' % prefix
69
    try:
70
        mod = import_module(module)
71
    except ImportError, e:
72
        raise ImproperlyConfigured(
73
            'Error loading activation backend %s: "%s"' % (module, e))
74
    try:
75
        backend_class = getattr(mod, backend_class_name)
76
    except AttributeError:
77
        raise ImproperlyConfigured(
78
            'Module "%s" does not define a activation backend named "%s"' % (
79
                module, backend_class_name))
80
    return backend_class(settings.MODERATION_ENABLED)
81

    
82

    
83
class ActivationBackend(object):
84
    """
85
    ActivationBackend handles user verification/activation.
86

87
    Example usage::
88
    >>> # it is wise to not instantiate a backend class directly but use
89
    >>> # get_backend method instead.
90
    >>> backend = get_backend()
91
    >>> formCls = backend.get_signup_form(request.POST)
92
    >>> if form.is_valid():
93
    >>>     user = form.save(commit=False)
94
    >>>     # this creates auth provider objects
95
    >>>     form.store_user(user)
96
    >>>     activation = backend.handle_registration(user)
97
    >>>     # activation.status is one of backend.Result.{*} activation result
98
    >>>     # types
99
    >>>
100
    >>>     # sending activation notifications is not done automatically
101
    >>>     # we need to call send_result_notifications
102
    >>>     backend.send_result_notifications(activation)
103
    >>>     return HttpResponse(activation.message)
104
    """
105

    
106
    verification_template_name = 'im/activation_email.txt'
107
    greeting_template_name = 'im/welcome_email.txt'
108
    pending_moderation_template_name = \
109
        'im/account_pending_moderation_notification.txt'
110
    activated_email_template_name = 'im/account_activated_notification.txt'
111

    
112
    class Result:
113
        # user created, email verification sent
114
        PENDING_VERIFICATION = 1
115
        # email verified
116
        PENDING_MODERATION = 2
117
        # user moderated
118
        ACCEPTED = 3
119
        # user rejected
120
        REJECTED = 4
121
        # inactive user activated
122
        ACTIVATED = 5
123
        # active user deactivated
124
        DEACTIVATED = 6
125
        # something went wrong
126
        ERROR = -1
127

    
128
    def __init__(self, moderation_enabled):
129
        self.moderation_enabled = moderation_enabled
130

    
131
    def _is_preaccepted(self, user):
132
        """
133
        Decide whether user should be automatically moderated. The method gets
134
        called only when self.moderation_enabled is set to True.
135

136
        The method returns False or a string identifier which later will be
137
        stored in user's accepted_policy field. This is helpfull for
138
        administrators to be aware of the reason a created user was
139
        automatically activated.
140
        """
141

    
142
        # check preaccepted mail patterns
143
        for pattern in settings.RE_USER_EMAIL_PATTERNS:
144
            if re.match(pattern, user.email):
145
                return 'email'
146

    
147
        # provider automoderate policy is on
148
        if user.get_auth_provider().get_automoderate_policy:
149
            return 'auth_provider_%s' % user.get_auth_provider().module
150

    
151
        return False
152

    
153
    def get_signup_form(self, provider='local', initial_data=None, **kwargs):
154
        """
155
        Returns a form instance for the type of registration the user chosen.
156
        This can be either a LocalUserCreationForm for classic method signups
157
        or ThirdPartyUserCreationForm for users who chosen to signup using a
158
        federated login method.
159
        """
160
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
161
        suffix = 'UserCreationForm'
162
        formclass = getattr(forms, '%s%s' % (main, suffix))
163
        kwargs['provider'] = provider
164
        return formclass(initial_data, **kwargs)
165

    
166
    def prepare_user(self, user, email_verified=None):
167
        """
168
        Initialization of a newly registered user. The method sets email
169
        verification code. If email_verified is set to True we automatically
170
        process user through the verification step.
171
        """
172
        logger.info("Initializing user registration %s", user.log_display)
173

    
174
        if not email_verified:
175
            email_verified = settings.SKIP_EMAIL_VERIFICATION
176

    
177
        user.renew_verification_code()
178
        user.save()
179

    
180
        if email_verified:
181
            logger.info("Auto verifying user email. %s",
182
                        user.log_display)
183
            return self.verify_user(user,
184
                                    user.verification_code)
185

    
186
        return ActivationResult(self.Result.PENDING_VERIFICATION)
187

    
188
    def verify_user(self, user, verification_code):
189
        """
190
        Process user verification using provided verification_code. This
191
        should take place in user activation view. If no moderation is enabled
192
        we automatically process user through activation process.
193
        """
194
        logger.info("Verifying user: %s", user.log_display)
195

    
196
        if user.email_verified:
197
            logger.warning("User email already verified: %s",
198
                           user.log_display)
199
            msg = astakos_messages.ACCOUNT_ALREADY_VERIFIED
200
            return ActivationResult(self.Result.ERROR, msg)
201

    
202
        if user.verification_code and \
203
                user.verification_code == verification_code:
204
            user.email_verified = True
205
            user.verified_at = datetime.datetime.now()
206
            # invalidate previous code
207
            user.renew_verification_code()
208
            user.save()
209
            logger.info("User email verified: %s", user.log_display)
210
        else:
211
            logger.error("User email verification failed "
212
                         "(invalid verification code): %s", user.log_display)
213
            msg = astakos_messages.VERIFICATION_FAILED
214
            return ActivationResult(self.Result.ERROR, msg)
215

    
216
        if not self.moderation_enabled:
217
            logger.warning("User preaccepted (%s): %s", 'auto_moderation',
218
                           user.log_display)
219
            return self.accept_user(user, policy='auto_moderation')
220

    
221
        preaccepted = self._is_preaccepted(user)
222
        if preaccepted:
223
            logger.warning("User preaccepted (%s): %s", preaccepted,
224
                           user.log_display)
225
            return self.accept_user(user, policy=preaccepted)
226

    
227
        if user.moderated:
228
            # set moderated to false because accept_user will return error
229
            # result otherwise.
230
            user.moderated = False
231
            return self.accept_user(user, policy='already_moderated')
232
        else:
233
            return ActivationResult(self.Result.PENDING_MODERATION)
234

    
235
    def accept_user(self, user, policy='manual'):
236
        logger.info("Moderating user: %s", user.log_display)
237
        if user.moderated and user.is_active:
238
            logger.warning("User already accepted, moderation"
239
                           " skipped: %s", user.log_display)
240
            msg = _(astakos_messages.ACCOUNT_ALREADY_MODERATED)
241
            return ActivationResult(self.Result.ERROR, msg)
242

    
243
        if not user.email_verified:
244
            logger.warning("Cannot accept unverified user: %s",
245
                           user.log_display)
246
            msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED)
247
            return ActivationResult(self.Result.ERROR, msg)
248

    
249
        # store a snapshot of user details by the time he
250
        # got accepted.
251
        if not user.accepted_email:
252
            user.accepted_email = user.email
253
        user.accepted_policy = policy
254
        user.moderated = True
255
        user.moderated_at = datetime.datetime.now()
256
        user.moderated_data = json.dumps(user.__dict__,
257
                                         default=lambda obj:
258
                                         str(obj))
259
        user.save()
260
        qh_sync_new_user(user)
261

    
262
        if user.is_rejected:
263
            logger.warning("User has previously been "
264
                           "rejected, reseting rejection state: %s",
265
                           user.log_display)
266
            user.is_rejected = False
267
            user.rejected_at = None
268

    
269
        user.save()
270
        logger.info("User accepted: %s", user.log_display)
271
        self.activate_user(user)
272
        return ActivationResult(self.Result.ACCEPTED)
273

    
274
    def activate_user(self, user):
275
        if not user.email_verified:
276
            msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED)
277
            return ActivationResult(self.Result.ERROR, msg)
278

    
279
        if not user.moderated:
280
            msg = _(astakos_messages.ACCOUNT_NOT_MODERATED)
281
            return ActivationResult(self.Result.ERROR, msg)
282

    
283
        if user.is_rejected:
284
            msg = _(astakos_messages.ACCOUNT_REJECTED)
285
            return ActivationResult(self.Result.ERROR, msg)
286

    
287
        if user.is_active:
288
            msg = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
289
            return ActivationResult(self.Result.ERROR, msg)
290

    
291
        user.is_active = True
292
        user.deactivated_reason = None
293
        user.deactivated_at = None
294
        user.save()
295
        logger.info("User activated: %s", user.log_display)
296
        return ActivationResult(self.Result.ACTIVATED)
297

    
298
    def deactivate_user(self, user, reason=''):
299
        user.is_active = False
300
        user.deactivated_reason = reason
301
        if user.is_active:
302
            user.deactivated_at = datetime.datetime.now()
303
        user.save()
304
        logger.info("User deactivated: %s", user.log_display)
305
        return ActivationResult(self.Result.DEACTIVATED)
306

    
307
    def reject_user(self, user, reason):
308
        logger.info("Rejecting user: %s", user.log_display)
309
        if user.moderated:
310
            logger.warning("User already moderated: %s", user.log_display)
311
            msg = _(astakos_messages.ACCOUNT_ALREADY_MODERATED)
312
            return ActivationResult(self.Result.ERROR, msg)
313

    
314
        if user.is_active:
315
            logger.warning("Cannot reject unverified user: %s",
316
                           user.log_display)
317
            msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED)
318
            return ActivationResult(self.Result.ERROR, msg)
319

    
320
        if not user.email_verified:
321
            logger.warning("Cannot reject unverified user: %s",
322
                           user.log_display)
323
            msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED)
324
            return ActivationResult(self.Result.ERROR, msg)
325

    
326
        user.moderated = True
327
        user.moderated_at = datetime.datetime.now()
328
        user.moderated_data = json.dumps(user.__dict__,
329
                                         default=lambda obj:
330
                                         str(obj))
331
        user.is_rejected = True
332
        user.rejected_reason = reason
333
        user.save()
334
        logger.info("User rejected: %s", user.log_display)
335
        return ActivationResult(self.Result.REJECTED)
336

    
337
    def handle_registration(self, user, email_verified=False):
338
        logger.info("Handling new user registration: %s", user.log_display)
339
        return self.prepare_user(user, email_verified=email_verified)
340

    
341
    def handle_verification(self, user, activation_code):
342
        logger.info("Handling user email verirfication: %s", user.log_display)
343
        return self.verify_user(user, activation_code)
344

    
345
    def handle_moderation(self, user, accept=True, reject_reason=None):
346
        logger.info("Handling user moderation (%r): %s",
347
                    accept, user.log_display)
348
        if accept:
349
            return self.accept_user(user)
350
        else:
351
            return self.reject_user(user, reject_reason)
352

    
353
    def send_user_verification_email(self, user):
354
        if user.is_active:
355
            raise Exception("User already active")
356

    
357
        # invalidate previous code
358
        user.renew_verification_code()
359
        user.save()
360
        functions.send_verification(user)
361
        user.activation_sent = datetime.datetime.now()
362
        user.save()
363

    
364
    def send_result_notifications(self, result, user):
365
        """
366
        Send corresponding notifications based on the status of activation
367
        result.
368

369
        Result.PENDING_VERIRFICATION
370
            * Send user the email verification url
371

372
        Result.PENDING_MODERATION
373
            * Notify admin for account moderation
374

375
        Result.ACCEPTED
376
            * Send user greeting notification
377

378
        Result.REJECTED
379
            * Send nothing
380
        """
381
        if result.status == self.Result.PENDING_VERIFICATION:
382
            logger.info("Sending notifications for user"
383
                        " creation: %s", user.log_display)
384
            # email user that contains the activation link
385
            self.send_user_verification_email(user)
386
            # TODO: optionally notify admins for new accounts
387

    
388
        if result.status == self.Result.PENDING_MODERATION:
389
            logger.info("Sending notifications for user"
390
                        " verification: %s", user.log_display)
391
            functions.send_account_pending_moderation_notification(
392
                user,
393
                self.pending_moderation_template_name)
394
            # TODO: notify user
395

    
396
        if result.status == self.Result.ACCEPTED:
397
            logger.info("Sending notifications for user"
398
                        " moderation: %s", user.log_display)
399
            functions.send_account_activated_notification(
400
                user,
401
                self.activated_email_template_name)
402
            functions.send_greeting(user,
403
                                    self.greeting_template_name)
404
            # TODO: notify admins
405

    
406
        if result.status == self.Result.REJECTED:
407
            logger.info("Sending notifications for user"
408
                        " rejection: %s", user.log_display)
409
            # TODO: notify user and admins
410

    
411

    
412
class InvitationsBackend(ActivationBackend):
413
    """
414
    A activation backend which implements the following workflow: a user
415
    supplies the necessary registation information, if the request contains a
416
    valid inivation code the user is automatically activated otherwise an
417
    inactive user account is created and the user is going to receive an email
418
    as soon as an administrator activates his/her account.
419
    """
420

    
421
    def get_signup_form(self, invitation, provider='local', initial_data=None,
422
                        instance=None):
423
        """
424
        Returns a form instance of the relevant class
425

426
        raises Invitation.DoesNotExist and ValueError if invitation is consumed
427
        or invitation username is reserved.
428
        """
429
        self.invitation = invitation
430
        prefix = 'Invited' if invitation else ''
431
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
432
        suffix = 'UserCreationForm'
433
        formclass = getattr(forms, '%s%s%s' % (prefix, main, suffix))
434
        return formclass(initial_data, instance=instance)
435

    
436
    def get_signup_initial_data(self, request, provider):
437
        """
438
        Returns the necassary activation form depending the user is invited or
439
        not.
440

441
        Throws Invitation.DoesNotExist in case ``code`` is not valid.
442
        """
443
        invitation = self.invitation
444
        initial_data = None
445
        if request.method == 'GET':
446
            if invitation:
447
                # create a tmp user with the invitation realname
448
                # to extract first and last name
449
                u = AstakosUser(realname=invitation.realname)
450
                initial_data = {'email': invitation.username,
451
                                'inviter': invitation.inviter.realname,
452
                                'first_name': u.first_name,
453
                                'last_name': u.last_name,
454
                                'provider': provider}
455
        else:
456
            if provider == request.POST.get('provider', ''):
457
                initial_data = request.POST
458
        return initial_data
459

    
460
    def _is_preaccepted(self, user):
461
        """
462
        Extends _is_preaccepted and if there is a valid, not-consumed
463
        invitation code for the specific user returns True else returns False.
464
        """
465
        preaccepted = super(InvitationsBackend, self)._is_preaccepted(user)
466
        if preaccepted:
467
            return preaccepted
468
        invitation = self.invitation
469
        if not invitation:
470
            if not self.moderation_enabled:
471
                return 'auto_moderation'
472
        if invitation.username == user.email and not invitation.is_consumed:
473
            invitation.consume()
474
            return 'invitation'
475
        return False
476

    
477

    
478
class SimpleBackend(ActivationBackend):
479
    """
480
    The common activation backend.
481
    """
482

    
483
# shortcut
484
ActivationResultStatus = ActivationBackend.Result
485

    
486

    
487
class ActivationResult(object):
488

    
489
    MESSAGE_BY_STATUS = {
490
        ActivationResultStatus.PENDING_VERIFICATION:
491
        _(astakos_messages.VERIFICATION_SENT),
492
        ActivationResultStatus.PENDING_MODERATION:
493
        _(astakos_messages.NOTIFICATION_SENT),
494
        ActivationResultStatus.ACCEPTED:
495
        _(astakos_messages.ACCOUNT_ACTIVATED),
496
        ActivationResultStatus.ACTIVATED:
497
        _(astakos_messages.ACCOUNT_ACTIVATED),
498
        ActivationResultStatus.DEACTIVATED:
499
        _(astakos_messages.ACCOUNT_DEACTIVATED),
500
        ActivationResultStatus.ERROR:
501
        _(astakos_messages.GENERIC_ERROR)
502
    }
503

    
504
    STATUS_DISPLAY = {
505
        ActivationResultStatus.PENDING_VERIFICATION: 'PENDING_VERIFICATION',
506
        ActivationResultStatus.PENDING_MODERATION: 'PENDING_MODERATION',
507
        ActivationResultStatus.ACCEPTED: 'ACCEPTED',
508
        ActivationResultStatus.ACTIVATED: 'ACTIVATED',
509
        ActivationResultStatus.DEACTIVATED: 'DEACTIVATED',
510
        ActivationResultStatus.ERROR: 'ERROR'
511
    }
512

    
513
    def __init__(self, status, message=None):
514
        if message is None:
515
            message = self.MESSAGE_BY_STATUS.get(status)
516

    
517
        self.message = message
518
        self.status = status
519

    
520
    def status_display(self):
521
        return self.STATUS_DISPLAY.get(self.status)
522

    
523
    def __repr__(self):
524
        return "ActivationResult [%s]: %s" % (self.status_display(),
525
                                              self.message)
526

    
527
    def is_error(self):
528
        return self.status == ActivationResultStatus.ERROR