Revision e7cb4085 snf-astakos-app/astakos/im/activation_backends.py

b/snf-astakos-app/astakos/im/activation_backends.py
37 37

  
38 38
from astakos.im.models import AstakosUser
39 39
from astakos.im.util import get_invitation
40
from astakos.im.functions import (
41
    send_activation, send_account_creation_notification, activate)
42
from astakos.im.settings import (
43
    INVITATIONS_ENABLED, RE_USER_EMAIL_PATTERNS)
44
from astakos.im import settings as astakos_settings
45
from astakos.im.forms import *
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 46

  
47 47
import astakos.im.messages as astakos_messages
48 48

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

  
52 54
logger = logging.getLogger(__name__)
53 55

  
54 56

  
55
def get_backend(request):
57
def get_backend():
56 58
    """
57 59
    Returns an instance of an activation backend,
58 60
    according to the INVITATIONS_ENABLED setting
......
64 66
    ``django.core.exceptions.ImproperlyConfigured`` is raised.
65 67
    """
66 68
    module = 'astakos.im.activation_backends'
67
    prefix = 'Invitations' if INVITATIONS_ENABLED else 'Simple'
69
    prefix = 'Invitations' if settings.INVITATIONS_ENABLED else 'Simple'
68 70
    backend_class_name = '%sBackend' % prefix
69 71
    try:
70 72
        mod = import_module(module)
......
77 79
        raise ImproperlyConfigured(
78 80
            'Module "%s" does not define a activation backend named "%s"' % (
79 81
                module, backend_class_name))
80
    return backend_class(request)
82
    return backend_class(settings.MODERATION_ENABLED)
81 83

  
82 84

  
83 85
class ActivationBackend(object):
84
    def __init__(self, request):
85
        self.request = request
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
86 132

  
87 133
    def _is_preaccepted(self, user):
88
        # return True if user email matches specific patterns
89
        for pattern in RE_USER_EMAIL_PATTERNS:
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:
90 146
            if re.match(pattern, user.email):
91
                return True
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

  
92 153
        return False
93 154

  
94
    def get_signup_form(self, provider='local', instance=None):
155
    def get_signup_form(self, provider='local', initial_data=None, **kwargs):
95 156
        """
96
        Returns a form instance of the relevant class
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.
97 161
        """
98 162
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
99 163
        suffix = 'UserCreationForm'
100
        formclass = '%s%s' % (main, suffix)
101
        request = self.request
102
        initial_data = None
103
        if request.method == 'POST':
104
            if provider == request.POST.get('provider', ''):
105
                initial_data = request.POST
106
        return globals()[formclass](initial_data, instance=instance, request=request)
107

  
108
    def handle_activation(
109
        self, user, activation_template_name='im/activation_email.txt',
110
        greeting_template_name='im/welcome_email.txt',
111
        admin_email_template_name='im/account_creation_notification.txt',
112
        helpdesk_email_template_name='im/helpdesk_notification.txt'
113
    ):
114
        """
115
        If the user is already active returns immediately.
116
        If the user is preaccepted and the email is verified, the account is
117
        activated automatically. Otherwise, if the email is not verified,
118
        it sends a verification email to the user.
119
        If the user is not preaccepted, it sends an email to the administrators
120
        and informs the user that the account is pending activation.
121
        """
122
        try:
123
            if user.is_active:
124
                return RegistationCompleted()
125

  
126
            if self._is_preaccepted(user):
127
                if user.email_verified:
128
                    activate(
129
                        user,
130
                        greeting_template_name,
131
                        helpdesk_email_template_name
132
                    )
133
                    return RegistationCompleted()
134
                else:
135
                    send_activation(
136
                        user,
137
                        activation_template_name
138
                    )
139
                    return VerificationSent()
140
            else:
141
                send_account_creation_notification(
142
                    template_name=admin_email_template_name,
143
                    dictionary={'user': user, 'group_creation': True}
144
                )
145
                return NotificationSent()
146
        except BaseException, e:
147
            logger.exception(e)
148
            raise e
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
149 409

  
150 410

  
151 411
class InvitationsBackend(ActivationBackend):
152 412
    """
153 413
    A activation backend which implements the following workflow: a user
154
    supplies the necessary registation information, if the request contains a valid
155
    inivation code the user is automatically activated otherwise an inactive user
156
    account is created and the user is going to receive an email as soon as an
157
    administrator activates his/her account.
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.
158 418
    """
159 419

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

  
164 425
        raises Invitation.DoesNotExist and ValueError if invitation is consumed
165 426
        or invitation username is reserved.
166 427
        """
167
        self.invitation = get_invitation(self.request)
168
        invitation = self.invitation
169
        initial_data = self.get_signup_initial_data(provider)
428
        self.invitation = invitation
170 429
        prefix = 'Invited' if invitation else ''
171 430
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
172 431
        suffix = 'UserCreationForm'
173
        formclass = '%s%s%s' % (prefix, main, suffix)
174
        return globals()[formclass](initial_data, instance=instance, request=self.request)
432
        formclass = getattr(forms, '%s%s%s' % (prefix, main, suffix))
433
        return formclass(initial_data, instance=instance)
175 434

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

  
180 440
        Throws Invitation.DoesNotExist in case ``code`` is not valid.
181 441
        """
182
        request = self.request
183 442
        invitation = self.invitation
184 443
        initial_data = None
185 444
        if request.method == 'GET':
......
199 458

  
200 459
    def _is_preaccepted(self, user):
201 460
        """
202
        Extends _is_preaccepted and if there is a valid, not-consumed invitation
203
        code for the specific user returns True else returns False.
461
        Extends _is_preaccepted and if there is a valid, not-consumed
462
        invitation code for the specific user returns True else returns False.
204 463
        """
205
        if super(InvitationsBackend, self)._is_preaccepted(user):
206
            return True
464
        preaccepted = super(InvitationsBackend, self)._is_preaccepted(user)
465
        if preaccepted:
466
            return preaccepted
207 467
        invitation = self.invitation
208 468
        if not invitation:
209
            return not astakos_settings.MODERATION_ENABLED
469
            if not self.moderation_enabled:
470
                return 'auto_moderation'
210 471
        if invitation.username == user.email and not invitation.is_consumed:
211 472
            invitation.consume()
212
            return True
473
            return 'invitation'
213 474
        return False
214 475

  
215 476

  
216 477
class SimpleBackend(ActivationBackend):
217 478
    """
218
    A activation backend which implements the following workflow: a user
219
    supplies the necessary registation information, an incative user account is
220
    created and receives an email in order to activate his/her account.
479
    The common activation backend.
221 480
    """
222
    def _is_preaccepted(self, user):
223
        if super(SimpleBackend, self)._is_preaccepted(user):
224
            return True
225 481

  
226
        return user.get_auth_provider().get_automoderate_policy
482
# shortcut
483
ActivationResultStatus = ActivationBackend.Result
227 484

  
228 485

  
229 486
class ActivationResult(object):
230
    def __init__(self, message):
231
        self.message = message
232 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
    }
233 511

  
234
class VerificationSent(ActivationResult):
235
    def __init__(self):
236
        message = _(astakos_messages.VERIFICATION_SENT)
237
        super(VerificationSent, self).__init__(message)
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
238 518

  
239
class NotificationSent(ActivationResult):
240
    def __init__(self):
241
        message = _(astakos_messages.NOTIFICATION_SENT)
242
        super(NotificationSent, self).__init__(message)
519
    def status_display(self):
520
        return self.STATUS_DISPLAY.get(self.status)
243 521

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

  
245
class RegistationCompleted(ActivationResult):
246
    def __init__(self):
247
        message = _(astakos_messages.REGISTRATION_COMPLETED)
248
        super(RegistationCompleted, self).__init__(message)
526
    def is_error(self):
527
        return self.status == ActivationResultStatus.ERROR

Also available in: Unified diff