Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / activation_backends.py @ 0d48fd8f

History | View | Annotate | Download (20.1 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, qh_sync_users,
45
                               register_pending_apps)
46

    
47
import astakos.im.messages as astakos_messages
48

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

    
54
logger = logging.getLogger(__name__)
55

    
56

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

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

    
84

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

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

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

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

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

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

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

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

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

    
153
        return False
154

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

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

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

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

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

    
188
        return ActivationResult(self.Result.PENDING_VERIFICATION)
189

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
316
        if user.is_active:
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
        if not user.email_verified:
323
            logger.warning("Cannot reject unverified user: %s",
324
                           user.log_display)
325
            msg = _(astakos_messages.ACCOUNT_NOT_VERIFIED)
326
            return ActivationResult(self.Result.ERROR, msg)
327

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

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

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

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

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

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

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

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

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

376
        Result.ACCEPTED
377
            * Send user greeting notification
378

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

    
389
        if result.status == self.Result.PENDING_MODERATION:
390
            logger.info("Sending notifications for user"
391
                        " verification: %s", user.log_display)
392
            functions.send_account_pending_moderation_notification(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(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