Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 8e45d6fd

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

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

    
58
QUEUE_CLIENT_ID = 3 # Astakos.
59

    
60
logger = logging.getLogger(__name__)
61

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

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

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

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

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

    
109
class AstakosGroup(Group):
110
    kind = models.ForeignKey(GroupKind)
111
    desc = models.TextField('Description', null=True)
112
    identifier = models.URLField('URI identifier', unique=True, default='', db_index=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=False)
118
    approval_date = models.DateTimeField('Activation date', null=True, blank=True)
119
    estimated_participants = models.PositiveIntegerField('Estimated number of 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 = self.membership_set.get(person=member)
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
class AstakosUser(User):
170
    """
171
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
172
    """
173
    # Use UserManager to get the create_user method, etc.
174
    objects = UserManager()
175

    
176
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
177
    provider = models.CharField('Provider', max_length=255, blank=True)
178

    
179
    #for invitations
180
    user_level = DEFAULT_USER_LEVEL
181
    level = models.IntegerField('Inviter level', default=user_level)
182
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
183

    
184
    auth_token = models.CharField('Authentication Token', max_length=32,
185
                                  null=True, blank=True)
186
    auth_token_created = models.DateTimeField('Token creation date', null=True)
187
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
188

    
189
    updated = models.DateTimeField('Update date')
190
    is_verified = models.BooleanField('Is verified?', default=False)
191

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

    
195
    email_verified = models.BooleanField('Email verified?', default=False)
196

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

    
229
    @realname.setter
230
    def realname(self, value):
231
        parts = value.split(' ')
232
        if len(parts) == 2:
233
            self.first_name = parts[0]
234
            self.last_name = parts[1]
235
        else:
236
            self.last_name = parts[0]
237

    
238
    @property
239
    def invitation(self):
240
        try:
241
            return Invitation.objects.get(username=self.email)
242
        except Invitation.DoesNotExist:
243
            return None
244

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

    
288
        self.auth_token = b64encode(md5.digest())
289
        self.auth_token_created = datetime.now()
290
        self.auth_token_expires = self.auth_token_created + \
291
                                  timedelta(hours=AUTH_TOKEN_DURATION)
292
        msg = 'Token renewed for %s' % self.email
293
        logger._log(LOGGING_LEVEL, msg, [])
294

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

    
338
class Membership(models.Model):
339
    person = models.ForeignKey(AstakosUser)
340
    group = models.ForeignKey(AstakosGroup)
341
    date_requested = models.DateField(default=datetime.now())
342
    date_joined = models.DateField(null=True, db_index=True)
343
    
344
    class Meta:
345
        unique_together = ("person", "group")
346
    
347
    @property
348
    def is_approved(self):
349
        if self.date_joined:
350
            return True
351
        return False
352

    
353
class AstakosGroupQuota(models.Model):
354
    limit = models.PositiveIntegerField('Limit')
355
    resource = models.ForeignKey(Resource)
356
    group = models.ForeignKey(AstakosGroup, blank=True)
357
    
358
    class Meta:
359
        unique_together = ("resource", "group")
360

    
361
class AstakosUserQuota(models.Model):
362
    limit = models.PositiveIntegerField('Limit')
363
    resource = models.ForeignKey(Resource)
364
    user = models.ForeignKey(AstakosUser)
365
    
366
    class Meta:
367
        unique_together = ("resource", "user")
368

    
369
class ApprovalTerms(models.Model):
370
    """
371
    Model for approval terms
372
    """
373

    
374
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
375
    location = models.CharField('Terms location', max_length=255)
376

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

    
400
    def __unicode__(self):
401
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
402

    
403
def report_user_event(user):
404
    def should_send(user):
405
        # report event incase of new user instance
406
        # or if specific fields are modified
407
        if not user.id:
408
            return True
409
        try:
410
            db_instance = AstakosUser.objects.get(id = user.id)
411
        except AstakosUser.DoesNotExist:
412
            return True
413
        for f in BILLING_FIELDS:
414
            if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
415
                return True
416
        return False
417

    
418
    if QUEUE_CONNECTION and should_send(user):
419

    
420
        from astakos.im.queue.userevent import UserEvent
421
        from synnefo.lib.queue import exchange_connect, exchange_send, \
422
                exchange_close
423

    
424
        eventType = 'create' if not user.id else 'modify'
425
        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
426
        conn = exchange_connect(QUEUE_CONNECTION)
427
        parts = urlparse(QUEUE_CONNECTION)
428
        exchange = parts.path[1:]
429
        routing_key = '%s.user' % exchange
430
        exchange_send(conn, routing_key, body)
431
        exchange_close(conn)
432

    
433
def _generate_invitation_code():
434
    while True:
435
        code = randint(1, 2L**63 - 1)
436
        try:
437
            Invitation.objects.get(code=code)
438
            # An invitation with this code already exists, try again
439
        except Invitation.DoesNotExist:
440
            return code
441

    
442
def get_latest_terms():
443
    try:
444
        term = ApprovalTerms.objects.order_by('-id')[0]
445
        return term
446
    except IndexError:
447
        pass
448
    return None
449

    
450
class EmailChangeManager(models.Manager):
451
    @transaction.commit_on_success
452
    def change_email(self, activation_key):
453
        """
454
        Validate an activation key and change the corresponding
455
        ``User`` if valid.
456

457
        If the key is valid and has not expired, return the ``User``
458
        after activating.
459

460
        If the key is not valid or has expired, return ``None``.
461

462
        If the key is valid but the ``User`` is already active,
463
        return ``None``.
464

465
        After successful email change the activation record is deleted.
466

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

    
490
class EmailChange(models.Model):
491
    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.'))
492
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
493
    requested_at = models.DateTimeField(default=datetime.now())
494
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
495

    
496
    objects = EmailChangeManager()
497

    
498
    def activation_key_expired(self):
499
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
500
        return self.requested_at + expiration_date < datetime.now()
501

    
502
class AdditionalMail(models.Model):
503
    """
504
    Model for registring invitations
505
    """
506
    owner = models.ForeignKey(AstakosUser)
507
    email = models.EmailField()
508

    
509
def create_astakos_user(u):
510
    try:
511
        AstakosUser.objects.get(user_ptr=u.pk)
512
    except AstakosUser.DoesNotExist:
513
        extended_user = AstakosUser(user_ptr_id=u.pk)
514
        extended_user.__dict__.update(u.__dict__)
515
        extended_user.renew_token()
516
        extended_user.save()
517
    except:
518
        pass
519

    
520
def superuser_post_syncdb(sender, **kwargs):
521
    # if there was created a superuser
522
    # associate it with an AstakosUser
523
    admins = User.objects.filter(is_superuser=True)
524
    for u in admins:
525
        create_astakos_user(u)
526

    
527
post_syncdb.connect(superuser_post_syncdb)
528

    
529
def superuser_post_save(sender, instance, **kwargs):
530
    if instance.is_superuser:
531
        create_astakos_user(instance)
532

    
533
post_save.connect(superuser_post_save, sender=User)