Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 0f4fa26d

History | View | Annotate | Download (20.2 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
    __groupnames = []
237
    
238
    owner = models.ManyToManyField(AstakosGroup, related_name='owner', null=True)
239
    
240
    class Meta:
241
        unique_together = ("provider", "third_party_identifier")
242
    
243
    def __init__(self, *args, **kwargs):
244
        super(AstakosUser, self).__init__(*args, **kwargs)
245
        self.__has_signed_terms = self.has_signed_terms
246
        if self.id:
247
            self.__groupnames = [g.name for g in self.astakos_groups.all()]
248
        else:
249
            self.is_active = False
250
    
251
    @property
252
    def realname(self):
253
        return '%s %s' %(self.first_name, self.last_name)
254

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

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

    
327
        self.auth_token = b64encode(md5.digest())
328
        self.auth_token_created = datetime.now()
329
        self.auth_token_expires = self.auth_token_created + \
330
                                  timedelta(hours=AUTH_TOKEN_DURATION)
331
        msg = 'Token renewed for %s' % self.email
332
        logger._log(LOGGING_LEVEL, msg, [])
333

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

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

    
397
class AstakosGroupQuota(models.Model):
398
    limit = models.PositiveIntegerField('Limit')
399
    resource = models.ForeignKey(Resource)
400
    group = models.ForeignKey(AstakosGroup, blank=True)
401
    
402
    class Meta:
403
        unique_together = ("resource", "group")
404

    
405
class AstakosUserQuota(models.Model):
406
    limit = models.PositiveIntegerField('Limit')
407
    resource = models.ForeignKey(Resource)
408
    user = models.ForeignKey(AstakosUser)
409
    
410
    class Meta:
411
        unique_together = ("resource", "user")
412

    
413
class ApprovalTerms(models.Model):
414
    """
415
    Model for approval terms
416
    """
417

    
418
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
419
    location = models.CharField('Terms location', max_length=255)
420

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

    
444
    def __unicode__(self):
445
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
446

    
447
def report_user_event(user):
448
    def should_send(user):
449
        # report event incase of new user instance
450
        # or if specific fields are modified
451
        if not user.id:
452
            return True
453
        try:
454
            db_instance = AstakosUser.objects.get(id = user.id)
455
        except AstakosUser.DoesNotExist:
456
            return True
457
        for f in BILLING_FIELDS:
458
            if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
459
                return True
460
        return False
461

    
462
    if QUEUE_CONNECTION and should_send(user):
463

    
464
        from astakos.im.queue.userevent import UserEvent
465
        from synnefo.lib.queue import exchange_connect, exchange_send, \
466
                exchange_close
467

    
468
        eventType = 'create' if not user.id else 'modify'
469
        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
470
        conn = exchange_connect(QUEUE_CONNECTION)
471
        parts = urlparse(QUEUE_CONNECTION)
472
        exchange = parts.path[1:]
473
        routing_key = '%s.user' % exchange
474
        exchange_send(conn, routing_key, body)
475
        exchange_close(conn)
476

    
477
def _generate_invitation_code():
478
    while True:
479
        code = randint(1, 2L**63 - 1)
480
        try:
481
            Invitation.objects.get(code=code)
482
            # An invitation with this code already exists, try again
483
        except Invitation.DoesNotExist:
484
            return code
485

    
486
def get_latest_terms():
487
    try:
488
        term = ApprovalTerms.objects.order_by('-id')[0]
489
        return term
490
    except IndexError:
491
        pass
492
    return None
493

    
494
class EmailChangeManager(models.Manager):
495
    @transaction.commit_on_success
496
    def change_email(self, activation_key):
497
        """
498
        Validate an activation key and change the corresponding
499
        ``User`` if valid.
500

501
        If the key is valid and has not expired, return the ``User``
502
        after activating.
503

504
        If the key is not valid or has expired, return ``None``.
505

506
        If the key is valid but the ``User`` is already active,
507
        return ``None``.
508

509
        After successful email change the activation record is deleted.
510

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

    
534
class EmailChange(models.Model):
535
    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.'))
536
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
537
    requested_at = models.DateTimeField(default=datetime.now())
538
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
539

    
540
    objects = EmailChangeManager()
541

    
542
    def activation_key_expired(self):
543
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
544
        return self.requested_at + expiration_date < datetime.now()
545

    
546
class AdditionalMail(models.Model):
547
    """
548
    Model for registring invitations
549
    """
550
    owner = models.ForeignKey(AstakosUser)
551
    email = models.EmailField()
552

    
553
def create_astakos_user(u):
554
    try:
555
        AstakosUser.objects.get(user_ptr=u.pk)
556
    except AstakosUser.DoesNotExist:
557
        extended_user = AstakosUser(user_ptr_id=u.pk)
558
        extended_user.__dict__.update(u.__dict__)
559
        extended_user.renew_token()
560
        extended_user.save()
561
    except:
562
        pass
563

    
564
def superuser_post_syncdb(sender, **kwargs):
565
    # if there was created a superuser
566
    # associate it with an AstakosUser
567
    admins = User.objects.filter(is_superuser=True)
568
    for u in admins:
569
        create_astakos_user(u)
570

    
571
post_syncdb.connect(superuser_post_syncdb)
572

    
573
def superuser_post_save(sender, instance, **kwargs):
574
    if instance.is_superuser:
575
        create_astakos_user(instance)
576

    
577
post_save.connect(superuser_post_save, sender=User)
578

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