Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 111f3da6

History | View | Annotate | Download (14.3 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, LOGGING_LEVEL
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
        msg = 'Token renewed for %s' % self.email
174
        logger._log(LOGGING_LEVEL, msg, [])
175

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

    
211
class ApprovalTerms(models.Model):
212
    """
213
    Model for approval terms
214
    """
215

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

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

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

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

    
257
    if QUEUE_CONNECTION and should_send(user):
258

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

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

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

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

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

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

299
        If the key is not valid or has expired, return ``None``.
300

301
        If the key is valid but the ``User`` is already active,
302
        return ``None``.
303

304
        After successful email change the activation record is deleted.
305

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

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

    
335
    objects = EmailChangeManager()
336

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

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

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

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

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

    
385
post_syncdb.connect(superuser_post_syncdb)
386

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

    
391
post_save.connect(superuser_post_save, sender=User)