Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / activation_backends.py @ 50f74340

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_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_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
        logger.info("User rejected: %s", user.log_display)
334
        return ActivationResult(self.Result.REJECTED)
335

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

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

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

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

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

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

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

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

374
        Result.ACCEPTED
375
            * Send user greeting notification
376

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

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

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

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

    
410

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

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

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

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

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

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

    
476

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

    
482
# shortcut
483
ActivationResultStatus = ActivationBackend.Result
484

    
485

    
486
class ActivationResult(object):
487

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

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

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

    
516
        self.message = message
517
        self.status = status
518

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

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

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