Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 373daf6a

History | View | Annotate | Download (18.8 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
    policy = models.ManyToManyField(Resource, null=True, blank=True, through='AstakosGroupQuota')
113
    creation_date = models.DateTimeField('Creation date', default=datetime.now())
114
    issue_date = models.DateTimeField('Issue date', null=True)
115
    expiration_date = models.DateTimeField('Expiration date', null=True)
116
    moderatation_enabled = models.BooleanField('Moderated membership?', default=True)
117
    approval_date = models.DateTimeField('Activation date', null=True, blank=True)
118
    estimated_participants = models.PositiveIntegerField('Estimated #participants', null=True)
119
    
120
    @property
121
    def is_disabled(self):
122
        if not approval_date:
123
            return False
124
        return True
125
    
126
    @property
127
    def is_active(self):
128
        if self.is_disabled:
129
            return False
130
        if not self.issue_date:
131
            return False
132
        if not self.expiration_date:
133
            return True
134
        now = datetime.now()
135
        if self.issue_date > now:
136
            return False
137
        if now >= self.expiration_date:
138
            return False
139
        return True
140
    
141
    @property
142
    def participants(self):
143
        if not self.id:
144
            return 0
145
        return self.user_set.count()
146
    
147
    def approve(self):
148
        self.approval_date = datetime.now()
149
        self.save()
150
    
151
    def disapprove(self):
152
        self.approval_date = None
153
        self.save()
154
    
155
    def approve_member(self, member):
156
        m, created = self.membership_set.get_or_create(person=member, group=self)
157
        m.date_joined = datetime.now()
158
        m.save()
159
        
160
    def disapprove_member(self, member):
161
        m = self.membership_set.remove(member)
162
    
163
    def get_members(self, approved=True):
164
        if approved:
165
            return self.membership_set().filter(is_approved=True)
166
        return self.membership_set().all()
167

    
168
class AstakosUser(User):
169
    """
170
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
171
    """
172
    # Use UserManager to get the create_user method, etc.
173
    objects = UserManager()
174

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
417
    if QUEUE_CONNECTION and should_send(user):
418

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

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

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

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

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

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

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

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

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

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

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

    
495
    objects = EmailChangeManager()
496

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

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

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

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

    
526
post_syncdb.connect(superuser_post_syncdb)
527

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

    
532
post_save.connect(superuser_post_save, sender=User)