import hashlib
import uuid
import logging
-import json
from time import asctime
from datetime import datetime, timedelta
from base64 import b64encode
-from urlparse import urlparse, urlunparse
from random import randint
from collections import defaultdict
-from south.signals import post_migrate
-from django.db import models, IntegrityError
+from django.db import models
from django.contrib.auth.models import User, UserManager, Group
from django.utils.translation import ugettext as _
from django.core.exceptions import ValidationError
-from django.template.loader import render_to_string
-from django.core.mail import send_mail
from django.db import transaction
-from django.db.models.signals import post_save, post_syncdb
-from django.db.models import Q, Count
+from django.db.models.signals import pre_save, post_save, post_syncdb, post_delete
+from django.dispatch import Signal
+from django.db.models import Q
-from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \
- AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME, \
- EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
+from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
+ AUTH_TOKEN_DURATION, BILLING_FIELDS,
+ EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
+from astakos.im.endpoints.quotaholder import (register_users, send_quota,
+ register_resources)
+from astakos.im.endpoints.aquarium.producer import report_user_event
-QUEUE_CLIENT_ID = 3 # Astakos.
+from astakos.im.tasks import propagate_groupmembers_quota
logger = logging.getLogger(__name__)
+
class Service(models.Model):
name = models.CharField('Name', max_length=255, unique=True, db_index=True)
url = models.FilePathField()
auth_token = models.CharField('Authentication Token', max_length=32,
null=True, blank=True)
auth_token_created = models.DateTimeField('Token creation date', null=True)
- auth_token_expires = models.DateTimeField('Token expiration date', null=True)
-
+ auth_token_expires = models.DateTimeField(
+ 'Token expiration date', null=True)
+
def save(self, **kwargs):
if not self.id:
self.renew_token()
self.full_clean()
super(Service, self).save(**kwargs)
-
+
def renew_token(self):
md5 = hashlib.md5()
md5.update(self.name.encode('ascii', 'ignore'))
self.auth_token = b64encode(md5.digest())
self.auth_token_created = datetime.now()
self.auth_token_expires = self.auth_token_created + \
- timedelta(hours=AUTH_TOKEN_DURATION)
-
+ timedelta(hours=AUTH_TOKEN_DURATION)
+
def __str__(self):
return self.name
+
class ResourceMetadata(models.Model):
key = models.CharField('Name', max_length=255, unique=True, db_index=True)
value = models.CharField('Value', max_length=255)
+
class Resource(models.Model):
name = models.CharField('Name', max_length=255, unique=True, db_index=True)
meta = models.ManyToManyField(ResourceMetadata)
service = models.ForeignKey(Service)
-
+
def __str__(self):
return '%s : %s' % (self.service, self.name)
+
class GroupKind(models.Model):
name = models.CharField('Name', max_length=255, unique=True, db_index=True)
-
+
def __str__(self):
return self.name
+
class AstakosGroup(Group):
kind = models.ForeignKey(GroupKind)
+ homepage = models.URLField(
+ 'Homepage Url', max_length=255, null=True, blank=True)
desc = models.TextField('Description', null=True)
- policy = models.ManyToManyField(Resource, null=True, blank=True, through='AstakosGroupQuota')
- creation_date = models.DateTimeField('Creation date', default=datetime.now())
+ policy = models.ManyToManyField(Resource, null=True, blank=True,
+ through='AstakosGroupQuota'
+ )
+ creation_date = models.DateTimeField('Creation date',
+ default=datetime.now()
+ )
issue_date = models.DateTimeField('Issue date', null=True)
expiration_date = models.DateTimeField('Expiration date', null=True)
- moderation_enabled = models.BooleanField('Moderated membership?', default=True)
- approval_date = models.DateTimeField('Activation date', null=True, blank=True)
- estimated_participants = models.PositiveIntegerField('Estimated #participants', null=True)
-
+ moderation_enabled = models.BooleanField('Moderated membership?',
+ default=True
+ )
+ approval_date = models.DateTimeField('Activation date', null=True,
+ blank=True
+ )
+ estimated_participants = models.PositiveIntegerField('Estimated #members',
+ null=True
+ )
+
@property
def is_disabled(self):
if not self.approval_date:
return True
return False
-
+
@property
def is_enabled(self):
if self.is_disabled:
if now >= self.expiration_date:
return False
return True
-
- @property
- def participants(self):
- return len(self.approved_members)
-
+
def enable(self):
+ if self.is_enabled:
+ return
self.approval_date = datetime.now()
self.save()
-
+ quota_disturbed.send(sender=self, users=self.approved_members)
+ propagate_groupmembers_quota.apply_async(
+ args=[self], eta=self.issue_date)
+ propagate_groupmembers_quota.apply_async(
+ args=[self], eta=self.expiration_date)
+
def disable(self):
+ if self.is_disabled:
+ return
self.approval_date = None
self.save()
-
+ quota_disturbed.send(sender=self, users=self.approved_members)
+
def approve_member(self, person):
m, created = self.membership_set.get_or_create(person=person)
# update date_joined in any case
- m.date_joined=datetime.now()
+ m.date_joined = datetime.now()
m.save()
-
+
def disapprove_member(self, person):
self.membership_set.remove(person=person)
-
+
@property
def members(self):
- return map(lambda m:m.person, self.membership_set.all())
-
+ return [m.person for m in self.membership_set.all()]
+
@property
def approved_members(self):
- f = filter(lambda m:m.is_approved, self.membership_set.all())
- return map(lambda m:m.person, f)
-
+ return [m.person for m in self.membership_set.all() if m.is_approved]
+
@property
def quota(self):
d = defaultdict(int)
for q in self.astakosgroupquota_set.all():
- d[q.resource] += q.limit
+ d[q.resource] += q.uplimit
return d
-
- @property
- def has_undefined_policies(self):
- # TODO: can avoid query?
- return Resource.objects.filter(~Q(astakosgroup=self)).exists()
-
+
@property
def owners(self):
return self.owner.all()
-
+
@owners.setter
def owners(self, l):
self.owner = l
map(self.approve_member, l)
+
class AstakosUser(User):
"""
Extends ``django.contrib.auth.models.User`` by defining additional fields.
#for invitations
user_level = DEFAULT_USER_LEVEL
level = models.IntegerField('Inviter level', default=user_level)
- invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
+ invitations = models.IntegerField(
+ 'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
auth_token = models.CharField('Authentication Token', max_length=32,
null=True, blank=True)
auth_token_created = models.DateTimeField('Token creation date', null=True)
- auth_token_expires = models.DateTimeField('Token expiration date', null=True)
+ auth_token_expires = models.DateTimeField(
+ 'Token expiration date', null=True)
updated = models.DateTimeField('Update date')
is_verified = models.BooleanField('Is verified?', default=False)
# ex. screen_name for twitter, eppn for shibboleth
- third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
+ third_party_identifier = models.CharField(
+ 'Third-party identifier', max_length=255, null=True, blank=True)
email_verified = models.BooleanField('Email verified?', default=False)
has_credits = models.BooleanField('Has credits?', default=False)
- has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
- date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
-
- activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True)
-
- policy = models.ManyToManyField(Resource, null=True, through='AstakosUserQuota')
-
- astakos_groups = models.ManyToManyField(AstakosGroup, verbose_name=_('agroups'), blank=True,
+ has_signed_terms = models.BooleanField(
+ 'Agree with the terms?', default=False)
+ date_signed_terms = models.DateTimeField(
+ 'Signed terms date', null=True, blank=True)
+
+ activation_sent = models.DateTimeField(
+ 'Activation sent data', null=True, blank=True)
+
+ policy = models.ManyToManyField(
+ Resource, null=True, through='AstakosUserQuota')
+
+ astakos_groups = models.ManyToManyField(
+ AstakosGroup, verbose_name=_('agroups'), blank=True,
help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
through='Membership')
-
+
__has_signed_terms = False
- __groupnames = []
-
- owner = models.ManyToManyField(AstakosGroup, related_name='owner', null=True)
-
+
+ owner = models.ManyToManyField(
+ AstakosGroup, related_name='owner', null=True)
+
class Meta:
unique_together = ("provider", "third_party_identifier")
-
+
def __init__(self, *args, **kwargs):
super(AstakosUser, self).__init__(*args, **kwargs)
self.__has_signed_terms = self.has_signed_terms
- if self.id:
- self.__groupnames = [g.name for g in self.astakos_groups.all()]
- else:
+ if not self.id:
self.is_active = False
-
+
@property
def realname(self):
- return '%s %s' %(self.first_name, self.last_name)
+ return '%s %s' % (self.first_name, self.last_name)
@realname.setter
def realname(self, value):
return Invitation.objects.get(username=self.email)
except Invitation.DoesNotExist:
return None
-
+
@property
def quota(self):
d = defaultdict(int)
- for q in self.astakosuserquota_set.all():
- d[q.resource.name] += q.limit
- for g in self.astakos_groups.all():
+ for q in self.astakosuserquota_set.all():
+ d[q.resource.name] += q.uplimit
+ for m in self.membership_set.all():
+ if not m.is_approved:
+ continue
+ g = m.group
if not g.is_enabled:
continue
- for r, limit in g.quota.iteritems():
- d[r] += limit
+ for r, uplimit in g.quota.iteritems():
+ d[r] += uplimit
# TODO set default for remaining
return d
-
+
def save(self, update_timestamps=True, **kwargs):
if update_timestamps:
if not self.id:
self.date_joined = datetime.now()
self.updated = datetime.now()
-
+
# update date_signed_terms if necessary
if self.__has_signed_terms != self.has_signed_terms:
self.date_signed_terms = datetime.now()
-
+
if not self.id:
# set username
while not self.username:
- username = uuid.uuid4().hex[:30]
+ username = uuid.uuid4().hex[:30]
try:
- AstakosUser.objects.get(username = username)
- except AstakosUser.DoesNotExist, e:
+ AstakosUser.objects.get(username=username)
+ except AstakosUser.DoesNotExist:
self.username = username
if not self.provider:
self.provider = 'local'
- report_user_event(self)
self.validate_unique_email_isactive()
if self.is_active and self.activation_sent:
# reset the activation sent
self.activation_sent = None
-
+
super(AstakosUser, self).save(**kwargs)
-
- # set group if does not exist
- groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
- if groupname not in self.__groupnames:
- try:
- group = AstakosGroup.objects.get(name = groupname)
- Membership(group=group, person=self, date_joined=datetime.now()).save()
- except AstakosGroup.DoesNotExist, e:
- logger.exception(e)
-
+
def renew_token(self):
md5 = hashlib.md5()
md5.update(self.username)
self.auth_token = b64encode(md5.digest())
self.auth_token_created = datetime.now()
self.auth_token_expires = self.auth_token_created + \
- timedelta(hours=AUTH_TOKEN_DURATION)
+ timedelta(hours=AUTH_TOKEN_DURATION)
msg = 'Token renewed for %s' % self.email
- logger._log(LOGGING_LEVEL, msg, [])
+ logger.log(LOGGING_LEVEL, msg)
def __unicode__(self):
- return self.username
-
+ return '%s (%s)' % (self.realname, self.email)
+
def conflicting_email(self):
- q = AstakosUser.objects.exclude(username = self.username)
- q = q.filter(email = self.email)
+ q = AstakosUser.objects.exclude(username=self.username)
+ q = q.filter(email=self.email)
if q.count() != 0:
return True
return False
-
+
def validate_unique_email_isactive(self):
"""
Implements a unique_together constraint for email and is_active fields.
"""
- q = AstakosUser.objects.exclude(username = self.username)
- q = q.filter(email = self.email)
- q = q.filter(is_active = self.is_active)
+ q = AstakosUser.objects.exclude(username=self.username)
+ q = q.filter(email=self.email)
+ q = q.filter(is_active=self.is_active)
if q.count() != 0:
- raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
-
+ raise ValidationError({'__all__': [_('Another account with the same email & is_active combination found.')]})
+
+ @property
def signed_terms(self):
term = get_latest_terms()
if not term:
return False
return True
+
class Membership(models.Model):
person = models.ForeignKey(AstakosUser)
group = models.ForeignKey(AstakosGroup)
date_requested = models.DateField(default=datetime.now(), blank=True)
date_joined = models.DateField(null=True, db_index=True, blank=True)
-
+
class Meta:
unique_together = ("person", "group")
-
+
def save(self, *args, **kwargs):
if not self.id:
if not self.group.moderation_enabled:
self.date_joined = datetime.now()
super(Membership, self).save(*args, **kwargs)
-
+
@property
def is_approved(self):
if self.date_joined:
return True
return False
-
+
def approve(self):
self.date_joined = datetime.now()
self.save()
-
+ quota_disturbed.send(sender=self, users=(self.person,))
+
def disapprove(self):
self.delete()
+ quota_disturbed.send(sender=self, users=(self.person,))
+
class AstakosGroupQuota(models.Model):
- limit = models.PositiveIntegerField('Limit')
+ limit = models.PositiveIntegerField('Limit') # obsolete field
+ uplimit = models.BigIntegerField('Up limit', null=True)
resource = models.ForeignKey(Resource)
group = models.ForeignKey(AstakosGroup, blank=True)
-
+
class Meta:
unique_together = ("resource", "group")
+
class AstakosUserQuota(models.Model):
- limit = models.PositiveIntegerField('Limit')
+ limit = models.PositiveIntegerField('Limit') # obsolete field
+ uplimit = models.BigIntegerField('Up limit', null=True)
resource = models.ForeignKey(Resource)
user = models.ForeignKey(AstakosUser)
-
+
class Meta:
unique_together = ("resource", "user")
+
class ApprovalTerms(models.Model):
"""
Model for approval terms
"""
- date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
+ date = models.DateTimeField(
+ 'Issue date', db_index=True, default=datetime.now())
location = models.CharField('Terms location', max_length=255)
+
class Invitation(models.Model):
"""
Model for registring invitations
is_consumed = models.BooleanField('Consumed?', default=False)
created = models.DateTimeField('Creation date', auto_now_add=True)
consumed = models.DateTimeField('Consumption date', null=True, blank=True)
-
+
def __init__(self, *args, **kwargs):
super(Invitation, self).__init__(*args, **kwargs)
if not self.id:
self.code = _generate_invitation_code()
-
+
def consume(self):
self.is_consumed = True
self.consumed = datetime.now()
def __unicode__(self):
return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
-def report_user_event(user):
- def should_send(user):
- # report event incase of new user instance
- # or if specific fields are modified
- if not user.id:
- return True
- try:
- db_instance = AstakosUser.objects.get(id = user.id)
- except AstakosUser.DoesNotExist:
- return True
- for f in BILLING_FIELDS:
- if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
- return True
- return False
-
- if QUEUE_CONNECTION and should_send(user):
-
- from astakos.im.queue.userevent import UserEvent
- from synnefo.lib.queue import exchange_connect, exchange_send, \
- exchange_close
-
- eventType = 'create' if not user.id else 'modify'
- body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
- conn = exchange_connect(QUEUE_CONNECTION)
- parts = urlparse(QUEUE_CONNECTION)
- exchange = parts.path[1:]
- routing_key = '%s.user' % exchange
- exchange_send(conn, routing_key, body)
- exchange_close(conn)
-
-def _generate_invitation_code():
- while True:
- code = randint(1, 2L**63 - 1)
- try:
- Invitation.objects.get(code=code)
- # An invitation with this code already exists, try again
- except Invitation.DoesNotExist:
- return code
-
-def get_latest_terms():
- try:
- term = ApprovalTerms.objects.order_by('-id')[0]
- return term
- except IndexError:
- pass
- return None
class EmailChangeManager(models.Manager):
@transaction.commit_on_success
Throws ValueError if there is already
"""
try:
- email_change = self.model.objects.get(activation_key=activation_key)
+ email_change = self.model.objects.get(
+ activation_key=activation_key)
if email_change.activation_key_expired():
email_change.delete()
raise EmailChange.DoesNotExist
except EmailChange.DoesNotExist:
raise ValueError(_('Invalid activation key'))
+
class EmailChange(models.Model):
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.'))
- user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
+ user = models.ForeignKey(
+ AstakosUser, unique=True, related_name='emailchange_user')
requested_at = models.DateTimeField(default=datetime.now())
- activation_key = models.CharField(max_length=40, unique=True, db_index=True)
+ activation_key = models.CharField(
+ max_length=40, unique=True, db_index=True)
objects = EmailChangeManager()
expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
return self.requested_at + expiration_date < datetime.now()
+
class AdditionalMail(models.Model):
"""
Model for registring invitations
owner = models.ForeignKey(AstakosUser)
email = models.EmailField()
+
+def _generate_invitation_code():
+ while True:
+ code = randint(1, 2L ** 63 - 1)
+ try:
+ Invitation.objects.get(code=code)
+ # An invitation with this code already exists, try again
+ except Invitation.DoesNotExist:
+ return code
+
+
+def get_latest_terms():
+ try:
+ term = ApprovalTerms.objects.order_by('-id')[0]
+ return term
+ except IndexError:
+ pass
+ return None
+
+
def create_astakos_user(u):
try:
AstakosUser.objects.get(user_ptr=u.pk)
extended_user.__dict__.update(u.__dict__)
extended_user.renew_token()
extended_user.save()
- except:
+ except BaseException, e:
+ logger.exception(e)
pass
-def superuser_post_syncdb(sender, **kwargs):
- # if there was created a superuser
- # associate it with an AstakosUser
+
+def fix_superusers(sender, **kwargs):
+ # Associate superusers with AstakosUser
admins = User.objects.filter(is_superuser=True)
for u in admins:
create_astakos_user(u)
-post_syncdb.connect(superuser_post_syncdb)
-def superuser_post_save(sender, instance, **kwargs):
- if instance.is_superuser:
- create_astakos_user(instance)
+def user_post_save(sender, instance, created, **kwargs):
+ if not created:
+ return
+ create_astakos_user(instance)
+
+
+def set_default_group(user):
+ try:
+ default = AstakosGroup.objects.get(name='default')
+ Membership(
+ group=default, person=user, date_joined=datetime.now()).save()
+ except AstakosGroup.DoesNotExist, e:
+ logger.exception(e)
-post_save.connect(superuser_post_save, sender=User)
-def get_resources():
- # use cache
- return Resource.objects.select_related().all()
\ No newline at end of file
+def astakosuser_pre_save(sender, instance, **kwargs):
+ instance.aquarium_report = False
+ instance.new = False
+ try:
+ db_instance = AstakosUser.objects.get(id=instance.id)
+ except AstakosUser.DoesNotExist:
+ # create event
+ instance.aquarium_report = True
+ instance.new = True
+ else:
+ get = AstakosUser.__getattribute__
+ l = filter(lambda f: get(db_instance, f) != get(instance, f),
+ BILLING_FIELDS
+ )
+ instance.aquarium_report = True if l else False
+
+
+def astakosuser_post_save(sender, instance, created, **kwargs):
+ if instance.aquarium_report:
+ report_user_event(instance, create=instance.new)
+ if not created:
+ return
+ set_default_group(instance)
+ # TODO handle socket.error & IOError
+ register_users((instance,))
+
+
+def resource_post_save(sender, instance, created, **kwargs):
+ if not created:
+ return
+ register_resources((instance,))
+
+
+def send_quota_disturbed(sender, instance, **kwargs):
+ users = []
+ extend = users.extend
+ if sender == Membership:
+ if not instance.group.is_enabled:
+ return
+ extend([instance.person])
+ elif sender == AstakosUserQuota:
+ extend([instance.user])
+ elif sender == AstakosGroupQuota:
+ if not instance.group.is_enabled:
+ return
+ extend(instance.group.astakosuser_set.all())
+ elif sender == AstakosGroup:
+ if not instance.is_enabled:
+ return
+ quota_disturbed.send(sender=sender, users=users)
+
+
+def on_quota_disturbed(sender, users, **kwargs):
+ print '>>>', locals()
+ if not users:
+ return
+ send_quota(users)
+
+post_syncdb.connect(fix_superusers)
+post_save.connect(user_post_save, sender=User)
+pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
+post_save.connect(astakosuser_post_save, sender=AstakosUser)
+post_save.connect(resource_post_save, sender=Resource)
+
+quota_disturbed = Signal(providing_args=["users"])
+quota_disturbed.connect(on_quota_disturbed)
+
+post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
+post_delete.connect(send_quota_disturbed, sender=Membership)
+post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
+post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
+post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
+post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)