Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 304acb60

History | View | Annotate | Download (20 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, urlunparse
43
from random import randint
44
from collections import defaultdict
45
from south.signals import post_migrate
46

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

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

    
61
QUEUE_CLIENT_ID = 3 # Astakos.
62

    
63
logger = logging.getLogger(__name__)
64

    
65
class Service(models.Model):
66
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
67
    url = models.FilePathField()
68
    icon = models.FilePathField(blank=True)
69
    auth_token = models.CharField('Authentication Token', max_length=32,
70
                                  null=True, blank=True)
71
    auth_token_created = models.DateTimeField('Token creation date', null=True)
72
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
73
    
74
    def save(self, **kwargs):
75
        if not self.id:
76
            self.renew_token()
77
        self.full_clean()
78
        super(Service, self).save(**kwargs)
79
    
80
    def renew_token(self):
81
        md5 = hashlib.md5()
82
        md5.update(self.name.encode('ascii', 'ignore'))
83
        md5.update(self.url.encode('ascii', 'ignore'))
84
        md5.update(asctime())
85

    
86
        self.auth_token = b64encode(md5.digest())
87
        self.auth_token_created = datetime.now()
88
        self.auth_token_expires = self.auth_token_created + \
89
                                  timedelta(hours=AUTH_TOKEN_DURATION)
90
    
91
    def __str__(self):
92
        return self.name
93

    
94
class ResourceMetadata(models.Model):
95
    key = models.CharField('Name', max_length=255, unique=True, db_index=True)
96
    value = models.CharField('Value', max_length=255)
97

    
98
class Resource(models.Model):
99
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
100
    meta = models.ManyToManyField(ResourceMetadata)
101
    service = models.ForeignKey(Service)
102
    
103
    def __str__(self):
104
        return '%s : %s' % (self.service, self.name)
105

    
106
class GroupKind(models.Model):
107
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
108
    
109
    def __str__(self):
110
        return self.name
111

    
112
class AstakosGroup(Group):
113
    kind = models.ForeignKey(GroupKind)
114
    desc = models.TextField('Description', null=True)
115
    policy = models.ManyToManyField(Resource, null=True, blank=True, through='AstakosGroupQuota')
116
    creation_date = models.DateTimeField('Creation date', default=datetime.now())
117
    issue_date = models.DateTimeField('Issue date', null=True)
118
    expiration_date = models.DateTimeField('Expiration date', null=True)
119
    moderation_enabled = models.BooleanField('Moderated membership?', default=True)
120
    approval_date = models.DateTimeField('Activation date', null=True, blank=True)
121
    estimated_participants = models.PositiveIntegerField('Estimated #participants', null=True)
122
    
123
    @property
124
    def is_disabled(self):
125
        if not self.approval_date:
126
            return True
127
        return False
128
    
129
    @property
130
    def is_enabled(self):
131
        if self.is_disabled:
132
            return False
133
        if not self.issue_date:
134
            return False
135
        if not self.expiration_date:
136
            return True
137
        now = datetime.now()
138
        if self.issue_date > now:
139
            return False
140
        if now >= self.expiration_date:
141
            return False
142
        return True
143
    
144
    @property
145
    def participants(self):
146
        return len(self.approved_members)
147
    
148
    def enable(self):
149
        self.approval_date = datetime.now()
150
        self.save()
151
    
152
    def disable(self):
153
        self.approval_date = None
154
        self.save()
155
    
156
    def approve_member(self, person):
157
        try:
158
            self.membership_set.create(person=person, date_joined=datetime.now())
159
        except IntegrityError:
160
            m = self.membership_set.get(person=person)
161
            m.date_joined = datetime.now()
162
            m.save()
163
    
164
    def disapprove_member(self, person):
165
        self.membership_set.remove(person=person)
166
    
167
    @property
168
    def members(self):
169
        return map(lambda m:m.person, self.membership_set.all())
170
    
171
    @property
172
    def approved_members(self):
173
        f = filter(lambda m:m.is_approved, self.membership_set.all())
174
        return map(lambda m:m.person, f)
175
    
176
    @property
177
    def quota(self):
178
        d = {}
179
        for q in  self.astakosgroupquota_set.all():
180
            d[q.resource.name] = q.limit
181
        return d
182
    
183
    @property
184
    def has_undefined_policies(self):
185
        # TODO: can avoid query?
186
        return Resource.objects.filter(~Q(astakosgroup=self)).exists()
187

    
188
class AstakosUser(User):
189
    """
190
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
191
    """
192
    # Use UserManager to get the create_user method, etc.
193
    objects = UserManager()
194

    
195
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
196
    provider = models.CharField('Provider', max_length=255, blank=True)
197

    
198
    #for invitations
199
    user_level = DEFAULT_USER_LEVEL
200
    level = models.IntegerField('Inviter level', default=user_level)
201
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
202

    
203
    auth_token = models.CharField('Authentication Token', max_length=32,
204
                                  null=True, blank=True)
205
    auth_token_created = models.DateTimeField('Token creation date', null=True)
206
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
207

    
208
    updated = models.DateTimeField('Update date')
209
    is_verified = models.BooleanField('Is verified?', default=False)
210

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

    
214
    email_verified = models.BooleanField('Email verified?', default=False)
215

    
216
    has_credits = models.BooleanField('Has credits?', default=False)
217
    has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
218
    date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
219
    
220
    activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True)
221
    
222
    policy = models.ManyToManyField(Resource, null=True, through='AstakosUserQuota')
223
    
224
    astakos_groups = models.ManyToManyField(AstakosGroup, verbose_name=_('agroups'), blank=True,
225
        help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
226
        through='Membership')
227
    
228
    __has_signed_terms = False
229
    __groupnames = []
230
    
231
    owner = models.ManyToManyField(AstakosGroup, related_name='owner', null=True)
232
    
233
    class Meta:
234
        unique_together = ("provider", "third_party_identifier")
235
    
236
    def __init__(self, *args, **kwargs):
237
        super(AstakosUser, self).__init__(*args, **kwargs)
238
        self.__has_signed_terms = self.has_signed_terms
239
        if self.id:
240
            self.__groupnames = [g.name for g in self.astakos_groups.all()]
241
        else:
242
            self.is_active = False
243
    
244
    @property
245
    def realname(self):
246
        return '%s %s' %(self.first_name, self.last_name)
247

    
248
    @realname.setter
249
    def realname(self, value):
250
        parts = value.split(' ')
251
        if len(parts) == 2:
252
            self.first_name = parts[0]
253
            self.last_name = parts[1]
254
        else:
255
            self.last_name = parts[0]
256

    
257
    @property
258
    def invitation(self):
259
        try:
260
            return Invitation.objects.get(username=self.email)
261
        except Invitation.DoesNotExist:
262
            return None
263
    
264
    @property
265
    def quota(self):
266
        d = defaultdict(int)
267
        for q in  self.astakosuserquota_set.all():
268
            d[q.resource.name] += q.limit
269
        for g in self.astakos_groups.all():
270
            if not g.is_enabled:
271
                continue
272
            for r, limit in g.quota.iteritems():
273
                d[r] += limit
274
        # TODO set default for remaining
275
        return d
276
        
277
    def save(self, update_timestamps=True, **kwargs):
278
        if update_timestamps:
279
            if not self.id:
280
                self.date_joined = datetime.now()
281
            self.updated = datetime.now()
282
        
283
        # update date_signed_terms if necessary
284
        if self.__has_signed_terms != self.has_signed_terms:
285
            self.date_signed_terms = datetime.now()
286
        
287
        if not self.id:
288
            # set username
289
            while not self.username:
290
                username =  uuid.uuid4().hex[:30]
291
                try:
292
                    AstakosUser.objects.get(username = username)
293
                except AstakosUser.DoesNotExist, e:
294
                    self.username = username
295
            if not self.provider:
296
                self.provider = 'local'
297
        report_user_event(self)
298
        self.validate_unique_email_isactive()
299
        if self.is_active and self.activation_sent:
300
            # reset the activation sent
301
            self.activation_sent = None
302
        
303
        super(AstakosUser, self).save(**kwargs)
304
        
305
        # set group if does not exist
306
        groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
307
        if groupname not in self.__groupnames:
308
            try:
309
                group = AstakosGroup.objects.get(name = groupname)
310
                Membership(group=group, person=self, date_joined=datetime.now()).save()
311
            except AstakosGroup.DoesNotExist, e:
312
                logger.exception(e)
313
    
314
    def renew_token(self):
315
        md5 = hashlib.md5()
316
        md5.update(self.username)
317
        md5.update(self.realname.encode('ascii', 'ignore'))
318
        md5.update(asctime())
319

    
320
        self.auth_token = b64encode(md5.digest())
321
        self.auth_token_created = datetime.now()
322
        self.auth_token_expires = self.auth_token_created + \
323
                                  timedelta(hours=AUTH_TOKEN_DURATION)
324
        msg = 'Token renewed for %s' % self.email
325
        logger._log(LOGGING_LEVEL, msg, [])
326

    
327
    def __unicode__(self):
328
        return self.username
329
    
330
    def conflicting_email(self):
331
        q = AstakosUser.objects.exclude(username = self.username)
332
        q = q.filter(email = self.email)
333
        if q.count() != 0:
334
            return True
335
        return False
336
    
337
    def validate_unique_email_isactive(self):
338
        """
339
        Implements a unique_together constraint for email and is_active fields.
340
        """
341
        q = AstakosUser.objects.exclude(username = self.username)
342
        q = q.filter(email = self.email)
343
        q = q.filter(is_active = self.is_active)
344
        if q.count() != 0:
345
            raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
346
    
347
    def signed_terms(self):
348
        term = get_latest_terms()
349
        if not term:
350
            return True
351
        if not self.has_signed_terms:
352
            return False
353
        if not self.date_signed_terms:
354
            return False
355
        if self.date_signed_terms < term.date:
356
            self.has_signed_terms = False
357
            self.date_signed_terms = None
358
            self.save()
359
            return False
360
        return True
361

    
362
class Membership(models.Model):
363
    person = models.ForeignKey(AstakosUser)
364
    group = models.ForeignKey(AstakosGroup)
365
    date_requested = models.DateField(default=datetime.now(), blank=True)
366
    date_joined = models.DateField(null=True, db_index=True, blank=True)
367
    
368
    class Meta:
369
        unique_together = ("person", "group")
370
    
371
    def save(self, *args, **kwargs):
372
        if not self.id:
373
            if not self.group.moderation_enabled:
374
                self.date_joined = datetime.now()
375
        super(Membership, self).save(*args, **kwargs)
376
    
377
    @property
378
    def is_approved(self):
379
        if self.date_joined:
380
            return True
381
        return False
382
    
383
    def approve(self):
384
        self.date_joined = datetime.now()
385
        self.save()
386
        
387
    def disapprove(self):
388
        self.delete()
389

    
390
class AstakosGroupQuota(models.Model):
391
    limit = models.PositiveIntegerField('Limit')
392
    resource = models.ForeignKey(Resource)
393
    group = models.ForeignKey(AstakosGroup, blank=True)
394
    
395
    class Meta:
396
        unique_together = ("resource", "group")
397

    
398
class AstakosUserQuota(models.Model):
399
    limit = models.PositiveIntegerField('Limit')
400
    resource = models.ForeignKey(Resource)
401
    user = models.ForeignKey(AstakosUser)
402
    
403
    class Meta:
404
        unique_together = ("resource", "user")
405

    
406
class ApprovalTerms(models.Model):
407
    """
408
    Model for approval terms
409
    """
410

    
411
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
412
    location = models.CharField('Terms location', max_length=255)
413

    
414
class Invitation(models.Model):
415
    """
416
    Model for registring invitations
417
    """
418
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
419
                                null=True)
420
    realname = models.CharField('Real name', max_length=255)
421
    username = models.CharField('Unique ID', max_length=255, unique=True)
422
    code = models.BigIntegerField('Invitation code', db_index=True)
423
    is_consumed = models.BooleanField('Consumed?', default=False)
424
    created = models.DateTimeField('Creation date', auto_now_add=True)
425
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
426
    
427
    def __init__(self, *args, **kwargs):
428
        super(Invitation, self).__init__(*args, **kwargs)
429
        if not self.id:
430
            self.code = _generate_invitation_code()
431
    
432
    def consume(self):
433
        self.is_consumed = True
434
        self.consumed = datetime.now()
435
        self.save()
436

    
437
    def __unicode__(self):
438
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
439

    
440
def report_user_event(user):
441
    def should_send(user):
442
        # report event incase of new user instance
443
        # or if specific fields are modified
444
        if not user.id:
445
            return True
446
        try:
447
            db_instance = AstakosUser.objects.get(id = user.id)
448
        except AstakosUser.DoesNotExist:
449
            return True
450
        for f in BILLING_FIELDS:
451
            if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
452
                return True
453
        return False
454

    
455
    if QUEUE_CONNECTION and should_send(user):
456

    
457
        from astakos.im.queue.userevent import UserEvent
458
        from synnefo.lib.queue import exchange_connect, exchange_send, \
459
                exchange_close
460

    
461
        eventType = 'create' if not user.id else 'modify'
462
        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
463
        conn = exchange_connect(QUEUE_CONNECTION)
464
        parts = urlparse(QUEUE_CONNECTION)
465
        exchange = parts.path[1:]
466
        routing_key = '%s.user' % exchange
467
        exchange_send(conn, routing_key, body)
468
        exchange_close(conn)
469

    
470
def _generate_invitation_code():
471
    while True:
472
        code = randint(1, 2L**63 - 1)
473
        try:
474
            Invitation.objects.get(code=code)
475
            # An invitation with this code already exists, try again
476
        except Invitation.DoesNotExist:
477
            return code
478

    
479
def get_latest_terms():
480
    try:
481
        term = ApprovalTerms.objects.order_by('-id')[0]
482
        return term
483
    except IndexError:
484
        pass
485
    return None
486

    
487
class EmailChangeManager(models.Manager):
488
    @transaction.commit_on_success
489
    def change_email(self, activation_key):
490
        """
491
        Validate an activation key and change the corresponding
492
        ``User`` if valid.
493

494
        If the key is valid and has not expired, return the ``User``
495
        after activating.
496

497
        If the key is not valid or has expired, return ``None``.
498

499
        If the key is valid but the ``User`` is already active,
500
        return ``None``.
501

502
        After successful email change the activation record is deleted.
503

504
        Throws ValueError if there is already
505
        """
506
        try:
507
            email_change = self.model.objects.get(activation_key=activation_key)
508
            if email_change.activation_key_expired():
509
                email_change.delete()
510
                raise EmailChange.DoesNotExist
511
            # is there an active user with this address?
512
            try:
513
                AstakosUser.objects.get(email=email_change.new_email_address)
514
            except AstakosUser.DoesNotExist:
515
                pass
516
            else:
517
                raise ValueError(_('The new email address is reserved.'))
518
            # update user
519
            user = AstakosUser.objects.get(pk=email_change.user_id)
520
            user.email = email_change.new_email_address
521
            user.save()
522
            email_change.delete()
523
            return user
524
        except EmailChange.DoesNotExist:
525
            raise ValueError(_('Invalid activation key'))
526

    
527
class EmailChange(models.Model):
528
    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.'))
529
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
530
    requested_at = models.DateTimeField(default=datetime.now())
531
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
532

    
533
    objects = EmailChangeManager()
534

    
535
    def activation_key_expired(self):
536
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
537
        return self.requested_at + expiration_date < datetime.now()
538

    
539
class AdditionalMail(models.Model):
540
    """
541
    Model for registring invitations
542
    """
543
    owner = models.ForeignKey(AstakosUser)
544
    email = models.EmailField()
545

    
546
def create_astakos_user(u):
547
    try:
548
        AstakosUser.objects.get(user_ptr=u.pk)
549
    except AstakosUser.DoesNotExist:
550
        extended_user = AstakosUser(user_ptr_id=u.pk)
551
        extended_user.__dict__.update(u.__dict__)
552
        extended_user.renew_token()
553
        extended_user.save()
554
    except:
555
        pass
556

    
557
def superuser_post_syncdb(sender, **kwargs):
558
    # if there was created a superuser
559
    # associate it with an AstakosUser
560
    admins = User.objects.filter(is_superuser=True)
561
    for u in admins:
562
        create_astakos_user(u)
563

    
564
post_syncdb.connect(superuser_post_syncdb)
565

    
566
def superuser_post_save(sender, instance, **kwargs):
567
    if instance.is_superuser:
568
        create_astakos_user(instance)
569

    
570
post_save.connect(superuser_post_save, sender=User)