Statistics
| Branch: | Tag: | Revision:

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

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

    
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
    moderatation_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
        if not self.id:
145
            return 0
146
        return self.user_set.count()
147
    
148
    def approve(self):
149
        self.approval_date = datetime.now()
150
        self.save()
151
    
152
    def disapprove(self):
153
        self.approval_date = None
154
        self.save()
155
    
156
    def approve_member(self, member):
157
        m, created = self.membership_set.get_or_create(person=member, group=self)
158
        m.date_joined = datetime.now()
159
        m.save()
160
        
161
    def disapprove_member(self, member):
162
        m = self.membership_set.remove(member)
163
    
164
    def get_members(self, approved=True):
165
        if approved:
166
            return self.membership_set().filter(is_approved=True)
167
        return self.membership_set().all()
168
    
169
    def get_policies(self):
170
        related = self.policy.through.objects
171
        return map(lambda r: {r.name:related.get(resource__id=r.id, group__id=self.id).limit}, self.policy.all())
172
    
173
    def has_undefined_policies(self):
174
        # TODO: can avoid query?
175
        return Resource.objects.filter(~Q(astakosgroup=self)).exists()
176

    
177
class AstakosUser(User):
178
    """
179
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
180
    """
181
    # Use UserManager to get the create_user method, etc.
182
    objects = UserManager()
183

    
184
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
185
    provider = models.CharField('Provider', max_length=255, blank=True)
186

    
187
    #for invitations
188
    user_level = DEFAULT_USER_LEVEL
189
    level = models.IntegerField('Inviter level', default=user_level)
190
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
191

    
192
    auth_token = models.CharField('Authentication Token', max_length=32,
193
                                  null=True, blank=True)
194
    auth_token_created = models.DateTimeField('Token creation date', null=True)
195
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
196

    
197
    updated = models.DateTimeField('Update date')
198
    is_verified = models.BooleanField('Is verified?', default=False)
199

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

    
203
    email_verified = models.BooleanField('Email verified?', default=False)
204

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

    
237
    @realname.setter
238
    def realname(self, value):
239
        parts = value.split(' ')
240
        if len(parts) == 2:
241
            self.first_name = parts[0]
242
            self.last_name = parts[1]
243
        else:
244
            self.last_name = parts[0]
245

    
246
    @property
247
    def invitation(self):
248
        try:
249
            return Invitation.objects.get(username=self.email)
250
        except Invitation.DoesNotExist:
251
            return None
252

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

    
296
        self.auth_token = b64encode(md5.digest())
297
        self.auth_token_created = datetime.now()
298
        self.auth_token_expires = self.auth_token_created + \
299
                                  timedelta(hours=AUTH_TOKEN_DURATION)
300
        msg = 'Token renewed for %s' % self.email
301
        logger._log(LOGGING_LEVEL, msg, [])
302

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

    
346
class Membership(models.Model):
347
    person = models.ForeignKey(AstakosUser)
348
    group = models.ForeignKey(AstakosGroup)
349
    date_requested = models.DateField(default=datetime.now())
350
    date_joined = models.DateField(null=True, db_index=True)
351
    
352
    class Meta:
353
        unique_together = ("person", "group")
354
    
355
    @property
356
    def is_approved(self):
357
        if self.date_joined:
358
            return True
359
        return False
360

    
361
class AstakosGroupQuota(models.Model):
362
    limit = models.PositiveIntegerField('Limit')
363
    resource = models.ForeignKey(Resource)
364
    group = models.ForeignKey(AstakosGroup, blank=True)
365
    
366
    class Meta:
367
        unique_together = ("resource", "group")
368

    
369
class AstakosUserQuota(models.Model):
370
    limit = models.PositiveIntegerField('Limit')
371
    resource = models.ForeignKey(Resource)
372
    user = models.ForeignKey(AstakosUser)
373
    
374
    class Meta:
375
        unique_together = ("resource", "user")
376

    
377
class ApprovalTerms(models.Model):
378
    """
379
    Model for approval terms
380
    """
381

    
382
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
383
    location = models.CharField('Terms location', max_length=255)
384

    
385
class Invitation(models.Model):
386
    """
387
    Model for registring invitations
388
    """
389
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
390
                                null=True)
391
    realname = models.CharField('Real name', max_length=255)
392
    username = models.CharField('Unique ID', max_length=255, unique=True)
393
    code = models.BigIntegerField('Invitation code', db_index=True)
394
    is_consumed = models.BooleanField('Consumed?', default=False)
395
    created = models.DateTimeField('Creation date', auto_now_add=True)
396
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
397
    
398
    def __init__(self, *args, **kwargs):
399
        super(Invitation, self).__init__(*args, **kwargs)
400
        if not self.id:
401
            self.code = _generate_invitation_code()
402
    
403
    def consume(self):
404
        self.is_consumed = True
405
        self.consumed = datetime.now()
406
        self.save()
407

    
408
    def __unicode__(self):
409
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
410

    
411
def report_user_event(user):
412
    def should_send(user):
413
        # report event incase of new user instance
414
        # or if specific fields are modified
415
        if not user.id:
416
            return True
417
        try:
418
            db_instance = AstakosUser.objects.get(id = user.id)
419
        except AstakosUser.DoesNotExist:
420
            return True
421
        for f in BILLING_FIELDS:
422
            if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
423
                return True
424
        return False
425

    
426
    if QUEUE_CONNECTION and should_send(user):
427

    
428
        from astakos.im.queue.userevent import UserEvent
429
        from synnefo.lib.queue import exchange_connect, exchange_send, \
430
                exchange_close
431

    
432
        eventType = 'create' if not user.id else 'modify'
433
        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
434
        conn = exchange_connect(QUEUE_CONNECTION)
435
        parts = urlparse(QUEUE_CONNECTION)
436
        exchange = parts.path[1:]
437
        routing_key = '%s.user' % exchange
438
        exchange_send(conn, routing_key, body)
439
        exchange_close(conn)
440

    
441
def _generate_invitation_code():
442
    while True:
443
        code = randint(1, 2L**63 - 1)
444
        try:
445
            Invitation.objects.get(code=code)
446
            # An invitation with this code already exists, try again
447
        except Invitation.DoesNotExist:
448
            return code
449

    
450
def get_latest_terms():
451
    try:
452
        term = ApprovalTerms.objects.order_by('-id')[0]
453
        return term
454
    except IndexError:
455
        pass
456
    return None
457

    
458
class EmailChangeManager(models.Manager):
459
    @transaction.commit_on_success
460
    def change_email(self, activation_key):
461
        """
462
        Validate an activation key and change the corresponding
463
        ``User`` if valid.
464

465
        If the key is valid and has not expired, return the ``User``
466
        after activating.
467

468
        If the key is not valid or has expired, return ``None``.
469

470
        If the key is valid but the ``User`` is already active,
471
        return ``None``.
472

473
        After successful email change the activation record is deleted.
474

475
        Throws ValueError if there is already
476
        """
477
        try:
478
            email_change = self.model.objects.get(activation_key=activation_key)
479
            if email_change.activation_key_expired():
480
                email_change.delete()
481
                raise EmailChange.DoesNotExist
482
            # is there an active user with this address?
483
            try:
484
                AstakosUser.objects.get(email=email_change.new_email_address)
485
            except AstakosUser.DoesNotExist:
486
                pass
487
            else:
488
                raise ValueError(_('The new email address is reserved.'))
489
            # update user
490
            user = AstakosUser.objects.get(pk=email_change.user_id)
491
            user.email = email_change.new_email_address
492
            user.save()
493
            email_change.delete()
494
            return user
495
        except EmailChange.DoesNotExist:
496
            raise ValueError(_('Invalid activation key'))
497

    
498
class EmailChange(models.Model):
499
    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.'))
500
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
501
    requested_at = models.DateTimeField(default=datetime.now())
502
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
503

    
504
    objects = EmailChangeManager()
505

    
506
    def activation_key_expired(self):
507
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
508
        return self.requested_at + expiration_date < datetime.now()
509

    
510
class AdditionalMail(models.Model):
511
    """
512
    Model for registring invitations
513
    """
514
    owner = models.ForeignKey(AstakosUser)
515
    email = models.EmailField()
516

    
517
def create_astakos_user(u):
518
    try:
519
        AstakosUser.objects.get(user_ptr=u.pk)
520
    except AstakosUser.DoesNotExist:
521
        extended_user = AstakosUser(user_ptr_id=u.pk)
522
        extended_user.__dict__.update(u.__dict__)
523
        extended_user.renew_token()
524
        extended_user.save()
525
    except:
526
        pass
527

    
528
def superuser_post_syncdb(sender, **kwargs):
529
    # if there was created a superuser
530
    # associate it with an AstakosUser
531
    admins = User.objects.filter(is_superuser=True)
532
    for u in admins:
533
        create_astakos_user(u)
534

    
535
post_syncdb.connect(superuser_post_syncdb)
536

    
537
def superuser_post_save(sender, instance, **kwargs):
538
    if instance.is_superuser:
539
        create_astakos_user(instance)
540

    
541
post_save.connect(superuser_post_save, sender=User)