Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 01ac12d5

History | View | Annotate | Download (19.4 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
        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
    @property
362
    def is_approved(self):
363
        if self.date_joined:
364
            return True
365
        return False
366
    
367
    def approve(self):
368
        self.date_joined = datetime.now()
369
        self.save()
370
        
371
    def disapprove(self):
372
        self.delete()
373

    
374
class AstakosGroupQuota(models.Model):
375
    limit = models.PositiveIntegerField('Limit')
376
    resource = models.ForeignKey(Resource)
377
    group = models.ForeignKey(AstakosGroup, blank=True)
378
    
379
    class Meta:
380
        unique_together = ("resource", "group")
381

    
382
class AstakosUserQuota(models.Model):
383
    limit = models.PositiveIntegerField('Limit')
384
    resource = models.ForeignKey(Resource)
385
    user = models.ForeignKey(AstakosUser)
386
    
387
    class Meta:
388
        unique_together = ("resource", "user")
389

    
390
class ApprovalTerms(models.Model):
391
    """
392
    Model for approval terms
393
    """
394

    
395
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
396
    location = models.CharField('Terms location', max_length=255)
397

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

    
421
    def __unicode__(self):
422
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
423

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

    
439
    if QUEUE_CONNECTION and should_send(user):
440

    
441
        from astakos.im.queue.userevent import UserEvent
442
        from synnefo.lib.queue import exchange_connect, exchange_send, \
443
                exchange_close
444

    
445
        eventType = 'create' if not user.id else 'modify'
446
        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
447
        conn = exchange_connect(QUEUE_CONNECTION)
448
        parts = urlparse(QUEUE_CONNECTION)
449
        exchange = parts.path[1:]
450
        routing_key = '%s.user' % exchange
451
        exchange_send(conn, routing_key, body)
452
        exchange_close(conn)
453

    
454
def _generate_invitation_code():
455
    while True:
456
        code = randint(1, 2L**63 - 1)
457
        try:
458
            Invitation.objects.get(code=code)
459
            # An invitation with this code already exists, try again
460
        except Invitation.DoesNotExist:
461
            return code
462

    
463
def get_latest_terms():
464
    try:
465
        term = ApprovalTerms.objects.order_by('-id')[0]
466
        return term
467
    except IndexError:
468
        pass
469
    return None
470

    
471
class EmailChangeManager(models.Manager):
472
    @transaction.commit_on_success
473
    def change_email(self, activation_key):
474
        """
475
        Validate an activation key and change the corresponding
476
        ``User`` if valid.
477

478
        If the key is valid and has not expired, return the ``User``
479
        after activating.
480

481
        If the key is not valid or has expired, return ``None``.
482

483
        If the key is valid but the ``User`` is already active,
484
        return ``None``.
485

486
        After successful email change the activation record is deleted.
487

488
        Throws ValueError if there is already
489
        """
490
        try:
491
            email_change = self.model.objects.get(activation_key=activation_key)
492
            if email_change.activation_key_expired():
493
                email_change.delete()
494
                raise EmailChange.DoesNotExist
495
            # is there an active user with this address?
496
            try:
497
                AstakosUser.objects.get(email=email_change.new_email_address)
498
            except AstakosUser.DoesNotExist:
499
                pass
500
            else:
501
                raise ValueError(_('The new email address is reserved.'))
502
            # update user
503
            user = AstakosUser.objects.get(pk=email_change.user_id)
504
            user.email = email_change.new_email_address
505
            user.save()
506
            email_change.delete()
507
            return user
508
        except EmailChange.DoesNotExist:
509
            raise ValueError(_('Invalid activation key'))
510

    
511
class EmailChange(models.Model):
512
    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.'))
513
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
514
    requested_at = models.DateTimeField(default=datetime.now())
515
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
516

    
517
    objects = EmailChangeManager()
518

    
519
    def activation_key_expired(self):
520
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
521
        return self.requested_at + expiration_date < datetime.now()
522

    
523
class AdditionalMail(models.Model):
524
    """
525
    Model for registring invitations
526
    """
527
    owner = models.ForeignKey(AstakosUser)
528
    email = models.EmailField()
529

    
530
def create_astakos_user(u):
531
    try:
532
        AstakosUser.objects.get(user_ptr=u.pk)
533
    except AstakosUser.DoesNotExist:
534
        extended_user = AstakosUser(user_ptr_id=u.pk)
535
        extended_user.__dict__.update(u.__dict__)
536
        extended_user.renew_token()
537
        extended_user.save()
538
    except:
539
        pass
540

    
541
def superuser_post_syncdb(sender, **kwargs):
542
    # if there was created a superuser
543
    # associate it with an AstakosUser
544
    admins = User.objects.filter(is_superuser=True)
545
    for u in admins:
546
        create_astakos_user(u)
547

    
548
post_syncdb.connect(superuser_post_syncdb)
549

    
550
def superuser_post_save(sender, instance, **kwargs):
551
    if instance.is_superuser:
552
        create_astakos_user(instance)
553

    
554
post_save.connect(superuser_post_save, sender=User)