Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / activation_backends.py @ 56bbece7

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
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.create_user()
94
    >>>     activation = backend.handle_registration(user)
95
    >>>     # activation.status is one of backend.Result.{*} activation result
96
    >>>     # types
97
    >>>
98
    >>>     # sending activation notifications is not done automatically
99
    >>>     # we need to call send_result_notifications
100
    >>>     backend.send_result_notifications(activation)
101
    >>>     return HttpResponse(activation.message)
102
    """
103

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

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

    
126
    def __init__(self, moderation_enabled):
127
        self.moderation_enabled = moderation_enabled
128

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

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

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

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

    
149
        return False
150

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

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

    
172
        if not email_verified:
173
            email_verified = settings.SKIP_EMAIL_VERIFICATION
174

    
175
        user.renew_verification_code()
176
        user.save()
177

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

    
184
        return ActivationResult(self.Result.PENDING_VERIFICATION)
185

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

367
        Result.PENDING_VERIRFICATION
368
            * Send user the email verification url
369

370
        Result.PENDING_MODERATION
371
            * Notify admin for account moderation
372

373
        Result.ACCEPTED
374
            * Send user greeting notification
375

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

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

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

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

    
409

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

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

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

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

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

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

    
473

    
474
class SimpleBackend(ActivationBackend):
475
    """
476
    The common activation backend.
477
    """
478

    
479
# shortcut
480
ActivationResultStatus = ActivationBackend.Result
481

    
482

    
483
class ActivationResult(object):
484

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

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

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

    
513
        self.message = message
514
        self.status = status
515

    
516
    def status_display(self):
517
        return self.STATUS_DISPLAY.get(self.status)
518

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

    
523
    def is_error(self):
524
        return self.status == ActivationResultStatus.ERROR