Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ e6759494

History | View | Annotate | Download (16.1 kB)

1
# Copyright 2011-2012 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
import hashlib
35
import uuid
36
import logging
37
import json
38

    
39
from time import asctime
40
from datetime import datetime, timedelta
41
from base64 import b64encode
42
from urlparse import urlparse
43
from random import randint
44

    
45
from django.db import models, IntegrityError
46
from django.contrib.auth.models import User, UserManager, Group
47
from django.utils.translation import ugettext as _
48
from django.core.exceptions import ValidationError
49
from django.template.loader import render_to_string
50
from django.core.mail import send_mail
51
from django.db import transaction
52
from django.db.models.signals import post_save, post_syncdb
53
from django.db.models import Q
54

    
55
from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \
56
    AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME, \
57
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
58

    
59
QUEUE_CLIENT_ID = 3 # Astakos.
60

    
61
logger = logging.getLogger(__name__)
62

    
63
class AstakosUser(User):
64
    """
65
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
66
    """
67
    # Use UserManager to get the create_user method, etc.
68
    objects = UserManager()
69

    
70
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
71
    provider = models.CharField('Provider', max_length=255, blank=True)
72

    
73
    #for invitations
74
    user_level = DEFAULT_USER_LEVEL
75
    level = models.IntegerField('Inviter level', default=user_level)
76
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
77

    
78
    auth_token = models.CharField('Authentication Token', max_length=32,
79
                                  null=True, blank=True)
80
    auth_token_created = models.DateTimeField('Token creation date', null=True)
81
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
82

    
83
    updated = models.DateTimeField('Update date')
84
    is_verified = models.BooleanField('Is verified?', default=False)
85

    
86
    # ex. screen_name for twitter, eppn for shibboleth
87
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
88

    
89
    email_verified = models.BooleanField('Email verified?', default=False)
90

    
91
    has_credits = models.BooleanField('Has credits?', default=False)
92
    has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
93
    date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
94
    
95
    activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True)
96
    
97
    __has_signed_terms = False
98
    __groupnames = []
99
    
100
    class Meta:
101
        unique_together = ("provider", "third_party_identifier")
102
    
103
    def __init__(self, *args, **kwargs):
104
        super(AstakosUser, self).__init__(*args, **kwargs)
105
        self.__has_signed_terms = self.has_signed_terms
106
        if self.id:
107
            self.__groupnames = [g.name for g in self.groups.all()]
108
        else:
109
            self.is_active = False
110
    
111
    @property
112
    def realname(self):
113
        return '%s %s' %(self.first_name, self.last_name)
114

    
115
    @realname.setter
116
    def realname(self, value):
117
        parts = value.split(' ')
118
        if len(parts) == 2:
119
            self.first_name = parts[0]
120
            self.last_name = parts[1]
121
        else:
122
            self.last_name = parts[0]
123

    
124
    @property
125
    def invitation(self):
126
        try:
127
            return Invitation.objects.get(username=self.email)
128
        except Invitation.DoesNotExist:
129
            return None
130

    
131
    def save(self, update_timestamps=True, **kwargs):
132
        if update_timestamps:
133
            if not self.id:
134
                self.date_joined = datetime.now()
135
            self.updated = datetime.now()
136
        
137
        # update date_signed_terms if necessary
138
        if self.__has_signed_terms != self.has_signed_terms:
139
            self.date_signed_terms = datetime.now()
140
        
141
        if not self.id:
142
            # set username
143
            while not self.username:
144
                username =  uuid.uuid4().hex[:30]
145
                try:
146
                    AstakosUser.objects.get(username = username)
147
                except AstakosUser.DoesNotExist, e:
148
                    self.username = username
149
            if not self.provider:
150
                self.provider = 'local'
151
        report_user_event(self)
152
        self.validate_unique_email_isactive()
153
        if self.is_active and self.activation_sent:
154
            # reset the activation sent
155
            self.activation_sent = None
156
        super(AstakosUser, self).save(**kwargs)
157
        
158
        # set default group if does not exist
159
        groupname = 'default'
160
        if groupname not in self.__groupnames:
161
            try:
162
                group = Group.objects.get(name = groupname)
163
                self.groups.add(group)
164
            except Group.DoesNotExist, e:
165
                logger.exception(e)
166
    
167
    def renew_token(self):
168
        md5 = hashlib.md5()
169
        md5.update(self.username)
170
        md5.update(self.realname.encode('ascii', 'ignore'))
171
        md5.update(asctime())
172

    
173
        self.auth_token = b64encode(md5.digest())
174
        self.auth_token_created = datetime.now()
175
        self.auth_token_expires = self.auth_token_created + \
176
                                  timedelta(hours=AUTH_TOKEN_DURATION)
177
        msg = 'Token renewed for %s' % self.email
178
        logger._log(LOGGING_LEVEL, msg, [])
179

    
180
    def __unicode__(self):
181
        return self.username
182
    
183
    def conflicting_email(self):
184
        q = AstakosUser.objects.exclude(username = self.username)
185
        q = q.filter(email = self.email)
186
        if q.count() != 0:
187
            return True
188
        return False
189
    
190
    def validate_unique_email_isactive(self):
191
        """
192
        Implements a unique_together constraint for email and is_active fields.
193
        """
194
        q = AstakosUser.objects.all()
195
        q = q.filter(email = self.email)
196
        q = q.filter(is_active = self.is_active)
197
        if self.id:
198
            q = q.filter(~Q(id = self.id))
199
        if q.count() != 0:
200
            raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
201
    
202
    def signed_terms(self):
203
        term = get_latest_terms()
204
        if not term:
205
            return True
206
        if not self.has_signed_terms:
207
            return False
208
        if not self.date_signed_terms:
209
            return False
210
        if self.date_signed_terms < term.date:
211
            self.has_signed_terms = False
212
            self.date_signed_terms = None
213
            self.save()
214
            return False
215
        return True
216

    
217
class ApprovalTerms(models.Model):
218
    """
219
    Model for approval terms
220
    """
221

    
222
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
223
    location = models.CharField('Terms location', max_length=255)
224

    
225
class Invitation(models.Model):
226
    """
227
    Model for registring invitations
228
    """
229
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
230
                                null=True)
231
    realname = models.CharField('Real name', max_length=255)
232
    username = models.CharField('Unique ID', max_length=255, unique=True)
233
    code = models.BigIntegerField('Invitation code', db_index=True)
234
    is_consumed = models.BooleanField('Consumed?', default=False)
235
    created = models.DateTimeField('Creation date', auto_now_add=True)
236
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
237
    
238
    def __init__(self, *args, **kwargs):
239
        super(Invitation, self).__init__(*args, **kwargs)
240
        if not self.id:
241
            self.code = _generate_invitation_code()
242
    
243
    def consume(self):
244
        self.is_consumed = True
245
        self.consumed = datetime.now()
246
        self.save()
247

    
248
    def __unicode__(self):
249
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
250

    
251
def report_user_event(user):
252
    def should_send(user):
253
        # report event incase of new user instance
254
        # or if specific fields are modified
255
        if not user.id:
256
            return True
257
        db_instance = AstakosUser.objects.get(id = user.id)
258
        for f in BILLING_FIELDS:
259
            if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
260
                return True
261
        return False
262

    
263
    if QUEUE_CONNECTION and should_send(user):
264

    
265
        from astakos.im.queue.userevent import UserEvent
266
        from synnefo.lib.queue import exchange_connect, exchange_send, \
267
                exchange_close
268

    
269
        eventType = 'create' if not user.id else 'modify'
270
        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
271
        conn = exchange_connect(QUEUE_CONNECTION)
272
        parts = urlparse(QUEUE_CONNECTION)
273
        exchange = parts.path[1:]
274
        routing_key = '%s.user' % exchange
275
        exchange_send(conn, routing_key, body)
276
        exchange_close(conn)
277

    
278
def _generate_invitation_code():
279
    while True:
280
        code = randint(1, 2L**63 - 1)
281
        try:
282
            Invitation.objects.get(code=code)
283
            # An invitation with this code already exists, try again
284
        except Invitation.DoesNotExist:
285
            return code
286

    
287
def get_latest_terms():
288
    try:
289
        term = ApprovalTerms.objects.order_by('-id')[0]
290
        return term
291
    except IndexError:
292
        pass
293
    return None
294

    
295
class EmailChangeManager(models.Manager):
296
    @transaction.commit_on_success
297
    def change_email(self, activation_key):
298
        """
299
        Validate an activation key and change the corresponding
300
        ``User`` if valid.
301

302
        If the key is valid and has not expired, return the ``User``
303
        after activating.
304

305
        If the key is not valid or has expired, return ``None``.
306

307
        If the key is valid but the ``User`` is already active,
308
        return ``None``.
309

310
        After successful email change the activation record is deleted.
311

312
        Throws ValueError if there is already
313
        """
314
        try:
315
            email_change = self.model.objects.get(activation_key=activation_key)
316
            if email_change.activation_key_expired():
317
                email_change.delete()
318
                raise EmailChange.DoesNotExist
319
            # is there an active user with this address?
320
            try:
321
                AstakosUser.objects.get(email=email_change.new_email_address)
322
            except AstakosUser.DoesNotExist:
323
                pass
324
            else:
325
                raise ValueError(_('The new email address is reserved.'))
326
            # update user
327
            user = AstakosUser.objects.get(pk=email_change.user_id)
328
            user.email = email_change.new_email_address
329
            user.save()
330
            email_change.delete()
331
            return user
332
        except EmailChange.DoesNotExist:
333
            raise ValueError(_('Invalid activation key'))
334

    
335
class EmailChange(models.Model):
336
    new_email_address = models.EmailField(_(u'new e-mail address'), help_text=_(u'Your old email address will be used until you verify your new one.'))
337
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
338
    requested_at = models.DateTimeField(default=datetime.now())
339
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
340

    
341
    objects = EmailChangeManager()
342

    
343
    def activation_key_expired(self):
344
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
345
        return self.requested_at + expiration_date < datetime.now()
346

    
347
class Service(models.Model):
348
    name = models.CharField('Name', max_length=255, unique=True)
349
    url = models.FilePathField()
350
    icon = models.FilePathField(blank=True)
351
    auth_token = models.CharField('Authentication Token', max_length=32,
352
                                  null=True, blank=True)
353
    auth_token_created = models.DateTimeField('Token creation date', null=True)
354
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
355
    
356
    def save(self, **kwargs):
357
        if not self.id:
358
            self.renew_token()
359
        self.full_clean()
360
        super(Service, self).save(**kwargs)
361
    
362
    def renew_token(self):
363
        md5 = hashlib.md5()
364
        md5.update(self.name.encode('ascii', 'ignore'))
365
        md5.update(self.url.encode('ascii', 'ignore'))
366
        md5.update(asctime())
367

    
368
        self.auth_token = b64encode(md5.digest())
369
        self.auth_token_created = datetime.now()
370
        self.auth_token_expires = self.auth_token_created + \
371
                                  timedelta(hours=AUTH_TOKEN_DURATION)
372

    
373
class AdditionalMail(models.Model):
374
    """
375
    Model for registring invitations
376
    """
377
    owner = models.ForeignKey(AstakosUser)
378
    email = models.EmailField()
379

    
380
class PendingThirdPartyUser(models.Model):
381
    """
382
    Model for registring successful third party user authentications
383
    """
384
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
385
    provider = models.CharField('Provider', max_length=255, blank=True)
386
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
387
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
388
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
389
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
390
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
391
    
392
    class Meta:
393
        unique_together = ("provider", "third_party_identifier")
394

    
395
    @property
396
    def realname(self):
397
        return '%s %s' %(self.first_name, self.last_name)
398

    
399
    @realname.setter
400
    def realname(self, value):
401
        parts = value.split(' ')
402
        if len(parts) == 2:
403
            self.first_name = parts[0]
404
            self.last_name = parts[1]
405
        else:
406
            self.last_name = parts[0]
407
    
408
    def save(self, **kwargs):
409
        if not self.id:
410
            # set username
411
            while not self.username:
412
                username =  uuid.uuid4().hex[:30]
413
                try:
414
                    AstakosUser.objects.get(username = username)
415
                except AstakosUser.DoesNotExist, e:
416
                    self.username = username
417
        super(PendingThirdPartyUser, self).save(**kwargs)
418

    
419
def create_astakos_user(u):
420
    try:
421
        AstakosUser.objects.get(user_ptr=u.pk)
422
    except AstakosUser.DoesNotExist:
423
        extended_user = AstakosUser(user_ptr_id=u.pk)
424
        extended_user.__dict__.update(u.__dict__)
425
        extended_user.renew_token()
426
        extended_user.save()
427
    except:
428
        pass
429

    
430
def superuser_post_syncdb(sender, **kwargs):
431
    # if there was created a superuser
432
    # associate it with an AstakosUser
433
    admins = User.objects.filter(is_superuser=True)
434
    for u in admins:
435
        create_astakos_user(u)
436

    
437
post_syncdb.connect(superuser_post_syncdb)
438

    
439
def superuser_post_save(sender, instance, **kwargs):
440
    if instance.is_superuser:
441
        create_astakos_user(instance)
442

    
443
post_save.connect(superuser_post_save, sender=User)