Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 7507ea03

History | View | Annotate | Download (22.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

    
38
from time import asctime
39
from datetime import datetime, timedelta
40
from base64 import b64encode
41
from random import randint
42
from collections import defaultdict
43

    
44
from django.db import models
45
from django.contrib.auth.models import User, UserManager, Group
46
from django.utils.translation import ugettext as _
47
from django.core.exceptions import ValidationError
48
from django.db import transaction
49
from django.db.models.signals import pre_save, post_save, post_syncdb, post_delete
50
from django.dispatch import Signal
51
from django.db.models import Q
52

    
53
from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
54
                                 AUTH_TOKEN_DURATION, BILLING_FIELDS,
55
                                 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
56
from astakos.im.endpoints.quotaholder import (register_users, send_quota,
57
                                              register_resources)
58
from astakos.im.endpoints.aquarium.producer import report_user_event
59

    
60
from astakos.im.tasks import propagate_groupmembers_quota
61

    
62
logger = logging.getLogger(__name__)
63

    
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(
73
        'Token expiration date', null=True)
74

    
75
    def save(self, **kwargs):
76
        if not self.id:
77
            self.renew_token()
78
        self.full_clean()
79
        super(Service, self).save(**kwargs)
80

    
81
    def renew_token(self):
82
        md5 = hashlib.md5()
83
        md5.update(self.name.encode('ascii', 'ignore'))
84
        md5.update(self.url.encode('ascii', 'ignore'))
85
        md5.update(asctime())
86

    
87
        self.auth_token = b64encode(md5.digest())
88
        self.auth_token_created = datetime.now()
89
        self.auth_token_expires = self.auth_token_created + \
90
            timedelta(hours=AUTH_TOKEN_DURATION)
91

    
92
    def __str__(self):
93
        return self.name
94

    
95

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

    
100

    
101
class Resource(models.Model):
102
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
103
    meta = models.ManyToManyField(ResourceMetadata)
104
    service = models.ForeignKey(Service)
105

    
106
    def __str__(self):
107
        return '%s.%s' % (self.service, self.name)
108

    
109

    
110
class GroupKind(models.Model):
111
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
112

    
113
    def __str__(self):
114
        return self.name
115

    
116

    
117
class AstakosGroup(Group):
118
    kind = models.ForeignKey(GroupKind)
119
    homepage = models.URLField(
120
        'Homepage Url', max_length=255, null=True, blank=True)
121
    desc = models.TextField('Description', null=True)
122
    policy = models.ManyToManyField(Resource, null=True, blank=True,
123
                                    through='AstakosGroupQuota')
124
    creation_date = models.DateTimeField('Creation date',
125
                                         default=datetime.now())
126
    issue_date = models.DateTimeField('Issue date', null=True)
127
    expiration_date = models.DateTimeField('Expiration date', null=True)
128
    moderation_enabled = models.BooleanField('Moderated membership?',
129
                                             default=True)
130
    approval_date = models.DateTimeField('Activation date', null=True,
131
                                         blank=True)
132
    estimated_participants = models.PositiveIntegerField('Estimated #members',
133
                                                         null=True)
134

    
135
    @property
136
    def is_disabled(self):
137
        if not self.approval_date:
138
            return True
139
        return False
140

    
141
    @property
142
    def is_enabled(self):
143
        if self.is_disabled:
144
            return False
145
        if not self.issue_date:
146
            return False
147
        if not self.expiration_date:
148
            return True
149
        now = datetime.now()
150
        if self.issue_date > now:
151
            return False
152
        if now >= self.expiration_date:
153
            return False
154
        return True
155

    
156
    def enable(self):
157
        if self.is_enabled:
158
            return
159
        self.approval_date = datetime.now()
160
        self.save()
161
        quota_disturbed.send(sender=self, users=self.approved_members)
162
        propagate_groupmembers_quota.apply_async(
163
            args=[self], eta=self.issue_date)
164
        propagate_groupmembers_quota.apply_async(
165
            args=[self], eta=self.expiration_date)
166

    
167
    def disable(self):
168
        if self.is_disabled:
169
            return
170
        self.approval_date = None
171
        self.save()
172
        quota_disturbed.send(sender=self, users=self.approved_members)
173

    
174
    def approve_member(self, person):
175
        m, created = self.membership_set.get_or_create(person=person)
176
        # update date_joined in any case
177
        m.date_joined = datetime.now()
178
        m.save()
179

    
180
    def disapprove_member(self, person):
181
        self.membership_set.remove(person=person)
182

    
183
    @property
184
    def members(self):
185
        q = self.membership_set.select_related().all()
186
        return [m.person for m in q]
187

    
188
    @property
189
    def approved_members(self):
190
        q = self.membership_set.select_related().all()
191
        return [m.person for m in q if m.is_approved]
192

    
193
    @property
194
    def quota(self):
195
        d = defaultdict(int)
196
        for q in self.astakosgroupquota_set.select_related().all():
197
            d[q.resource] += q.uplimit
198
        return d
199

    
200
    @property
201
    def owners(self):
202
        return self.owner.all()
203

    
204
    @property
205
    def owner_details(self):
206
        return self.owner.select_related().all()
207

    
208
    @owners.setter
209
    def owners(self, l):
210
        self.owner = l
211
        map(self.approve_member, l)
212

    
213

    
214
class AstakosUser(User):
215
    """
216
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
217
    """
218
    # Use UserManager to get the create_user method, etc.
219
    objects = UserManager()
220

    
221
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
222
    provider = models.CharField('Provider', max_length=255, blank=True)
223

    
224
    #for invitations
225
    user_level = DEFAULT_USER_LEVEL
226
    level = models.IntegerField('Inviter level', default=user_level)
227
    invitations = models.IntegerField(
228
        'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
229

    
230
    auth_token = models.CharField('Authentication Token', max_length=32,
231
                                  null=True, blank=True)
232
    auth_token_created = models.DateTimeField('Token creation date', null=True)
233
    auth_token_expires = models.DateTimeField(
234
        'Token expiration date', null=True)
235

    
236
    updated = models.DateTimeField('Update date')
237
    is_verified = models.BooleanField('Is verified?', default=False)
238

    
239
    # ex. screen_name for twitter, eppn for shibboleth
240
    third_party_identifier = models.CharField(
241
        'Third-party identifier', max_length=255, null=True, blank=True)
242

    
243
    email_verified = models.BooleanField('Email verified?', default=False)
244

    
245
    has_credits = models.BooleanField('Has credits?', default=False)
246
    has_signed_terms = models.BooleanField(
247
        'Agree with the terms?', default=False)
248
    date_signed_terms = models.DateTimeField(
249
        'Signed terms date', null=True, blank=True)
250

    
251
    activation_sent = models.DateTimeField(
252
        'Activation sent data', null=True, blank=True)
253

    
254
    policy = models.ManyToManyField(
255
        Resource, null=True, through='AstakosUserQuota')
256

    
257
    astakos_groups = models.ManyToManyField(
258
        AstakosGroup, verbose_name=_('agroups'), blank=True,
259
        help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
260
        through='Membership')
261

    
262
    __has_signed_terms = False
263

    
264
    owner = models.ManyToManyField(
265
        AstakosGroup, related_name='owner', null=True)
266

    
267
    class Meta:
268
        unique_together = ("provider", "third_party_identifier")
269

    
270
    def __init__(self, *args, **kwargs):
271
        super(AstakosUser, self).__init__(*args, **kwargs)
272
        self.__has_signed_terms = self.has_signed_terms
273
        if not self.id:
274
            self.is_active = False
275

    
276
    @property
277
    def realname(self):
278
        return '%s %s' % (self.first_name, self.last_name)
279

    
280
    @realname.setter
281
    def realname(self, value):
282
        parts = value.split(' ')
283
        if len(parts) == 2:
284
            self.first_name = parts[0]
285
            self.last_name = parts[1]
286
        else:
287
            self.last_name = parts[0]
288

    
289
    @property
290
    def invitation(self):
291
        try:
292
            return Invitation.objects.get(username=self.email)
293
        except Invitation.DoesNotExist:
294
            return None
295

    
296
    @property
297
    def quota(self):
298
        d = defaultdict(int)
299
        for q in self.astakosuserquota_set.select_related().all():
300
            d[q.resource.name] += q.uplimit
301
        for m in self.membership_set.select_related().all():
302
            if not m.is_approved:
303
                continue
304
            g = m.group
305
            if not g.is_enabled:
306
                continue
307
            for r, uplimit in g.quota.iteritems():
308
                d[r] += uplimit
309
        # TODO set default for remaining
310
        return d
311

    
312
    def save(self, update_timestamps=True, **kwargs):
313
        if update_timestamps:
314
            if not self.id:
315
                self.date_joined = datetime.now()
316
            self.updated = datetime.now()
317

    
318
        # update date_signed_terms if necessary
319
        if self.__has_signed_terms != self.has_signed_terms:
320
            self.date_signed_terms = datetime.now()
321

    
322
        if not self.id:
323
            # set username
324
            while not self.username:
325
                username = uuid.uuid4().hex[:30]
326
                try:
327
                    AstakosUser.objects.get(username=username)
328
                except AstakosUser.DoesNotExist:
329
                    self.username = username
330
            if not self.provider:
331
                self.provider = 'local'
332
        self.validate_unique_email_isactive()
333
        if self.is_active and self.activation_sent:
334
            # reset the activation sent
335
            self.activation_sent = None
336

    
337
        super(AstakosUser, self).save(**kwargs)
338

    
339
    def renew_token(self):
340
        md5 = hashlib.md5()
341
        md5.update(self.username)
342
        md5.update(self.realname.encode('ascii', 'ignore'))
343
        md5.update(asctime())
344

    
345
        self.auth_token = b64encode(md5.digest())
346
        self.auth_token_created = datetime.now()
347
        self.auth_token_expires = self.auth_token_created + \
348
            timedelta(hours=AUTH_TOKEN_DURATION)
349
        msg = 'Token renewed for %s' % self.email
350
        logger.log(LOGGING_LEVEL, msg)
351

    
352
    def __unicode__(self):
353
        return '%s (%s)' % (self.realname, self.email)
354

    
355
    def conflicting_email(self):
356
        q = AstakosUser.objects.exclude(username=self.username)
357
        q = q.filter(email=self.email)
358
        if q.count() != 0:
359
            return True
360
        return False
361

    
362
    def validate_unique_email_isactive(self):
363
        """
364
        Implements a unique_together constraint for email and is_active fields.
365
        """
366
        q = AstakosUser.objects.exclude(username=self.username)
367
        q = q.filter(email=self.email)
368
        q = q.filter(is_active=self.is_active)
369
        if q.count() != 0:
370
            raise ValidationError({'__all__': [_('Another account with the same email & is_active combination found.')]})
371

    
372
    @property
373
    def signed_terms(self):
374
        term = get_latest_terms()
375
        if not term:
376
            return True
377
        if not self.has_signed_terms:
378
            return False
379
        if not self.date_signed_terms:
380
            return False
381
        if self.date_signed_terms < term.date:
382
            self.has_signed_terms = False
383
            self.date_signed_terms = None
384
            self.save()
385
            return False
386
        return True
387

    
388

    
389
class Membership(models.Model):
390
    person = models.ForeignKey(AstakosUser)
391
    group = models.ForeignKey(AstakosGroup)
392
    date_requested = models.DateField(default=datetime.now(), blank=True)
393
    date_joined = models.DateField(null=True, db_index=True, blank=True)
394

    
395
    class Meta:
396
        unique_together = ("person", "group")
397

    
398
    def save(self, *args, **kwargs):
399
        if not self.id:
400
            if not self.group.moderation_enabled:
401
                self.date_joined = datetime.now()
402
        super(Membership, self).save(*args, **kwargs)
403

    
404
    @property
405
    def is_approved(self):
406
        if self.date_joined:
407
            return True
408
        return False
409

    
410
    def approve(self):
411
        self.date_joined = datetime.now()
412
        self.save()
413
        quota_disturbed.send(sender=self, users=(self.person,))
414

    
415
    def disapprove(self):
416
        self.delete()
417
        quota_disturbed.send(sender=self, users=(self.person,))
418

    
419

    
420
class AstakosGroupQuota(models.Model):
421
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
422
    uplimit = models.BigIntegerField('Up limit', null=True)
423
    resource = models.ForeignKey(Resource)
424
    group = models.ForeignKey(AstakosGroup, blank=True)
425

    
426
    class Meta:
427
        unique_together = ("resource", "group")
428

    
429

    
430
class AstakosUserQuota(models.Model):
431
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
432
    uplimit = models.BigIntegerField('Up limit', null=True)
433
    resource = models.ForeignKey(Resource)
434
    user = models.ForeignKey(AstakosUser)
435

    
436
    class Meta:
437
        unique_together = ("resource", "user")
438

    
439

    
440
class ApprovalTerms(models.Model):
441
    """
442
    Model for approval terms
443
    """
444

    
445
    date = models.DateTimeField(
446
        'Issue date', db_index=True, default=datetime.now())
447
    location = models.CharField('Terms location', max_length=255)
448

    
449

    
450
class Invitation(models.Model):
451
    """
452
    Model for registring invitations
453
    """
454
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
455
                                null=True)
456
    realname = models.CharField('Real name', max_length=255)
457
    username = models.CharField('Unique ID', max_length=255, unique=True)
458
    code = models.BigIntegerField('Invitation code', db_index=True)
459
    is_consumed = models.BooleanField('Consumed?', default=False)
460
    created = models.DateTimeField('Creation date', auto_now_add=True)
461
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
462

    
463
    def __init__(self, *args, **kwargs):
464
        super(Invitation, self).__init__(*args, **kwargs)
465
        if not self.id:
466
            self.code = _generate_invitation_code()
467

    
468
    def consume(self):
469
        self.is_consumed = True
470
        self.consumed = datetime.now()
471
        self.save()
472

    
473
    def __unicode__(self):
474
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
475

    
476

    
477
class EmailChangeManager(models.Manager):
478
    @transaction.commit_on_success
479
    def change_email(self, activation_key):
480
        """
481
        Validate an activation key and change the corresponding
482
        ``User`` if valid.
483

484
        If the key is valid and has not expired, return the ``User``
485
        after activating.
486

487
        If the key is not valid or has expired, return ``None``.
488

489
        If the key is valid but the ``User`` is already active,
490
        return ``None``.
491

492
        After successful email change the activation record is deleted.
493

494
        Throws ValueError if there is already
495
        """
496
        try:
497
            email_change = self.model.objects.get(
498
                activation_key=activation_key)
499
            if email_change.activation_key_expired():
500
                email_change.delete()
501
                raise EmailChange.DoesNotExist
502
            # is there an active user with this address?
503
            try:
504
                AstakosUser.objects.get(email=email_change.new_email_address)
505
            except AstakosUser.DoesNotExist:
506
                pass
507
            else:
508
                raise ValueError(_('The new email address is reserved.'))
509
            # update user
510
            user = AstakosUser.objects.get(pk=email_change.user_id)
511
            user.email = email_change.new_email_address
512
            user.save()
513
            email_change.delete()
514
            return user
515
        except EmailChange.DoesNotExist:
516
            raise ValueError(_('Invalid activation key'))
517

    
518

    
519
class EmailChange(models.Model):
520
    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.'))
521
    user = models.ForeignKey(
522
        AstakosUser, unique=True, related_name='emailchange_user')
523
    requested_at = models.DateTimeField(default=datetime.now())
524
    activation_key = models.CharField(
525
        max_length=40, unique=True, db_index=True)
526

    
527
    objects = EmailChangeManager()
528

    
529
    def activation_key_expired(self):
530
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
531
        return self.requested_at + expiration_date < datetime.now()
532

    
533

    
534
class AdditionalMail(models.Model):
535
    """
536
    Model for registring invitations
537
    """
538
    owner = models.ForeignKey(AstakosUser)
539
    email = models.EmailField()
540

    
541

    
542
def _generate_invitation_code():
543
    while True:
544
        code = randint(1, 2L ** 63 - 1)
545
        try:
546
            Invitation.objects.get(code=code)
547
            # An invitation with this code already exists, try again
548
        except Invitation.DoesNotExist:
549
            return code
550

    
551

    
552
def get_latest_terms():
553
    try:
554
        term = ApprovalTerms.objects.order_by('-id')[0]
555
        return term
556
    except IndexError:
557
        pass
558
    return None
559

    
560

    
561
def create_astakos_user(u):
562
    try:
563
        AstakosUser.objects.get(user_ptr=u.pk)
564
    except AstakosUser.DoesNotExist:
565
        extended_user = AstakosUser(user_ptr_id=u.pk)
566
        extended_user.__dict__.update(u.__dict__)
567
        extended_user.renew_token()
568
        extended_user.save()
569
    except BaseException, e:
570
        logger.exception(e)
571
        pass
572

    
573

    
574
def fix_superusers(sender, **kwargs):
575
    # Associate superusers with AstakosUser
576
    admins = User.objects.filter(is_superuser=True)
577
    for u in admins:
578
        create_astakos_user(u)
579

    
580

    
581
def user_post_save(sender, instance, created, **kwargs):
582
    if not created:
583
        return
584
    create_astakos_user(instance)
585

    
586

    
587
def set_default_group(user):
588
    try:
589
        default = AstakosGroup.objects.get(name='default')
590
        Membership(
591
            group=default, person=user, date_joined=datetime.now()).save()
592
    except AstakosGroup.DoesNotExist, e:
593
        logger.exception(e)
594

    
595

    
596
def astakosuser_pre_save(sender, instance, **kwargs):
597
    instance.aquarium_report = False
598
    instance.new = False
599
    try:
600
        db_instance = AstakosUser.objects.get(id=instance.id)
601
    except AstakosUser.DoesNotExist:
602
        # create event
603
        instance.aquarium_report = True
604
        instance.new = True
605
    else:
606
        get = AstakosUser.__getattribute__
607
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
608
                   BILLING_FIELDS
609
                   )
610
        instance.aquarium_report = True if l else False
611

    
612

    
613
def astakosuser_post_save(sender, instance, created, **kwargs):
614
    if instance.aquarium_report:
615
        report_user_event(instance, create=instance.new)
616
    if not created:
617
        return
618
    set_default_group(instance)
619
    # TODO handle socket.error & IOError
620
    register_users((instance,))
621

    
622

    
623
def resource_post_save(sender, instance, created, **kwargs):
624
    if not created:
625
        return
626
    register_resources((instance,))
627

    
628

    
629
def send_quota_disturbed(sender, instance, **kwargs):
630
    users = []
631
    extend = users.extend
632
    if sender == Membership:
633
        if not instance.group.is_enabled:
634
            return
635
        extend([instance.person])
636
    elif sender == AstakosUserQuota:
637
        extend([instance.user])
638
    elif sender == AstakosGroupQuota:
639
        if not instance.group.is_enabled:
640
            return
641
        extend(instance.group.astakosuser_set.all())
642
    elif sender == AstakosGroup:
643
        if not instance.is_enabled:
644
            return
645
    quota_disturbed.send(sender=sender, users=users)
646

    
647

    
648
def on_quota_disturbed(sender, users, **kwargs):
649
    print '>>>', locals()
650
    if not users:
651
        return
652
    send_quota(users)
653

    
654
post_syncdb.connect(fix_superusers)
655
post_save.connect(user_post_save, sender=User)
656
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
657
post_save.connect(astakosuser_post_save, sender=AstakosUser)
658
post_save.connect(resource_post_save, sender=Resource)
659

    
660
quota_disturbed = Signal(providing_args=["users"])
661
quota_disturbed.connect(on_quota_disturbed)
662

    
663
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
664
post_delete.connect(send_quota_disturbed, sender=Membership)
665
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
666
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
667
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
668
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)