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, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
56 from astakos.im.endpoints.quotaholder import register_users, send_quota
57 from astakos.im.endpoints.aquarium.producer import report_user_event
59 from astakos.im.tasks import propagate_groupmembers_quota
61 logger = logging.getLogger(__name__)
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)
72 def save(self, **kwargs):
76 super(Service, self).save(**kwargs)
78 def renew_token(self):
80 md5.update(self.name.encode('ascii', 'ignore'))
81 md5.update(self.url.encode('ascii', 'ignore'))
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)
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)
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)
102 return '%s : %s' % (self.service, self.name)
104 class GroupKind(models.Model):
105 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
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,
114 through='AstakosGroupQuota'
116 creation_date = models.DateTimeField('Creation date',
117 default=datetime.now()
119 issue_date = models.DateTimeField('Issue date', null=True)
120 expiration_date = models.DateTimeField('Expiration date', null=True)
121 moderation_enabled = models.BooleanField('Moderated membership?',
124 approval_date = models.DateTimeField('Activation date', null=True,
127 estimated_participants = models.PositiveIntegerField('Estimated #members',
132 def is_disabled(self):
133 if not self.approval_date:
138 def is_enabled(self):
141 if not self.issue_date:
143 if not self.expiration_date:
146 if self.issue_date > now:
148 if now >= self.expiration_date:
155 self.approval_date = datetime.now()
157 quota_disturbed.send(sender=self, users=self.approved_members)
158 propagate_groupmembers_quota.apply_async(args=[self], eta=self.issue_date)
159 propagate_groupmembers_quota.apply_async(args=[self], eta=self.expiration_date)
164 self.approval_date = None
166 quota_disturbed.send(sender=self, users=self.approved_members)
168 def approve_member(self, person):
169 m, created = self.membership_set.get_or_create(person=person)
170 # update date_joined in any case
171 m.date_joined=datetime.now()
174 def disapprove_member(self, person):
175 self.membership_set.remove(person=person)
179 return [m.person for m in self.membership_set.all()]
182 def approved_members(self):
183 return [m.person for m in self.membership_set.all() if m.is_approved]
188 for q in self.astakosgroupquota_set.all():
189 d[q.resource] += q.limit
194 return self.owner.all()
199 map(self.approve_member, l)
201 class AstakosUser(User):
203 Extends ``django.contrib.auth.models.User`` by defining additional fields.
205 # Use UserManager to get the create_user method, etc.
206 objects = UserManager()
208 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
209 provider = models.CharField('Provider', max_length=255, blank=True)
212 user_level = DEFAULT_USER_LEVEL
213 level = models.IntegerField('Inviter level', default=user_level)
214 invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
216 auth_token = models.CharField('Authentication Token', max_length=32,
217 null=True, blank=True)
218 auth_token_created = models.DateTimeField('Token creation date', null=True)
219 auth_token_expires = models.DateTimeField('Token expiration date', null=True)
221 updated = models.DateTimeField('Update date')
222 is_verified = models.BooleanField('Is verified?', default=False)
224 # ex. screen_name for twitter, eppn for shibboleth
225 third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
227 email_verified = models.BooleanField('Email verified?', default=False)
229 has_credits = models.BooleanField('Has credits?', default=False)
230 has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
231 date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
233 activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True)
235 policy = models.ManyToManyField(Resource, null=True, through='AstakosUserQuota')
237 astakos_groups = models.ManyToManyField(AstakosGroup, verbose_name=_('agroups'), blank=True,
238 help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
239 through='Membership')
241 __has_signed_terms = False
243 owner = models.ManyToManyField(AstakosGroup, related_name='owner', null=True)
246 unique_together = ("provider", "third_party_identifier")
248 def __init__(self, *args, **kwargs):
249 super(AstakosUser, self).__init__(*args, **kwargs)
250 self.__has_signed_terms = self.has_signed_terms
252 self.is_active = False
256 return '%s %s' %(self.first_name, self.last_name)
259 def realname(self, value):
260 parts = value.split(' ')
262 self.first_name = parts[0]
263 self.last_name = parts[1]
265 self.last_name = parts[0]
268 def invitation(self):
270 return Invitation.objects.get(username=self.email)
271 except Invitation.DoesNotExist:
277 for q in self.astakosuserquota_set.all():
278 d[q.resource.name] += q.limit
279 for m in self.membership_set.all():
280 if not m.is_approved:
285 for r, limit in g.quota.iteritems():
287 # TODO set default for remaining
290 def save(self, update_timestamps=True, **kwargs):
291 if update_timestamps:
293 self.date_joined = datetime.now()
294 self.updated = datetime.now()
296 # update date_signed_terms if necessary
297 if self.__has_signed_terms != self.has_signed_terms:
298 self.date_signed_terms = datetime.now()
302 while not self.username:
303 username = uuid.uuid4().hex[:30]
305 AstakosUser.objects.get(username = username)
306 except AstakosUser.DoesNotExist:
307 self.username = username
308 if not self.provider:
309 self.provider = 'local'
310 self.validate_unique_email_isactive()
311 if self.is_active and self.activation_sent:
312 # reset the activation sent
313 self.activation_sent = None
315 super(AstakosUser, self).save(**kwargs)
317 def renew_token(self):
319 md5.update(self.username)
320 md5.update(self.realname.encode('ascii', 'ignore'))
321 md5.update(asctime())
323 self.auth_token = b64encode(md5.digest())
324 self.auth_token_created = datetime.now()
325 self.auth_token_expires = self.auth_token_created + \
326 timedelta(hours=AUTH_TOKEN_DURATION)
327 msg = 'Token renewed for %s' % self.email
328 logger.log(LOGGING_LEVEL, msg)
330 def __unicode__(self):
331 return '%s (%s)' % (self.realname, self.email)
333 def conflicting_email(self):
334 q = AstakosUser.objects.exclude(username = self.username)
335 q = q.filter(email = self.email)
340 def validate_unique_email_isactive(self):
342 Implements a unique_together constraint for email and is_active fields.
344 q = AstakosUser.objects.exclude(username = self.username)
345 q = q.filter(email = self.email)
346 q = q.filter(is_active = self.is_active)
348 raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
351 def signed_terms(self):
352 term = get_latest_terms()
355 if not self.has_signed_terms:
357 if not self.date_signed_terms:
359 if self.date_signed_terms < term.date:
360 self.has_signed_terms = False
361 self.date_signed_terms = None
366 class Membership(models.Model):
367 person = models.ForeignKey(AstakosUser)
368 group = models.ForeignKey(AstakosGroup)
369 date_requested = models.DateField(default=datetime.now(), blank=True)
370 date_joined = models.DateField(null=True, db_index=True, blank=True)
373 unique_together = ("person", "group")
375 def save(self, *args, **kwargs):
377 if not self.group.moderation_enabled:
378 self.date_joined = datetime.now()
379 super(Membership, self).save(*args, **kwargs)
382 def is_approved(self):
388 self.date_joined = datetime.now()
390 quota_disturbed.send(sender=self, users=(self.person,))
392 def disapprove(self):
394 quota_disturbed.send(sender=self, users=(self.person,))
396 class AstakosGroupQuota(models.Model):
397 limit = models.PositiveIntegerField('Limit')
398 resource = models.ForeignKey(Resource)
399 group = models.ForeignKey(AstakosGroup, blank=True)
402 unique_together = ("resource", "group")
404 class AstakosUserQuota(models.Model):
405 limit = models.PositiveIntegerField('Limit')
406 resource = models.ForeignKey(Resource)
407 user = models.ForeignKey(AstakosUser)
410 unique_together = ("resource", "user")
412 class ApprovalTerms(models.Model):
414 Model for approval terms
417 date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
418 location = models.CharField('Terms location', max_length=255)
420 class Invitation(models.Model):
422 Model for registring invitations
424 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
426 realname = models.CharField('Real name', max_length=255)
427 username = models.CharField('Unique ID', max_length=255, unique=True)
428 code = models.BigIntegerField('Invitation code', db_index=True)
429 is_consumed = models.BooleanField('Consumed?', default=False)
430 created = models.DateTimeField('Creation date', auto_now_add=True)
431 consumed = models.DateTimeField('Consumption date', null=True, blank=True)
433 def __init__(self, *args, **kwargs):
434 super(Invitation, self).__init__(*args, **kwargs)
436 self.code = _generate_invitation_code()
439 self.is_consumed = True
440 self.consumed = datetime.now()
443 def __unicode__(self):
444 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
446 class EmailChangeManager(models.Manager):
447 @transaction.commit_on_success
448 def change_email(self, activation_key):
450 Validate an activation key and change the corresponding
453 If the key is valid and has not expired, return the ``User``
456 If the key is not valid or has expired, return ``None``.
458 If the key is valid but the ``User`` is already active,
461 After successful email change the activation record is deleted.
463 Throws ValueError if there is already
466 email_change = self.model.objects.get(activation_key=activation_key)
467 if email_change.activation_key_expired():
468 email_change.delete()
469 raise EmailChange.DoesNotExist
470 # is there an active user with this address?
472 AstakosUser.objects.get(email=email_change.new_email_address)
473 except AstakosUser.DoesNotExist:
476 raise ValueError(_('The new email address is reserved.'))
478 user = AstakosUser.objects.get(pk=email_change.user_id)
479 user.email = email_change.new_email_address
481 email_change.delete()
483 except EmailChange.DoesNotExist:
484 raise ValueError(_('Invalid activation key'))
486 class EmailChange(models.Model):
487 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.'))
488 user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
489 requested_at = models.DateTimeField(default=datetime.now())
490 activation_key = models.CharField(max_length=40, unique=True, db_index=True)
492 objects = EmailChangeManager()
494 def activation_key_expired(self):
495 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
496 return self.requested_at + expiration_date < datetime.now()
498 class AdditionalMail(models.Model):
500 Model for registring invitations
502 owner = models.ForeignKey(AstakosUser)
503 email = models.EmailField()
505 def _generate_invitation_code():
507 code = randint(1, 2L**63 - 1)
509 Invitation.objects.get(code=code)
510 # An invitation with this code already exists, try again
511 except Invitation.DoesNotExist:
514 def get_latest_terms():
516 term = ApprovalTerms.objects.order_by('-id')[0]
522 def create_astakos_user(u):
524 AstakosUser.objects.get(user_ptr=u.pk)
525 except AstakosUser.DoesNotExist:
526 extended_user = AstakosUser(user_ptr_id=u.pk)
527 extended_user.__dict__.update(u.__dict__)
528 extended_user.renew_token()
530 except BaseException, e:
534 def fix_superusers(sender, **kwargs):
535 # Associate superusers with AstakosUser
536 admins = User.objects.filter(is_superuser=True)
538 create_astakos_user(u)
540 def user_post_save(sender, instance, created, **kwargs):
543 create_astakos_user(instance)
545 def set_default_group(user):
547 default = AstakosGroup.objects.get(name = 'default')
548 Membership(group=default, person=user, date_joined=datetime.now()).save()
549 except AstakosGroup.DoesNotExist, e:
552 def astakosuser_pre_save(sender, instance, **kwargs):
553 instance.aquarium_report = False
556 db_instance = AstakosUser.objects.get(id = instance.id)
557 except AstakosUser.DoesNotExist:
559 instance.aquarium_report = True
562 get = AstakosUser.__getattribute__
563 l = filter(lambda f: get(db_instance, f) != get(instance, f),
566 instance.aquarium_report = True if l else False
568 def astakosuser_post_save(sender, instance, created, **kwargs):
569 if instance.aquarium_report:
570 report_user_event(instance, create=instance.new)
573 set_default_group(instance)
574 # TODO handle socket.error & IOError
575 register_users((instance,))
577 def send_quota_disturbed(sender, instance, **kwargs):
579 extend = users.extend
580 if sender == Membership:
581 if not instance.group.is_enabled:
583 extend([instance.person])
584 elif sender == AstakosUserQuota:
585 extend([instance.user])
586 elif sender == AstakosGroupQuota:
587 if not instance.group.is_enabled:
589 extend(instance.group.astakosuser_set.all())
590 elif sender == AstakosGroup:
591 if not instance.is_enabled:
593 quota_disturbed.send(sender=sender, users=users)
595 def on_quota_disturbed(sender, users, **kwargs):
596 print '>>>', locals()
601 post_syncdb.connect(fix_superusers)
602 post_save.connect(user_post_save, sender=User)
603 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
604 post_save.connect(astakosuser_post_save, sender=AstakosUser)
606 quota_disturbed = Signal(providing_args=["users"])
607 quota_disturbed.connect(on_quota_disturbed)
609 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
610 post_delete.connect(send_quota_disturbed, sender=Membership)
611 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
612 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
613 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
614 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)