Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (20.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, 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
        m, created = self.membership_set.get_or_create(person=person)
158
        # update date_joined in any case
159
        m.date_joined=datetime.now()
160
        m.save()
161
    
162
    def disapprove_member(self, person):
163
        self.membership_set.remove(person=person)
164
    
165
    @property
166
    def members(self):
167
        return map(lambda m:m.person, self.membership_set.all())
168
    
169
    @property
170
    def approved_members(self):
171
        f = filter(lambda m:m.is_approved, self.membership_set.all())
172
        return map(lambda m:m.person, f)
173
    
174
    @property
175
    def quota(self):
176
        d = defaultdict(int)
177
        for q in self.astakosgroupquota_set.all():
178
            d[q.resource] += q.limit
179
        return d
180
    
181
    @property
182
    def has_undefined_policies(self):
183
        # TODO: can avoid query?
184
        return Resource.objects.filter(~Q(astakosgroup=self)).exists()
185
    
186
    @property
187
    def owners(self):
188
        return self.owner.all()
189
    
190
    @owners.setter
191
    def owners(self, l):
192
        self.owner = l
193
        map(self.approve_member, l)
194

    
195
class AstakosUser(User):
196
    """
197
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
198
    """
199
    # Use UserManager to get the create_user method, etc.
200
    objects = UserManager()
201

    
202
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
203
    provider = models.CharField('Provider', max_length=255, blank=True)
204

    
205
    #for invitations
206
    user_level = DEFAULT_USER_LEVEL
207
    level = models.IntegerField('Inviter level', default=user_level)
208
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
209

    
210
    auth_token = models.CharField('Authentication Token', max_length=32,
211
                                  null=True, blank=True)
212
    auth_token_created = models.DateTimeField('Token creation date', null=True)
213
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
214

    
215
    updated = models.DateTimeField('Update date')
216
    is_verified = models.BooleanField('Is verified?', default=False)
217

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

    
221
    email_verified = models.BooleanField('Email verified?', default=False)
222

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

    
252
    @realname.setter
253
    def realname(self, value):
254
        parts = value.split(' ')
255
        if len(parts) == 2:
256
            self.first_name = parts[0]
257
            self.last_name = parts[1]
258
        else:
259
            self.last_name = parts[0]
260

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

    
315
        self.auth_token = b64encode(md5.digest())
316
        self.auth_token_created = datetime.now()
317
        self.auth_token_expires = self.auth_token_created + \
318
                                  timedelta(hours=AUTH_TOKEN_DURATION)
319
        msg = 'Token renewed for %s' % self.email
320
        logger._log(LOGGING_LEVEL, msg, [])
321

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

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

    
385
class AstakosGroupQuota(models.Model):
386
    limit = models.PositiveIntegerField('Limit')
387
    resource = models.ForeignKey(Resource)
388
    group = models.ForeignKey(AstakosGroup, blank=True)
389
    
390
    class Meta:
391
        unique_together = ("resource", "group")
392

    
393
class AstakosUserQuota(models.Model):
394
    limit = models.PositiveIntegerField('Limit')
395
    resource = models.ForeignKey(Resource)
396
    user = models.ForeignKey(AstakosUser)
397
    
398
    class Meta:
399
        unique_together = ("resource", "user")
400

    
401
class ApprovalTerms(models.Model):
402
    """
403
    Model for approval terms
404
    """
405

    
406
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
407
    location = models.CharField('Terms location', max_length=255)
408

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

    
432
    def __unicode__(self):
433
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
434

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

    
450
    if QUEUE_CONNECTION and should_send(user):
451

    
452
        from astakos.im.queue.userevent import UserEvent
453
        from synnefo.lib.queue import exchange_connect, exchange_send, \
454
                exchange_close
455

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

    
465
def _generate_invitation_code():
466
    while True:
467
        code = randint(1, 2L**63 - 1)
468
        try:
469
            Invitation.objects.get(code=code)
470
            # An invitation with this code already exists, try again
471
        except Invitation.DoesNotExist:
472
            return code
473

    
474
def get_latest_terms():
475
    try:
476
        term = ApprovalTerms.objects.order_by('-id')[0]
477
        return term
478
    except IndexError:
479
        pass
480
    return None
481

    
482
class EmailChangeManager(models.Manager):
483
    @transaction.commit_on_success
484
    def change_email(self, activation_key):
485
        """
486
        Validate an activation key and change the corresponding
487
        ``User`` if valid.
488

489
        If the key is valid and has not expired, return the ``User``
490
        after activating.
491

492
        If the key is not valid or has expired, return ``None``.
493

494
        If the key is valid but the ``User`` is already active,
495
        return ``None``.
496

497
        After successful email change the activation record is deleted.
498

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

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

    
528
    objects = EmailChangeManager()
529

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

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

    
541
def create_astakos_user(u):
542
    try:
543
        AstakosUser.objects.get(user_ptr=u.pk)
544
    except AstakosUser.DoesNotExist:
545
        extended_user = AstakosUser(user_ptr_id=u.pk)
546
        extended_user.__dict__.update(u.__dict__)
547
        extended_user.renew_token()
548
        extended_user.save()
549
    except:
550
        pass
551

    
552
def superuser_post_syncdb(sender, **kwargs):
553
    # if there was created a superuser
554
    # associate it with an AstakosUser
555
    admins = User.objects.filter(is_superuser=True)
556
    for u in admins:
557
        create_astakos_user(u)
558

    
559
post_syncdb.connect(superuser_post_syncdb)
560

    
561
def superuser_post_save(sender, instance, **kwargs):
562
    if instance.is_superuser:
563
        create_astakos_user(instance)
564

    
565
post_save.connect(superuser_post_save, sender=User)
566

    
567
def set_default_group(sender, instance, created, **kwargs):
568
    if not created:
569
        return
570
    try:
571
        default = AstakosGroup.objects.get(name = 'default')
572
        Membership(group=default, person=instance, date_joined=datetime.now()).save()
573
    except AstakosGroup.DoesNotExist, e:
574
        logger.exception(e)
575

    
576
post_save.connect(set_default_group, sender=AstakosUser)
577

    
578
def get_resources():
579
    # use cache
580
    return Resource.objects.select_related().all()