Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 28252c7f

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

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

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

    
59
QUEUE_CLIENT_ID = 3 # Astakos.
60

    
61
logger = logging.getLogger(__name__)
62

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

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

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

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

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

    
110
class AstakosGroup(Group):
111
    kind = models.ForeignKey(GroupKind)
112
    desc = models.TextField('Description', null=True)
113
    policy = models.ManyToManyField(Resource, null=True, blank=True, through='AstakosGroupQuota')
114
    creation_date = models.DateTimeField('Creation date', default=datetime.now())
115
    issue_date = models.DateTimeField('Issue date', null=True)
116
    expiration_date = models.DateTimeField('Expiration date', null=True)
117
    moderation_enabled = models.BooleanField('Moderated membership?', default=True)
118
    approval_date = models.DateTimeField('Activation date', null=True, blank=True)
119
    estimated_participants = models.PositiveIntegerField('Estimated #participants', null=True)
120
    
121
    @property
122
    def is_disabled(self):
123
        if not approval_date:
124
            return False
125
        return True
126
    
127
    @property
128
    def is_active(self):
129
        if self.is_disabled:
130
            return False
131
        if not self.issue_date:
132
            return False
133
        if not self.expiration_date:
134
            return True
135
        now = datetime.now()
136
        if self.issue_date > now:
137
            return False
138
        if now >= self.expiration_date:
139
            return False
140
        return True
141
    
142
    @property
143
    def participants(self):
144
        return len(self.approved_members)
145
    
146
    def approve(self):
147
        self.approval_date = datetime.now()
148
        self.save()
149
    
150
    def disapprove(self):
151
        self.approval_date = None
152
        self.save()
153
    
154
    def approve_member(self, person):
155
        try:
156
            self.membership_set.create(person=person, date_joined=datetime.now())
157
        except IntegrityError:
158
            m = self.membership_set.get(person=person)
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 policies(self):
176
        return self.astakosgroupquota_set.all()
177
    
178
    @property
179
    def has_undefined_policies(self):
180
        # TODO: can avoid query?
181
        return Resource.objects.filter(~Q(astakosgroup=self)).exists()
182

    
183
class AstakosUser(User):
184
    """
185
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
186
    """
187
    # Use UserManager to get the create_user method, etc.
188
    objects = UserManager()
189

    
190
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
191
    provider = models.CharField('Provider', max_length=255, blank=True)
192

    
193
    #for invitations
194
    user_level = DEFAULT_USER_LEVEL
195
    level = models.IntegerField('Inviter level', default=user_level)
196
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
197

    
198
    auth_token = models.CharField('Authentication Token', max_length=32,
199
                                  null=True, blank=True)
200
    auth_token_created = models.DateTimeField('Token creation date', null=True)
201
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
202

    
203
    updated = models.DateTimeField('Update date')
204
    is_verified = models.BooleanField('Is verified?', default=False)
205

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

    
209
    email_verified = models.BooleanField('Email verified?', default=False)
210

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

    
243
    @realname.setter
244
    def realname(self, value):
245
        parts = value.split(' ')
246
        if len(parts) == 2:
247
            self.first_name = parts[0]
248
            self.last_name = parts[1]
249
        else:
250
            self.last_name = parts[0]
251

    
252
    @property
253
    def invitation(self):
254
        try:
255
            return Invitation.objects.get(username=self.email)
256
        except Invitation.DoesNotExist:
257
            return None
258

    
259
    def save(self, update_timestamps=True, **kwargs):
260
        if update_timestamps:
261
            if not self.id:
262
                self.date_joined = datetime.now()
263
            self.updated = datetime.now()
264
        
265
        # update date_signed_terms if necessary
266
        if self.__has_signed_terms != self.has_signed_terms:
267
            self.date_signed_terms = datetime.now()
268
        
269
        if not self.id:
270
            # set username
271
            while not self.username:
272
                username =  uuid.uuid4().hex[:30]
273
                try:
274
                    AstakosUser.objects.get(username = username)
275
                except AstakosUser.DoesNotExist, e:
276
                    self.username = username
277
            if not self.provider:
278
                self.provider = 'local'
279
        report_user_event(self)
280
        self.validate_unique_email_isactive()
281
        if self.is_active and self.activation_sent:
282
            # reset the activation sent
283
            self.activation_sent = None
284
        
285
        super(AstakosUser, self).save(**kwargs)
286
        
287
        # set group if does not exist
288
        groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
289
        if groupname not in self.__groupnames:
290
            try:
291
                group = Group.objects.get(name = groupname)
292
                self.groups.add(group)
293
            except Group.DoesNotExist, e:
294
                logger.exception(e)
295
    
296
    def renew_token(self):
297
        md5 = hashlib.md5()
298
        md5.update(self.username)
299
        md5.update(self.realname.encode('ascii', 'ignore'))
300
        md5.update(asctime())
301

    
302
        self.auth_token = b64encode(md5.digest())
303
        self.auth_token_created = datetime.now()
304
        self.auth_token_expires = self.auth_token_created + \
305
                                  timedelta(hours=AUTH_TOKEN_DURATION)
306
        msg = 'Token renewed for %s' % self.email
307
        logger._log(LOGGING_LEVEL, msg, [])
308

    
309
    def __unicode__(self):
310
        return self.username
311
    
312
    def conflicting_email(self):
313
        q = AstakosUser.objects.exclude(username = self.username)
314
        q = q.filter(email = self.email)
315
        if q.count() != 0:
316
            return True
317
        return False
318
    
319
    def validate_unique_email_isactive(self):
320
        """
321
        Implements a unique_together constraint for email and is_active fields.
322
        """
323
        q = AstakosUser.objects.exclude(username = self.username)
324
        q = q.filter(email = self.email)
325
        q = q.filter(is_active = self.is_active)
326
        if q.count() != 0:
327
            raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
328
    
329
    def signed_terms(self):
330
        term = get_latest_terms()
331
        if not term:
332
            return True
333
        if not self.has_signed_terms:
334
            return False
335
        if not self.date_signed_terms:
336
            return False
337
        if self.date_signed_terms < term.date:
338
            self.has_signed_terms = False
339
            self.date_signed_terms = None
340
            self.save()
341
            return False
342
        return True
343
    
344
    def enroll_group(self, group):
345
        self.membership_set.add(group)
346
    
347
    def get_astakos_groups(self, approved=True):
348
        if approved:
349
            return self.membership_set().filter(is_approved=True)
350
        return self.membership_set().all()
351

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

    
380
class AstakosGroupQuota(models.Model):
381
    limit = models.PositiveIntegerField('Limit')
382
    resource = models.ForeignKey(Resource)
383
    group = models.ForeignKey(AstakosGroup, blank=True)
384
    
385
    class Meta:
386
        unique_together = ("resource", "group")
387

    
388
class AstakosUserQuota(models.Model):
389
    limit = models.PositiveIntegerField('Limit')
390
    resource = models.ForeignKey(Resource)
391
    user = models.ForeignKey(AstakosUser)
392
    
393
    class Meta:
394
        unique_together = ("resource", "user")
395

    
396
class ApprovalTerms(models.Model):
397
    """
398
    Model for approval terms
399
    """
400

    
401
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
402
    location = models.CharField('Terms location', max_length=255)
403

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

    
427
    def __unicode__(self):
428
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
429

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

    
445
    if QUEUE_CONNECTION and should_send(user):
446

    
447
        from astakos.im.queue.userevent import UserEvent
448
        from synnefo.lib.queue import exchange_connect, exchange_send, \
449
                exchange_close
450

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

    
460
def _generate_invitation_code():
461
    while True:
462
        code = randint(1, 2L**63 - 1)
463
        try:
464
            Invitation.objects.get(code=code)
465
            # An invitation with this code already exists, try again
466
        except Invitation.DoesNotExist:
467
            return code
468

    
469
def get_latest_terms():
470
    try:
471
        term = ApprovalTerms.objects.order_by('-id')[0]
472
        return term
473
    except IndexError:
474
        pass
475
    return None
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(activation_key=activation_key)
498
            if email_change.activation_key_expired():
499
                email_change.delete()
500
                raise EmailChange.DoesNotExist
501
            # is there an active user with this address?
502
            try:
503
                AstakosUser.objects.get(email=email_change.new_email_address)
504
            except AstakosUser.DoesNotExist:
505
                pass
506
            else:
507
                raise ValueError(_('The new email address is reserved.'))
508
            # update user
509
            user = AstakosUser.objects.get(pk=email_change.user_id)
510
            user.email = email_change.new_email_address
511
            user.save()
512
            email_change.delete()
513
            return user
514
        except EmailChange.DoesNotExist:
515
            raise ValueError(_('Invalid activation key'))
516

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

    
523
    objects = EmailChangeManager()
524

    
525
    def activation_key_expired(self):
526
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
527
        return self.requested_at + expiration_date < datetime.now()
528

    
529
class AdditionalMail(models.Model):
530
    """
531
    Model for registring invitations
532
    """
533
    owner = models.ForeignKey(AstakosUser)
534
    email = models.EmailField()
535

    
536
def create_astakos_user(u):
537
    try:
538
        AstakosUser.objects.get(user_ptr=u.pk)
539
    except AstakosUser.DoesNotExist:
540
        extended_user = AstakosUser(user_ptr_id=u.pk)
541
        extended_user.__dict__.update(u.__dict__)
542
        extended_user.renew_token()
543
        extended_user.save()
544
    except:
545
        pass
546

    
547
def superuser_post_syncdb(sender, **kwargs):
548
    # if there was created a superuser
549
    # associate it with an AstakosUser
550
    admins = User.objects.filter(is_superuser=True)
551
    for u in admins:
552
        create_astakos_user(u)
553

    
554
post_syncdb.connect(superuser_post_syncdb)
555

    
556
def superuser_post_save(sender, instance, **kwargs):
557
    if instance.is_superuser:
558
        create_astakos_user(instance)
559

    
560
post_save.connect(superuser_post_save, sender=User)