1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
38 from time import asctime
39 from datetime import datetime, timedelta
40 from base64 import b64encode
41 from random import randint
42 from collections import defaultdict
44 from django.db import models
45 from django.contrib.auth.models import User, UserManager, Group
46 from django.utils.translation import ugettext as _
47 from django.core.exceptions import ValidationError
48 from django.db import transaction
49 from django.db.models.signals import pre_save, post_save, post_syncdb, post_delete
50 from django.dispatch import Signal
51 from django.db.models import Q
53 from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
54 AUTH_TOKEN_DURATION, BILLING_FIELDS,
55 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
56 from astakos.im.endpoints.quotaholder import (register_users, send_quota,
58 from astakos.im.endpoints.aquarium.producer import report_user_event
60 from astakos.im.tasks import propagate_groupmembers_quota
62 logger = logging.getLogger(__name__)
65 class Service(models.Model):
66 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
67 url = models.FilePathField()
68 icon = models.FilePathField(blank=True)
69 auth_token = models.CharField('Authentication Token', max_length=32,
70 null=True, blank=True)
71 auth_token_created = models.DateTimeField('Token creation date', null=True)
72 auth_token_expires = models.DateTimeField(
73 'Token expiration date', null=True)
75 def save(self, **kwargs):
79 super(Service, self).save(**kwargs)
81 def renew_token(self):
83 md5.update(self.name.encode('ascii', 'ignore'))
84 md5.update(self.url.encode('ascii', 'ignore'))
87 self.auth_token = b64encode(md5.digest())
88 self.auth_token_created = datetime.now()
89 self.auth_token_expires = self.auth_token_created + \
90 timedelta(hours=AUTH_TOKEN_DURATION)
96 class ResourceMetadata(models.Model):
97 key = models.CharField('Name', max_length=255, unique=True, db_index=True)
98 value = models.CharField('Value', max_length=255)
101 class Resource(models.Model):
102 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
103 meta = models.ManyToManyField(ResourceMetadata)
104 service = models.ForeignKey(Service)
107 return '%s.%s' % (self.service, self.name)
110 class GroupKind(models.Model):
111 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
117 class AstakosGroup(Group):
118 kind = models.ForeignKey(GroupKind)
119 homepage = models.URLField(
120 'Homepage Url', max_length=255, null=True, blank=True)
121 desc = models.TextField('Description', null=True)
122 policy = models.ManyToManyField(Resource, null=True, blank=True,
123 through='AstakosGroupQuota')
124 creation_date = models.DateTimeField('Creation date',
125 default=datetime.now())
126 issue_date = models.DateTimeField('Issue date', null=True)
127 expiration_date = models.DateTimeField('Expiration date', null=True)
128 moderation_enabled = models.BooleanField('Moderated membership?',
130 approval_date = models.DateTimeField('Activation date', null=True,
132 estimated_participants = models.PositiveIntegerField('Estimated #members',
136 def is_disabled(self):
137 if not self.approval_date:
142 def is_enabled(self):
145 if not self.issue_date:
147 if not self.expiration_date:
150 if self.issue_date > now:
152 if now >= self.expiration_date:
159 self.approval_date = datetime.now()
161 quota_disturbed.send(sender=self, users=self.approved_members)
162 propagate_groupmembers_quota.apply_async(
163 args=[self], eta=self.issue_date)
164 propagate_groupmembers_quota.apply_async(
165 args=[self], eta=self.expiration_date)
170 self.approval_date = None
172 quota_disturbed.send(sender=self, users=self.approved_members)
174 def approve_member(self, person):
175 m, created = self.membership_set.get_or_create(person=person)
176 # update date_joined in any case
177 m.date_joined = datetime.now()
180 def disapprove_member(self, person):
181 self.membership_set.remove(person=person)
185 q = self.membership_set.select_related().all()
186 return [m.person for m in q]
189 def approved_members(self):
190 q = self.membership_set.select_related().all()
191 return [m.person for m in q if m.is_approved]
196 for q in self.astakosgroupquota_set.select_related().all():
197 d[q.resource] += q.uplimit
202 return self.owner.all()
205 def owner_details(self):
206 return self.owner.select_related().all()
211 map(self.approve_member, l)
214 class AstakosUser(User):
216 Extends ``django.contrib.auth.models.User`` by defining additional fields.
218 # Use UserManager to get the create_user method, etc.
219 objects = UserManager()
221 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
222 provider = models.CharField('Provider', max_length=255, blank=True)
225 user_level = DEFAULT_USER_LEVEL
226 level = models.IntegerField('Inviter level', default=user_level)
227 invitations = models.IntegerField(
228 'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
230 auth_token = models.CharField('Authentication Token', max_length=32,
231 null=True, blank=True)
232 auth_token_created = models.DateTimeField('Token creation date', null=True)
233 auth_token_expires = models.DateTimeField(
234 'Token expiration date', null=True)
236 updated = models.DateTimeField('Update date')
237 is_verified = models.BooleanField('Is verified?', default=False)
239 # ex. screen_name for twitter, eppn for shibboleth
240 third_party_identifier = models.CharField(
241 'Third-party identifier', max_length=255, null=True, blank=True)
243 email_verified = models.BooleanField('Email verified?', default=False)
245 has_credits = models.BooleanField('Has credits?', default=False)
246 has_signed_terms = models.BooleanField(
247 'Agree with the terms?', default=False)
248 date_signed_terms = models.DateTimeField(
249 'Signed terms date', null=True, blank=True)
251 activation_sent = models.DateTimeField(
252 'Activation sent data', null=True, blank=True)
254 policy = models.ManyToManyField(
255 Resource, null=True, through='AstakosUserQuota')
257 astakos_groups = models.ManyToManyField(
258 AstakosGroup, verbose_name=_('agroups'), blank=True,
259 help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
260 through='Membership')
262 __has_signed_terms = False
264 owner = models.ManyToManyField(
265 AstakosGroup, related_name='owner', null=True)
268 unique_together = ("provider", "third_party_identifier")
270 def __init__(self, *args, **kwargs):
271 super(AstakosUser, self).__init__(*args, **kwargs)
272 self.__has_signed_terms = self.has_signed_terms
274 self.is_active = False
278 return '%s %s' % (self.first_name, self.last_name)
281 def realname(self, value):
282 parts = value.split(' ')
284 self.first_name = parts[0]
285 self.last_name = parts[1]
287 self.last_name = parts[0]
290 def invitation(self):
292 return Invitation.objects.get(username=self.email)
293 except Invitation.DoesNotExist:
299 for q in self.astakosuserquota_set.select_related().all():
300 d[q.resource] += q.uplimit
301 for m in self.membership_set.select_related().all():
302 if not m.is_approved:
307 for r, uplimit in g.quota.iteritems():
310 # TODO set default for remaining
313 def save(self, update_timestamps=True, **kwargs):
314 if update_timestamps:
316 self.date_joined = datetime.now()
317 self.updated = datetime.now()
319 # update date_signed_terms if necessary
320 if self.__has_signed_terms != self.has_signed_terms:
321 self.date_signed_terms = datetime.now()
325 while not self.username:
326 username = uuid.uuid4().hex[:30]
328 AstakosUser.objects.get(username=username)
329 except AstakosUser.DoesNotExist:
330 self.username = username
331 if not self.provider:
332 self.provider = 'local'
333 self.validate_unique_email_isactive()
334 if self.is_active and self.activation_sent:
335 # reset the activation sent
336 self.activation_sent = None
338 super(AstakosUser, self).save(**kwargs)
340 def renew_token(self):
342 md5.update(self.username)
343 md5.update(self.realname.encode('ascii', 'ignore'))
344 md5.update(asctime())
346 self.auth_token = b64encode(md5.digest())
347 self.auth_token_created = datetime.now()
348 self.auth_token_expires = self.auth_token_created + \
349 timedelta(hours=AUTH_TOKEN_DURATION)
350 msg = 'Token renewed for %s' % self.email
351 logger.log(LOGGING_LEVEL, msg)
353 def __unicode__(self):
354 return '%s (%s)' % (self.realname, self.email)
356 def conflicting_email(self):
357 q = AstakosUser.objects.exclude(username=self.username)
358 q = q.filter(email=self.email)
363 def validate_unique_email_isactive(self):
365 Implements a unique_together constraint for email and is_active fields.
367 q = AstakosUser.objects.exclude(username=self.username)
368 q = q.filter(email=self.email)
369 q = q.filter(is_active=self.is_active)
371 raise ValidationError({'__all__': [_('Another account with the same email & is_active combination found.')]})
374 def signed_terms(self):
375 term = get_latest_terms()
378 if not self.has_signed_terms:
380 if not self.date_signed_terms:
382 if self.date_signed_terms < term.date:
383 self.has_signed_terms = False
384 self.date_signed_terms = None
390 class Membership(models.Model):
391 person = models.ForeignKey(AstakosUser)
392 group = models.ForeignKey(AstakosGroup)
393 date_requested = models.DateField(default=datetime.now(), blank=True)
394 date_joined = models.DateField(null=True, db_index=True, blank=True)
397 unique_together = ("person", "group")
399 def save(self, *args, **kwargs):
401 if not self.group.moderation_enabled:
402 self.date_joined = datetime.now()
403 super(Membership, self).save(*args, **kwargs)
406 def is_approved(self):
412 self.date_joined = datetime.now()
414 quota_disturbed.send(sender=self, users=(self.person,))
416 def disapprove(self):
418 quota_disturbed.send(sender=self, users=(self.person,))
421 class AstakosGroupQuota(models.Model):
422 limit = models.PositiveIntegerField('Limit', null=True) # obsolete field
423 uplimit = models.BigIntegerField('Up limit', null=True)
424 resource = models.ForeignKey(Resource)
425 group = models.ForeignKey(AstakosGroup, blank=True)
428 unique_together = ("resource", "group")
431 class AstakosUserQuota(models.Model):
432 limit = models.PositiveIntegerField('Limit', null=True) # obsolete field
433 uplimit = models.BigIntegerField('Up limit', null=True)
434 resource = models.ForeignKey(Resource)
435 user = models.ForeignKey(AstakosUser)
438 unique_together = ("resource", "user")
441 class ApprovalTerms(models.Model):
443 Model for approval terms
446 date = models.DateTimeField(
447 'Issue date', db_index=True, default=datetime.now())
448 location = models.CharField('Terms location', max_length=255)
451 class Invitation(models.Model):
453 Model for registring invitations
455 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
457 realname = models.CharField('Real name', max_length=255)
458 username = models.CharField('Unique ID', max_length=255, unique=True)
459 code = models.BigIntegerField('Invitation code', db_index=True)
460 is_consumed = models.BooleanField('Consumed?', default=False)
461 created = models.DateTimeField('Creation date', auto_now_add=True)
462 consumed = models.DateTimeField('Consumption date', null=True, blank=True)
464 def __init__(self, *args, **kwargs):
465 super(Invitation, self).__init__(*args, **kwargs)
467 self.code = _generate_invitation_code()
470 self.is_consumed = True
471 self.consumed = datetime.now()
474 def __unicode__(self):
475 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
478 class EmailChangeManager(models.Manager):
479 @transaction.commit_on_success
480 def change_email(self, activation_key):
482 Validate an activation key and change the corresponding
485 If the key is valid and has not expired, return the ``User``
488 If the key is not valid or has expired, return ``None``.
490 If the key is valid but the ``User`` is already active,
493 After successful email change the activation record is deleted.
495 Throws ValueError if there is already
498 email_change = self.model.objects.get(
499 activation_key=activation_key)
500 if email_change.activation_key_expired():
501 email_change.delete()
502 raise EmailChange.DoesNotExist
503 # is there an active user with this address?
505 AstakosUser.objects.get(email=email_change.new_email_address)
506 except AstakosUser.DoesNotExist:
509 raise ValueError(_('The new email address is reserved.'))
511 user = AstakosUser.objects.get(pk=email_change.user_id)
512 user.email = email_change.new_email_address
514 email_change.delete()
516 except EmailChange.DoesNotExist:
517 raise ValueError(_('Invalid activation key'))
520 class EmailChange(models.Model):
521 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.'))
522 user = models.ForeignKey(
523 AstakosUser, unique=True, related_name='emailchange_user')
524 requested_at = models.DateTimeField(default=datetime.now())
525 activation_key = models.CharField(
526 max_length=40, unique=True, db_index=True)
528 objects = EmailChangeManager()
530 def activation_key_expired(self):
531 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
532 return self.requested_at + expiration_date < datetime.now()
535 class AdditionalMail(models.Model):
537 Model for registring invitations
539 owner = models.ForeignKey(AstakosUser)
540 email = models.EmailField()
543 def _generate_invitation_code():
545 code = randint(1, 2L ** 63 - 1)
547 Invitation.objects.get(code=code)
548 # An invitation with this code already exists, try again
549 except Invitation.DoesNotExist:
553 def get_latest_terms():
555 term = ApprovalTerms.objects.order_by('-id')[0]
562 def create_astakos_user(u):
564 AstakosUser.objects.get(user_ptr=u.pk)
565 except AstakosUser.DoesNotExist:
566 extended_user = AstakosUser(user_ptr_id=u.pk)
567 extended_user.__dict__.update(u.__dict__)
568 extended_user.renew_token()
570 except BaseException, e:
575 def fix_superusers(sender, **kwargs):
576 # Associate superusers with AstakosUser
577 admins = User.objects.filter(is_superuser=True)
579 create_astakos_user(u)
582 def user_post_save(sender, instance, created, **kwargs):
585 create_astakos_user(instance)
588 def set_default_group(user):
590 default = AstakosGroup.objects.get(name='default')
592 group=default, person=user, date_joined=datetime.now()).save()
593 except AstakosGroup.DoesNotExist, e:
597 def astakosuser_pre_save(sender, instance, **kwargs):
598 instance.aquarium_report = False
601 db_instance = AstakosUser.objects.get(id=instance.id)
602 except AstakosUser.DoesNotExist:
604 instance.aquarium_report = True
607 get = AstakosUser.__getattribute__
608 l = filter(lambda f: get(db_instance, f) != get(instance, f),
611 instance.aquarium_report = True if l else False
614 def astakosuser_post_save(sender, instance, created, **kwargs):
615 if instance.aquarium_report:
616 report_user_event(instance, create=instance.new)
619 set_default_group(instance)
620 # TODO handle socket.error & IOError
621 register_users((instance,))
624 def resource_post_save(sender, instance, created, **kwargs):
627 register_resources((instance,))
630 def send_quota_disturbed(sender, instance, **kwargs):
632 extend = users.extend
633 if sender == Membership:
634 if not instance.group.is_enabled:
636 extend([instance.person])
637 elif sender == AstakosUserQuota:
638 extend([instance.user])
639 elif sender == AstakosGroupQuota:
640 if not instance.group.is_enabled:
642 extend(instance.group.astakosuser_set.all())
643 elif sender == AstakosGroup:
644 if not instance.is_enabled:
646 quota_disturbed.send(sender=sender, users=users)
649 def on_quota_disturbed(sender, users, **kwargs):
650 print '>>>', locals()
655 post_syncdb.connect(fix_superusers)
656 post_save.connect(user_post_save, sender=User)
657 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
658 post_save.connect(astakosuser_post_save, sender=AstakosUser)
659 post_save.connect(resource_post_save, sender=Resource)
661 quota_disturbed = Signal(providing_args=["users"])
662 quota_disturbed.connect(on_quota_disturbed)
664 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
665 post_delete.connect(send_quota_disturbed, sender=Membership)
666 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
667 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
668 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
669 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)