Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / activation_backends.py @ 2c960473

History | View | Annotate | Download (19.8 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 import models
39
from astakos.im import functions
40
from astakos.im import settings
41
from astakos.im import forms
42

    
43
import astakos.im.messages as astakos_messages
44

    
45
import datetime
46
import logging
47
import re
48
import json
49

    
50
logger = logging.getLogger(__name__)
51

    
52

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

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

    
80

    
81
class ActivationBackend(object):
82
    """
83
    ActivationBackend handles user verification/activation.
84

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

    
102
    verification_template_name = 'im/activation_email.txt'
103
    greeting_template_name = 'im/welcome_email.txt'
104
    pending_moderation_template_name = \
105
        'im/account_pending_moderation_notification.txt'
106
    activated_email_template_name = 'im/account_activated_notification.txt'
107

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

    
124
    def __init__(self, moderation_enabled):
125
        self.moderation_enabled = moderation_enabled
126

    
127
    def _is_preaccepted(self, user):
128
        """
129
        Decide whether user should be automatically moderated. The method gets
130
        called only when self.moderation_enabled is set to True.
131

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

    
138
        # check preaccepted mail patterns
139
        for pattern in settings.RE_USER_EMAIL_PATTERNS:
140
            if re.match(pattern, user.email):
141
                return 'email'
142

    
143
        # provider automoderate policy is on
144
        if user.get_auth_provider().get_automoderate_policy:
145
            return 'auth_provider_%s' % user.get_auth_provider().module
146

    
147
        return False
148

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

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

    
170
        if not email_verified:
171
            email_verified = settings.SKIP_EMAIL_VERIFICATION
172

    
173
        user.renew_verification_code()
174
        user.save()
175

    
176
        if email_verified:
177
            logger.info("Auto verifying user email. %s",
178
                        user.log_display)
179
            return self.verify_user(user,
180
                                    user.verification_code)
181

    
182
        return ActivationResult(self.Result.PENDING_VERIFICATION)
183

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

    
192
        if user.email_verified:
193
            logger.warning("User email already verified: %s",
194
                           user.log_display)
195
            msg = astakos_messages.ACCOUNT_ALREADY_VERIFIED
196
            return ActivationResult(self.Result.ERROR, msg)
197

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

    
212
        if not self.moderation_enabled:
213
            logger.warning("User preaccepted (%s): %s", 'auto_moderation',
214
                           user.log_display)
215
            return self.accept_user(user, policy='auto_moderation')
216

    
217
        preaccepted = self._is_preaccepted(user)
218
        if preaccepted:
219
            logger.warning("User preaccepted (%s): %s", preaccepted,
220
                           user.log_display)
221
            return self.accept_user(user, policy=preaccepted)
222

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

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

    
239
        if not user.email_verified:
240
            logger.warning("Cannot accept unverified user: %s",
241
                           user.log_display)
242
            msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED)
243
            return ActivationResult(self.Result.ERROR, msg)
244

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

    
258
        if user.is_rejected:
259
            logger.warning("User has previously been "
260
                           "rejected, reseting rejection state: %s",
261
                           user.log_display)
262
            user.is_rejected = False
263
            user.rejected_at = None
264

    
265
        user.save()
266
        logger.info("User accepted: %s", user.log_display)
267
        self.activate_user(user)
268
        return ActivationResult(self.Result.ACCEPTED)
269

    
270
    def activate_user(self, user):
271
        if not user.email_verified:
272
            msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED)
273
            return ActivationResult(self.Result.ERROR, msg)
274

    
275
        if not user.moderated:
276
            msg = _(astakos_messages.ACCOUNT_NOT_MODERATED)
277
            return ActivationResult(self.Result.ERROR, msg)
278

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

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

    
287
        user.is_active = True
288
        user.deactivated_reason = None
289
        user.deactivated_at = None
290
        user.save()
291
        logger.info("User activated: %s", user.log_display)
292
        return ActivationResult(self.Result.ACTIVATED)
293

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

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

    
310
        if user.is_active:
311
            logger.warning("Cannot reject unverified user: %s",
312
                           user.log_display)
313
            msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED)
314
            return ActivationResult(self.Result.ERROR, msg)
315

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

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

    
333
    def handle_registration(self, user, email_verified=False):
334
        logger.info("Handling new user registration: %s", user.log_display)
335
        return self.prepare_user(user, email_verified=email_verified)
336

    
337
    def handle_verification(self, user, activation_code):
338
        logger.info("Handling user email verirfication: %s", user.log_display)
339
        return self.verify_user(user, activation_code)
340

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

    
349
    def send_user_verification_email(self, user):
350
        if user.is_active:
351
            raise Exception("User already active")
352

    
353
        # invalidate previous code
354
        user.renew_verification_code()
355
        user.save()
356
        functions.send_verification(user)
357
        user.activation_sent = datetime.datetime.now()
358
        user.save()
359

    
360
    def send_result_notifications(self, result, user):
361
        """
362
        Send corresponding notifications based on the status of activation
363
        result.
364

365
        Result.PENDING_VERIRFICATION
366
            * Send user the email verification url
367

368
        Result.PENDING_MODERATION
369
            * Notify admin for account moderation
370

371
        Result.ACCEPTED
372
            * Send user greeting notification
373

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

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

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

    
402
        if result.status == self.Result.REJECTED:
403
            logger.info("Sending notifications for user"
404
                        " rejection: %s", user.log_display)
405
            # TODO: notify user and admins
406

    
407

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

    
417
    def get_signup_form(self, invitation, provider='local', initial_data=None,
418
                        instance=None):
419
        """
420
        Returns a form instance of the relevant class
421

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

    
432
    def get_signup_initial_data(self, request, provider):
433
        """
434
        Returns the necassary activation form depending the user is invited or
435
        not.
436

437
        Throws Invitation.DoesNotExist in case ``code`` is not valid.
438
        """
439
        invitation = self.invitation
440
        initial_data = None
441
        if request.method == 'GET':
442
            if invitation:
443
                first, last = models.split_realname(invitation.realname)
444
                initial_data = {'email': invitation.username,
445
                                'inviter': invitation.inviter.realname,
446
                                'first_name': first,
447
                                'last_name': last,
448
                                'provider': provider}
449
        else:
450
            if provider == request.POST.get('provider', ''):
451
                initial_data = request.POST
452
        return initial_data
453

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

    
471

    
472
class SimpleBackend(ActivationBackend):
473
    """
474
    The common activation backend.
475
    """
476

    
477
# shortcut
478
ActivationResultStatus = ActivationBackend.Result
479

    
480

    
481
class ActivationResult(object):
482

    
483
    MESSAGE_BY_STATUS = {
484
        ActivationResultStatus.PENDING_VERIFICATION:
485
        _(astakos_messages.VERIFICATION_SENT),
486
        ActivationResultStatus.PENDING_MODERATION:
487
        _(astakos_messages.NOTIFICATION_SENT),
488
        ActivationResultStatus.ACCEPTED:
489
        _(astakos_messages.ACCOUNT_ACTIVATED),
490
        ActivationResultStatus.ACTIVATED:
491
        _(astakos_messages.ACCOUNT_ACTIVATED),
492
        ActivationResultStatus.DEACTIVATED:
493
        _(astakos_messages.ACCOUNT_DEACTIVATED),
494
        ActivationResultStatus.ERROR:
495
        _(astakos_messages.GENERIC_ERROR)
496
    }
497

    
498
    STATUS_DISPLAY = {
499
        ActivationResultStatus.PENDING_VERIFICATION: 'PENDING_VERIFICATION',
500
        ActivationResultStatus.PENDING_MODERATION: 'PENDING_MODERATION',
501
        ActivationResultStatus.ACCEPTED: 'ACCEPTED',
502
        ActivationResultStatus.ACTIVATED: 'ACTIVATED',
503
        ActivationResultStatus.DEACTIVATED: 'DEACTIVATED',
504
        ActivationResultStatus.ERROR: 'ERROR'
505
    }
506

    
507
    def __init__(self, status, message=None):
508
        if message is None:
509
            message = self.MESSAGE_BY_STATUS.get(status)
510

    
511
        self.message = message
512
        self.status = status
513

    
514
    def status_display(self):
515
        return self.STATUS_DISPLAY.get(self.status)
516

    
517
    def __repr__(self):
518
        return "ActivationResult [%s]: %s" % (self.status_display(),
519
                                              self.message)
520

    
521
    def is_error(self):
522
        return self.status == ActivationResultStatus.ERROR