Statistics
| Branch: | Tag: | Revision:

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

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.util import get_invitation
40
from astakos.im import functions
41
from astakos.im import settings
42
from astakos.im import forms
43

    
44
from astakos.im.quotas import qh_sync_user
45

    
46
import astakos.im.messages as astakos_messages
47

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

    
53
logger = logging.getLogger(__name__)
54

    
55

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

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

    
83

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

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

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

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

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

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

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

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

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

    
152
        return False
153

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

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

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

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

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

    
187
        return ActivationResult(self.Result.PENDING_VERIFICATION)
188

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
327
        user.moderated = True
328
        user.moderated_at = datetime.datetime.now()
329
        user.moderated_data = json.dumps(user.__dict__,
330
                                         default=lambda obj:
331
                                         str(obj))
332
        user.is_rejected = True
333
        user.rejected_reason = reason
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(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(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
                # create a tmp user with the invitation realname
446
                # to extract first and last name
447
                u = AstakosUser(realname=invitation.realname)
448
                initial_data = {'email': invitation.username,
449
                                'inviter': invitation.inviter.realname,
450
                                'first_name': u.first_name,
451
                                'last_name': u.last_name,
452
                                'provider': provider}
453
        else:
454
            if provider == request.POST.get('provider', ''):
455
                initial_data = request.POST
456
        return initial_data
457

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

    
475

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

    
481
# shortcut
482
ActivationResultStatus = ActivationBackend.Result
483

    
484

    
485
class ActivationResult(object):
486

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

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

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

    
515
        self.message = message
516
        self.status = status
517

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

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

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