Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 751d24cf

History | View | Annotate | Download (14.2 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
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.
15
#
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.
28
#
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.
33

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38

    
39
from time import asctime
40
from datetime import datetime, timedelta
41
from base64 import b64encode
42
from urlparse import urlparse, urlunparse
43
from random import randint
44

    
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
53

    
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
57

    
58
QUEUE_CLIENT_ID = 3 # Astakos.
59

    
60
logger = logging.getLogger(__name__)
61

    
62
class AstakosUser(User):
63
    """
64
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
65
    """
66
    # Use UserManager to get the create_user method, etc.
67
    objects = UserManager()
68

    
69
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
70
    provider = models.CharField('Provider', max_length=255, blank=True)
71

    
72
    #for invitations
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))
76

    
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)
81

    
82
    updated = models.DateTimeField('Update date')
83
    is_verified = models.BooleanField('Is verified?', default=False)
84

    
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)
87

    
88
    email_verified = models.BooleanField('Email verified?', default=False)
89

    
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)
93
    
94
    activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True)
95
    
96
    __has_signed_terms = False
97
    __groupnames = []
98
    
99
    def __init__(self, *args, **kwargs):
100
        super(AstakosUser, self).__init__(*args, **kwargs)
101
        self.__has_signed_terms = self.has_signed_terms
102
        if self.id:
103
            self.__groupnames = [g.name for g in self.groups.all()]
104
        else:
105
            self.is_active = False
106
    
107
    @property
108
    def realname(self):
109
        return '%s %s' %(self.first_name, self.last_name)
110

    
111
    @realname.setter
112
    def realname(self, value):
113
        parts = value.split(' ')
114
        if len(parts) == 2:
115
            self.first_name = parts[0]
116
            self.last_name = parts[1]
117
        else:
118
            self.last_name = parts[0]
119

    
120
    @property
121
    def invitation(self):
122
        try:
123
            return Invitation.objects.get(username=self.email)
124
        except Invitation.DoesNotExist:
125
            return None
126

    
127
    def save(self, update_timestamps=True, **kwargs):
128
        if update_timestamps:
129
            if not self.id:
130
                self.date_joined = datetime.now()
131
            self.updated = datetime.now()
132
        
133
        # update date_signed_terms if necessary
134
        if self.__has_signed_terms != self.has_signed_terms:
135
            self.date_signed_terms = datetime.now()
136
        
137
        if not self.id:
138
            # set username
139
            while not self.username:
140
                username =  uuid.uuid4().hex[:30]
141
                try:
142
                    AstakosUser.objects.get(username = username)
143
                except AstakosUser.DoesNotExist, e:
144
                    self.username = username
145
            if not self.provider:
146
                self.provider = 'local'
147
        report_user_event(self)
148
        self.validate_unique_email_isactive()
149
        if self.is_active and self.activation_sent:
150
            # reset the activation sent
151
            self.activation_sent = None
152
        super(AstakosUser, self).save(**kwargs)
153
        
154
        # set group if does not exist
155
        groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
156
        if groupname not in self.__groupnames:
157
            try:
158
                group = Group.objects.get(name = groupname)
159
                self.groups.add(group)
160
            except Group.DoesNotExist, e:
161
                logger.exception(e)
162
    
163
    def renew_token(self):
164
        md5 = hashlib.md5()
165
        md5.update(self.username)
166
        md5.update(self.realname.encode('ascii', 'ignore'))
167
        md5.update(asctime())
168

    
169
        self.auth_token = b64encode(md5.digest())
170
        self.auth_token_created = datetime.now()
171
        self.auth_token_expires = self.auth_token_created + \
172
                                  timedelta(hours=AUTH_TOKEN_DURATION)
173

    
174
    def __unicode__(self):
175
        return self.username
176
    
177
    def conflicting_email(self):
178
        q = AstakosUser.objects.exclude(username = self.username)
179
        q = q.filter(email = self.email)
180
        if q.count() != 0:
181
            return True
182
        return False
183
    
184
    def validate_unique_email_isactive(self):
185
        """
186
        Implements a unique_together constraint for email and is_active fields.
187
        """
188
        q = AstakosUser.objects.exclude(username = self.username)
189
        q = q.filter(email = self.email)
190
        q = q.filter(is_active = self.is_active)
191
        if q.count() != 0:
192
            raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
193
    
194
    def signed_terms(self):
195
        term = get_latest_terms()
196
        if not term:
197
            return True
198
        if not self.has_signed_terms:
199
            return False
200
        if not self.date_signed_terms:
201
            return False
202
        if self.date_signed_terms < term.date:
203
            self.has_signed_terms = False
204
            self.date_signed_terms = None
205
            self.save()
206
            return False
207
        return True
208

    
209
class ApprovalTerms(models.Model):
210
    """
211
    Model for approval terms
212
    """
213

    
214
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
215
    location = models.CharField('Terms location', max_length=255)
216

    
217
class Invitation(models.Model):
218
    """
219
    Model for registring invitations
220
    """
221
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
222
                                null=True)
223
    realname = models.CharField('Real name', max_length=255)
224
    username = models.CharField('Unique ID', max_length=255, unique=True)
225
    code = models.BigIntegerField('Invitation code', db_index=True)
226
    is_consumed = models.BooleanField('Consumed?', default=False)
227
    created = models.DateTimeField('Creation date', auto_now_add=True)
228
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
229
    
230
    def __init__(self, *args, **kwargs):
231
        super(Invitation, self).__init__(*args, **kwargs)
232
        if not self.id:
233
            self.code = _generate_invitation_code()
234
    
235
    def consume(self):
236
        self.is_consumed = True
237
        self.consumed = datetime.now()
238
        self.save()
239

    
240
    def __unicode__(self):
241
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
242

    
243
def report_user_event(user):
244
    def should_send(user):
245
        # report event incase of new user instance
246
        # or if specific fields are modified
247
        if not user.id:
248
            return True
249
        db_instance = AstakosUser.objects.get(id = user.id)
250
        for f in BILLING_FIELDS:
251
            if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
252
                return True
253
        return False
254

    
255
    if QUEUE_CONNECTION and should_send(user):
256

    
257
        from astakos.im.queue.userevent import UserEvent
258
        from synnefo.lib.queue import exchange_connect, exchange_send, \
259
                exchange_close
260

    
261
        eventType = 'create' if not user.id else 'modify'
262
        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
263
        conn = exchange_connect(QUEUE_CONNECTION)
264
        parts = urlparse(QUEUE_CONNECTION)
265
        exchange = parts.path[1:]
266
        routing_key = '%s.user' % exchange
267
        exchange_send(conn, routing_key, body)
268
        exchange_close(conn)
269

    
270
def _generate_invitation_code():
271
    while True:
272
        code = randint(1, 2L**63 - 1)
273
        try:
274
            Invitation.objects.get(code=code)
275
            # An invitation with this code already exists, try again
276
        except Invitation.DoesNotExist:
277
            return code
278

    
279
def get_latest_terms():
280
    try:
281
        term = ApprovalTerms.objects.order_by('-id')[0]
282
        return term
283
    except IndexError:
284
        pass
285
    return None
286

    
287
class EmailChangeManager(models.Manager):
288
    @transaction.commit_on_success
289
    def change_email(self, activation_key):
290
        """
291
        Validate an activation key and change the corresponding
292
        ``User`` if valid.
293

294
        If the key is valid and has not expired, return the ``User``
295
        after activating.
296

297
        If the key is not valid or has expired, return ``None``.
298

299
        If the key is valid but the ``User`` is already active,
300
        return ``None``.
301

302
        After successful email change the activation record is deleted.
303

304
        Throws ValueError if there is already
305
        """
306
        try:
307
            email_change = self.model.objects.get(activation_key=activation_key)
308
            if email_change.activation_key_expired():
309
                email_change.delete()
310
                raise EmailChange.DoesNotExist
311
            # is there an active user with this address?
312
            try:
313
                AstakosUser.objects.get(email=email_change.new_email_address)
314
            except AstakosUser.DoesNotExist:
315
                pass
316
            else:
317
                raise ValueError(_('The new email address is reserved.'))
318
            # update user
319
            user = AstakosUser.objects.get(pk=email_change.user_id)
320
            user.email = email_change.new_email_address
321
            user.save()
322
            email_change.delete()
323
            return user
324
        except EmailChange.DoesNotExist:
325
            raise ValueError(_('Invalid activation key'))
326

    
327
class EmailChange(models.Model):
328
    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.'))
329
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
330
    requested_at = models.DateTimeField(default=datetime.now())
331
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
332

    
333
    objects = EmailChangeManager()
334

    
335
    def activation_key_expired(self):
336
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
337
        return self.requested_at + expiration_date < datetime.now()
338

    
339
class Service(models.Model):
340
    name = models.CharField('Name', max_length=255, unique=True)
341
    url = models.FilePathField()
342
    icon = models.FilePathField(blank=True)
343
    auth_token = models.CharField('Authentication Token', max_length=32,
344
                                  null=True, blank=True)
345
    auth_token_created = models.DateTimeField('Token creation date', null=True)
346
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
347
    
348
    def save(self, **kwargs):
349
        if not self.id:
350
            self.renew_token()
351
        self.full_clean()
352
        super(Service, self).save(**kwargs)
353
    
354
    def renew_token(self):
355
        md5 = hashlib.md5()
356
        md5.update(self.name.encode('ascii', 'ignore'))
357
        md5.update(self.url.encode('ascii', 'ignore'))
358
        md5.update(asctime())
359

    
360
        self.auth_token = b64encode(md5.digest())
361
        self.auth_token_created = datetime.now()
362
        self.auth_token_expires = self.auth_token_created + \
363
                                  timedelta(hours=AUTH_TOKEN_DURATION)
364

    
365
def create_astakos_user(u):
366
    try:
367
        AstakosUser.objects.get(user_ptr=u.pk)
368
    except AstakosUser.DoesNotExist:
369
        extended_user = AstakosUser(user_ptr_id=u.pk)
370
        extended_user.__dict__.update(u.__dict__)
371
        extended_user.renew_token()
372
        extended_user.save()
373
    except:
374
        pass
375

    
376
def superuser_post_syncdb(sender, **kwargs):
377
    # if there was created a superuser
378
    # associate it with an AstakosUser
379
    admins = User.objects.filter(is_superuser=True)
380
    for u in admins:
381
        create_astakos_user(u)
382

    
383
post_syncdb.connect(superuser_post_syncdb)
384

    
385
def superuser_post_save(sender, instance, **kwargs):
386
    if instance.is_superuser:
387
        create_astakos_user(instance)
388

    
389
post_save.connect(superuser_post_save, sender=User)