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.
39 from time import asctime
40 from datetime import datetime, timedelta
41 from base64 import b64encode
42 from urlparse import urlparse
43 from random import randint
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
54 from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \
55 AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME, \
56 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
58 QUEUE_CLIENT_ID = 3 # Astakos.
60 logger = logging.getLogger(__name__)
62 class AstakosUser(User):
64 Extends ``django.contrib.auth.models.User`` by defining additional fields.
66 # Use UserManager to get the create_user method, etc.
67 objects = UserManager()
69 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
70 provider = models.CharField('Provider', max_length=255, blank=True)
73 user_level = DEFAULT_USER_LEVEL
74 level = models.IntegerField('Inviter level', default=user_level)
75 invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
77 auth_token = models.CharField('Authentication Token', max_length=32,
78 null=True, blank=True)
79 auth_token_created = models.DateTimeField('Token creation date', null=True)
80 auth_token_expires = models.DateTimeField('Token expiration date', null=True)
82 updated = models.DateTimeField('Update date')
83 is_verified = models.BooleanField('Is verified?', default=False)
85 # ex. screen_name for twitter, eppn for shibboleth
86 third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
88 email_verified = models.BooleanField('Email verified?', default=False)
90 has_credits = models.BooleanField('Has credits?', default=False)
91 has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
92 date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
94 activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True)
96 __has_signed_terms = False
100 unique_together = ("provider", "third_party_identifier")
102 def __init__(self, *args, **kwargs):
103 super(AstakosUser, self).__init__(*args, **kwargs)
104 self.__has_signed_terms = self.has_signed_terms
106 self.__groupnames = [g.name for g in self.groups.all()]
108 self.is_active = False
112 return '%s %s' %(self.first_name, self.last_name)
115 def realname(self, value):
116 parts = value.split(' ')
118 self.first_name = parts[0]
119 self.last_name = parts[1]
121 self.last_name = parts[0]
124 def invitation(self):
126 return Invitation.objects.get(username=self.email)
127 except Invitation.DoesNotExist:
130 def save(self, update_timestamps=True, **kwargs):
131 if update_timestamps:
133 self.date_joined = datetime.now()
134 self.updated = datetime.now()
136 # update date_signed_terms if necessary
137 if self.__has_signed_terms != self.has_signed_terms:
138 self.date_signed_terms = datetime.now()
142 while not self.username:
143 username = uuid.uuid4().hex[:30]
145 AstakosUser.objects.get(username = username)
146 except AstakosUser.DoesNotExist, e:
147 self.username = username
148 if not self.provider:
149 self.provider = 'local'
150 report_user_event(self)
151 self.validate_unique_email_isactive()
152 if self.is_active and self.activation_sent:
153 # reset the activation sent
154 self.activation_sent = None
155 super(AstakosUser, self).save(**kwargs)
157 # set group if does not exist
158 groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
159 if groupname not in self.__groupnames:
161 group = Group.objects.get(name = groupname)
162 self.groups.add(group)
163 except Group.DoesNotExist, e:
166 def renew_token(self):
168 md5.update(self.username)
169 md5.update(self.realname.encode('ascii', 'ignore'))
170 md5.update(asctime())
172 self.auth_token = b64encode(md5.digest())
173 self.auth_token_created = datetime.now()
174 self.auth_token_expires = self.auth_token_created + \
175 timedelta(hours=AUTH_TOKEN_DURATION)
176 msg = 'Token renewed for %s' % self.email
177 logger._log(LOGGING_LEVEL, msg, [])
179 def __unicode__(self):
182 def conflicting_email(self):
183 q = AstakosUser.objects.exclude(username = self.username)
184 q = q.filter(email = self.email)
189 def validate_unique_email_isactive(self):
191 Implements a unique_together constraint for email and is_active fields.
193 q = AstakosUser.objects.exclude(username = self.username)
194 q = q.filter(email = self.email)
195 q = q.filter(is_active = self.is_active)
197 raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
199 def signed_terms(self):
200 term = get_latest_terms()
203 if not self.has_signed_terms:
205 if not self.date_signed_terms:
207 if self.date_signed_terms < term.date:
208 self.has_signed_terms = False
209 self.date_signed_terms = None
214 class ApprovalTerms(models.Model):
216 Model for approval terms
219 date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
220 location = models.CharField('Terms location', max_length=255)
222 class Invitation(models.Model):
224 Model for registring invitations
226 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
228 realname = models.CharField('Real name', max_length=255)
229 username = models.CharField('Unique ID', max_length=255, unique=True)
230 code = models.BigIntegerField('Invitation code', db_index=True)
231 is_consumed = models.BooleanField('Consumed?', default=False)
232 created = models.DateTimeField('Creation date', auto_now_add=True)
233 consumed = models.DateTimeField('Consumption date', null=True, blank=True)
235 def __init__(self, *args, **kwargs):
236 super(Invitation, self).__init__(*args, **kwargs)
238 self.code = _generate_invitation_code()
241 self.is_consumed = True
242 self.consumed = datetime.now()
245 def __unicode__(self):
246 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
248 def report_user_event(user):
249 def should_send(user):
250 # report event incase of new user instance
251 # or if specific fields are modified
254 db_instance = AstakosUser.objects.get(id = user.id)
255 for f in BILLING_FIELDS:
256 if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
260 if QUEUE_CONNECTION and should_send(user):
262 from astakos.im.queue.userevent import UserEvent
263 from synnefo.lib.queue import exchange_connect, exchange_send, \
266 eventType = 'create' if not user.id else 'modify'
267 body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
268 conn = exchange_connect(QUEUE_CONNECTION)
269 parts = urlparse(QUEUE_CONNECTION)
270 exchange = parts.path[1:]
271 routing_key = '%s.user' % exchange
272 exchange_send(conn, routing_key, body)
275 def _generate_invitation_code():
277 code = randint(1, 2L**63 - 1)
279 Invitation.objects.get(code=code)
280 # An invitation with this code already exists, try again
281 except Invitation.DoesNotExist:
284 def get_latest_terms():
286 term = ApprovalTerms.objects.order_by('-id')[0]
292 class EmailChangeManager(models.Manager):
293 @transaction.commit_on_success
294 def change_email(self, activation_key):
296 Validate an activation key and change the corresponding
299 If the key is valid and has not expired, return the ``User``
302 If the key is not valid or has expired, return ``None``.
304 If the key is valid but the ``User`` is already active,
307 After successful email change the activation record is deleted.
309 Throws ValueError if there is already
312 email_change = self.model.objects.get(activation_key=activation_key)
313 if email_change.activation_key_expired():
314 email_change.delete()
315 raise EmailChange.DoesNotExist
316 # is there an active user with this address?
318 AstakosUser.objects.get(email=email_change.new_email_address)
319 except AstakosUser.DoesNotExist:
322 raise ValueError(_('The new email address is reserved.'))
324 user = AstakosUser.objects.get(pk=email_change.user_id)
325 user.email = email_change.new_email_address
327 email_change.delete()
329 except EmailChange.DoesNotExist:
330 raise ValueError(_('Invalid activation key'))
332 class EmailChange(models.Model):
333 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.'))
334 user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
335 requested_at = models.DateTimeField(default=datetime.now())
336 activation_key = models.CharField(max_length=40, unique=True, db_index=True)
338 objects = EmailChangeManager()
340 def activation_key_expired(self):
341 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
342 return self.requested_at + expiration_date < datetime.now()
344 class Service(models.Model):
345 name = models.CharField('Name', max_length=255, unique=True)
346 url = models.FilePathField()
347 icon = models.FilePathField(blank=True)
348 auth_token = models.CharField('Authentication Token', max_length=32,
349 null=True, blank=True)
350 auth_token_created = models.DateTimeField('Token creation date', null=True)
351 auth_token_expires = models.DateTimeField('Token expiration date', null=True)
353 def save(self, **kwargs):
357 super(Service, self).save(**kwargs)
359 def renew_token(self):
361 md5.update(self.name.encode('ascii', 'ignore'))
362 md5.update(self.url.encode('ascii', 'ignore'))
363 md5.update(asctime())
365 self.auth_token = b64encode(md5.digest())
366 self.auth_token_created = datetime.now()
367 self.auth_token_expires = self.auth_token_created + \
368 timedelta(hours=AUTH_TOKEN_DURATION)
370 class AdditionalMail(models.Model):
372 Model for registring invitations
374 owner = models.ForeignKey(AstakosUser)
375 email = models.EmailField()
377 class PendingThirdPartyUser(models.Model):
379 Model for registring successful third party user authentications
381 third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
382 provider = models.CharField('Provider', max_length=255, blank=True)
383 email = models.EmailField(_('e-mail address'), blank=True)
384 first_name = models.CharField(_('first name'), max_length=30, blank=True)
385 last_name = models.CharField(_('last name'), max_length=30, blank=True)
386 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
387 username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
391 return '%s %s' %(self.first_name, self.last_name)
394 def realname(self, value):
395 parts = value.split(' ')
397 self.first_name = parts[0]
398 self.last_name = parts[1]
400 self.last_name = parts[0]
402 def save(self, **kwargs):
405 while not self.username:
406 username = uuid.uuid4().hex[:30]
408 AstakosUser.objects.get(username = username)
409 except AstakosUser.DoesNotExist, e:
410 self.username = username
411 super(PendingThirdPartyUser, self).save(**kwargs)
413 def create_astakos_user(u):
415 AstakosUser.objects.get(user_ptr=u.pk)
416 except AstakosUser.DoesNotExist:
417 extended_user = AstakosUser(user_ptr_id=u.pk)
418 extended_user.__dict__.update(u.__dict__)
419 extended_user.renew_token()
424 def superuser_post_syncdb(sender, **kwargs):
425 # if there was created a superuser
426 # associate it with an AstakosUser
427 admins = User.objects.filter(is_superuser=True)
429 create_astakos_user(u)
431 post_syncdb.connect(superuser_post_syncdb)
433 def superuser_post_save(sender, instance, **kwargs):
434 if instance.is_superuser:
435 create_astakos_user(instance)
437 post_save.connect(superuser_post_save, sender=User)