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)
186 for m in self.membership_set.all():
187 m.person.is_approved = m.is_approved
192 def approved_members(self):
193 return [m.person for m in self.membership_set.all() if m.is_approved]
198 for q in self.astakosgroupquota_set.all():
199 d[q.resource] += q.uplimit
204 return self.owner.all()
209 map(self.approve_member, l)
212 class AstakosUser(User):
214 Extends ``django.contrib.auth.models.User`` by defining additional fields.
216 # Use UserManager to get the create_user method, etc.
217 objects = UserManager()
219 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
220 provider = models.CharField('Provider', max_length=255, blank=True)
223 user_level = DEFAULT_USER_LEVEL
224 level = models.IntegerField('Inviter level', default=user_level)
225 invitations = models.IntegerField(
226 'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
228 auth_token = models.CharField('Authentication Token', max_length=32,
229 null=True, blank=True)
230 auth_token_created = models.DateTimeField('Token creation date', null=True)
231 auth_token_expires = models.DateTimeField(
232 'Token expiration date', null=True)
234 updated = models.DateTimeField('Update date')
235 is_verified = models.BooleanField('Is verified?', default=False)
237 # ex. screen_name for twitter, eppn for shibboleth
238 third_party_identifier = models.CharField(
239 'Third-party identifier', max_length=255, null=True, blank=True)
241 email_verified = models.BooleanField('Email verified?', default=False)
243 has_credits = models.BooleanField('Has credits?', default=False)
244 has_signed_terms = models.BooleanField(
245 'Agree with the terms?', default=False)
246 date_signed_terms = models.DateTimeField(
247 'Signed terms date', null=True, blank=True)
249 activation_sent = models.DateTimeField(
250 'Activation sent data', null=True, blank=True)
252 policy = models.ManyToManyField(
253 Resource, null=True, through='AstakosUserQuota')
255 astakos_groups = models.ManyToManyField(
256 AstakosGroup, verbose_name=_('agroups'), blank=True,
257 help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
258 through='Membership')
260 __has_signed_terms = False
262 owner = models.ManyToManyField(
263 AstakosGroup, related_name='owner', null=True)
266 unique_together = ("provider", "third_party_identifier")
268 def __init__(self, *args, **kwargs):
269 super(AstakosUser, self).__init__(*args, **kwargs)
270 self.__has_signed_terms = self.has_signed_terms
272 self.is_active = False
276 return '%s %s' % (self.first_name, self.last_name)
279 def realname(self, value):
280 parts = value.split(' ')
282 self.first_name = parts[0]
283 self.last_name = parts[1]
285 self.last_name = parts[0]
288 def invitation(self):
290 return Invitation.objects.get(username=self.email)
291 except Invitation.DoesNotExist:
297 for q in self.astakosuserquota_set.all():
298 d[q.resource.name] += q.uplimit
299 for m in self.membership_set.all():
300 if not m.is_approved:
305 for r, uplimit in g.quota.iteritems():
307 # TODO set default for remaining
310 def save(self, update_timestamps=True, **kwargs):
311 if update_timestamps:
313 self.date_joined = datetime.now()
314 self.updated = datetime.now()
316 # update date_signed_terms if necessary
317 if self.__has_signed_terms != self.has_signed_terms:
318 self.date_signed_terms = datetime.now()
322 while not self.username:
323 username = uuid.uuid4().hex[:30]
325 AstakosUser.objects.get(username=username)
326 except AstakosUser.DoesNotExist:
327 self.username = username
328 if not self.provider:
329 self.provider = 'local'
330 self.validate_unique_email_isactive()
331 if self.is_active and self.activation_sent:
332 # reset the activation sent
333 self.activation_sent = None
335 super(AstakosUser, self).save(**kwargs)
337 def renew_token(self):
339 md5.update(self.username)
340 md5.update(self.realname.encode('ascii', 'ignore'))
341 md5.update(asctime())
343 self.auth_token = b64encode(md5.digest())
344 self.auth_token_created = datetime.now()
345 self.auth_token_expires = self.auth_token_created + \
346 timedelta(hours=AUTH_TOKEN_DURATION)
347 msg = 'Token renewed for %s' % self.email
348 logger.log(LOGGING_LEVEL, msg)
350 def __unicode__(self):
351 return '%s (%s)' % (self.realname, self.email)
353 def conflicting_email(self):
354 q = AstakosUser.objects.exclude(username=self.username)
355 q = q.filter(email=self.email)
360 def validate_unique_email_isactive(self):
362 Implements a unique_together constraint for email and is_active fields.
364 q = AstakosUser.objects.exclude(username=self.username)
365 q = q.filter(email=self.email)
366 q = q.filter(is_active=self.is_active)
368 raise ValidationError({'__all__': [_('Another account with the same email & is_active combination found.')]})
371 def signed_terms(self):
372 term = get_latest_terms()
375 if not self.has_signed_terms:
377 if not self.date_signed_terms:
379 if self.date_signed_terms < term.date:
380 self.has_signed_terms = False
381 self.date_signed_terms = None
387 class Membership(models.Model):
388 person = models.ForeignKey(AstakosUser)
389 group = models.ForeignKey(AstakosGroup)
390 date_requested = models.DateField(default=datetime.now(), blank=True)
391 date_joined = models.DateField(null=True, db_index=True, blank=True)
394 unique_together = ("person", "group")
396 def save(self, *args, **kwargs):
398 if not self.group.moderation_enabled:
399 self.date_joined = datetime.now()
400 super(Membership, self).save(*args, **kwargs)
403 def is_approved(self):
409 self.date_joined = datetime.now()
411 quota_disturbed.send(sender=self, users=(self.person,))
413 def disapprove(self):
415 quota_disturbed.send(sender=self, users=(self.person,))
418 class AstakosGroupQuota(models.Model):
419 limit = models.PositiveIntegerField('Limit') # obsolete field
420 uplimit = models.BigIntegerField('Up limit', null=True)
421 resource = models.ForeignKey(Resource)
422 group = models.ForeignKey(AstakosGroup, blank=True)
425 unique_together = ("resource", "group")
428 class AstakosUserQuota(models.Model):
429 limit = models.PositiveIntegerField('Limit') # obsolete field
430 uplimit = models.BigIntegerField('Up limit', null=True)
431 resource = models.ForeignKey(Resource)
432 user = models.ForeignKey(AstakosUser)
435 unique_together = ("resource", "user")
438 class ApprovalTerms(models.Model):
440 Model for approval terms
443 date = models.DateTimeField(
444 'Issue date', db_index=True, default=datetime.now())
445 location = models.CharField('Terms location', max_length=255)
448 class Invitation(models.Model):
450 Model for registring invitations
452 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
454 realname = models.CharField('Real name', max_length=255)
455 username = models.CharField('Unique ID', max_length=255, unique=True)
456 code = models.BigIntegerField('Invitation code', db_index=True)
457 is_consumed = models.BooleanField('Consumed?', default=False)
458 created = models.DateTimeField('Creation date', auto_now_add=True)
459 consumed = models.DateTimeField('Consumption date', null=True, blank=True)
461 def __init__(self, *args, **kwargs):
462 super(Invitation, self).__init__(*args, **kwargs)
464 self.code = _generate_invitation_code()
467 self.is_consumed = True
468 self.consumed = datetime.now()
471 def __unicode__(self):
472 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
475 class EmailChangeManager(models.Manager):
476 @transaction.commit_on_success
477 def change_email(self, activation_key):
479 Validate an activation key and change the corresponding
482 If the key is valid and has not expired, return the ``User``
485 If the key is not valid or has expired, return ``None``.
487 If the key is valid but the ``User`` is already active,
490 After successful email change the activation record is deleted.
492 Throws ValueError if there is already
495 email_change = self.model.objects.get(
496 activation_key=activation_key)
497 if email_change.activation_key_expired():
498 email_change.delete()
499 raise EmailChange.DoesNotExist
500 # is there an active user with this address?
502 AstakosUser.objects.get(email=email_change.new_email_address)
503 except AstakosUser.DoesNotExist:
506 raise ValueError(_('The new email address is reserved.'))
508 user = AstakosUser.objects.get(pk=email_change.user_id)
509 user.email = email_change.new_email_address
511 email_change.delete()
513 except EmailChange.DoesNotExist:
514 raise ValueError(_('Invalid activation key'))
517 class EmailChange(models.Model):
518 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.'))
519 user = models.ForeignKey(
520 AstakosUser, unique=True, related_name='emailchange_user')
521 requested_at = models.DateTimeField(default=datetime.now())
522 activation_key = models.CharField(
523 max_length=40, unique=True, db_index=True)
525 objects = EmailChangeManager()
527 def activation_key_expired(self):
528 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
529 return self.requested_at + expiration_date < datetime.now()
532 class AdditionalMail(models.Model):
534 Model for registring invitations
536 owner = models.ForeignKey(AstakosUser)
537 email = models.EmailField()
540 def _generate_invitation_code():
542 code = randint(1, 2L ** 63 - 1)
544 Invitation.objects.get(code=code)
545 # An invitation with this code already exists, try again
546 except Invitation.DoesNotExist:
550 def get_latest_terms():
552 term = ApprovalTerms.objects.order_by('-id')[0]
559 def create_astakos_user(u):
561 AstakosUser.objects.get(user_ptr=u.pk)
562 except AstakosUser.DoesNotExist:
563 extended_user = AstakosUser(user_ptr_id=u.pk)
564 extended_user.__dict__.update(u.__dict__)
565 extended_user.renew_token()
567 except BaseException, e:
572 def fix_superusers(sender, **kwargs):
573 # Associate superusers with AstakosUser
574 admins = User.objects.filter(is_superuser=True)
576 create_astakos_user(u)
579 def user_post_save(sender, instance, created, **kwargs):
582 create_astakos_user(instance)
585 def set_default_group(user):
587 default = AstakosGroup.objects.get(name='default')
589 group=default, person=user, date_joined=datetime.now()).save()
590 except AstakosGroup.DoesNotExist, e:
594 def astakosuser_pre_save(sender, instance, **kwargs):
595 instance.aquarium_report = False
598 db_instance = AstakosUser.objects.get(id=instance.id)
599 except AstakosUser.DoesNotExist:
601 instance.aquarium_report = True
604 get = AstakosUser.__getattribute__
605 l = filter(lambda f: get(db_instance, f) != get(instance, f),
608 instance.aquarium_report = True if l else False
611 def astakosuser_post_save(sender, instance, created, **kwargs):
612 if instance.aquarium_report:
613 report_user_event(instance, create=instance.new)
616 set_default_group(instance)
617 # TODO handle socket.error & IOError
618 register_users((instance,))
621 def resource_post_save(sender, instance, created, **kwargs):
624 register_resources((instance,))
627 def send_quota_disturbed(sender, instance, **kwargs):
629 extend = users.extend
630 if sender == Membership:
631 if not instance.group.is_enabled:
633 extend([instance.person])
634 elif sender == AstakosUserQuota:
635 extend([instance.user])
636 elif sender == AstakosGroupQuota:
637 if not instance.group.is_enabled:
639 extend(instance.group.astakosuser_set.all())
640 elif sender == AstakosGroup:
641 if not instance.is_enabled:
643 quota_disturbed.send(sender=sender, users=users)
646 def on_quota_disturbed(sender, users, **kwargs):
647 print '>>>', locals()
652 post_syncdb.connect(fix_superusers)
653 post_save.connect(user_post_save, sender=User)
654 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
655 post_save.connect(astakosuser_post_save, sender=AstakosUser)
656 post_save.connect(resource_post_save, sender=Resource)
658 quota_disturbed = Signal(providing_args=["users"])
659 quota_disturbed.connect(on_quota_disturbed)
661 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
662 post_delete.connect(send_quota_disturbed, sender=Membership)
663 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
664 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
665 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
666 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)