Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 74b273d8

History | View | Annotate | Download (14.5 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
    class Meta:
100
        unique_together = ("provider", "third_party_identifier")
101
    
102
    def __init__(self, *args, **kwargs):
103
        super(AstakosUser, self).__init__(*args, **kwargs)
104
        self.__has_signed_terms = self.has_signed_terms
105
        if self.id:
106
            self.__groupnames = [g.name for g in self.groups.all()]
107
        else:
108
            self.is_active = False
109
    
110
    @property
111
    def realname(self):
112
        return '%s %s' %(self.first_name, self.last_name)
113

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

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

    
130
    def save(self, update_timestamps=True, **kwargs):
131
        if update_timestamps:
132
            if not self.id:
133
                self.date_joined = datetime.now()
134
            self.updated = datetime.now()
135
        
136
        # update date_signed_terms if necessary
137
        if self.__has_signed_terms != self.has_signed_terms:
138
            self.date_signed_terms = datetime.now()
139
        
140
        if not self.id:
141
            # set username
142
            while not self.username:
143
                username =  uuid.uuid4().hex[:30]
144
                try:
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)
156
        
157
        # set group if does not exist
158
        groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
159
        if groupname not in self.__groupnames:
160
            try:
161
                group = Group.objects.get(name = groupname)
162
                self.groups.add(group)
163
            except Group.DoesNotExist, e:
164
                logger.exception(e)
165
    
166
    def renew_token(self):
167
        md5 = hashlib.md5()
168
        md5.update(self.username)
169
        md5.update(self.realname.encode('ascii', 'ignore'))
170
        md5.update(asctime())
171

    
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, [])
178

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

    
214
class ApprovalTerms(models.Model):
215
    """
216
    Model for approval terms
217
    """
218

    
219
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
220
    location = models.CharField('Terms location', max_length=255)
221

    
222
class Invitation(models.Model):
223
    """
224
    Model for registring invitations
225
    """
226
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
227
                                null=True)
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)
234
    
235
    def __init__(self, *args, **kwargs):
236
        super(Invitation, self).__init__(*args, **kwargs)
237
        if not self.id:
238
            self.code = _generate_invitation_code()
239
    
240
    def consume(self):
241
        self.is_consumed = True
242
        self.consumed = datetime.now()
243
        self.save()
244

    
245
    def __unicode__(self):
246
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
247

    
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
252
        if not user.id:
253
            return True
254
        db_instance = AstakosUser.objects.get(id = user.id)
255
        for f in BILLING_FIELDS:
256
            if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
257
                return True
258
        return False
259

    
260
    if QUEUE_CONNECTION and should_send(user):
261

    
262
        from astakos.im.queue.userevent import UserEvent
263
        from synnefo.lib.queue import exchange_connect, exchange_send, \
264
                exchange_close
265

    
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)
273
        exchange_close(conn)
274

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

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

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

299
        If the key is valid and has not expired, return the ``User``
300
        after activating.
301

302
        If the key is not valid or has expired, return ``None``.
303

304
        If the key is valid but the ``User`` is already active,
305
        return ``None``.
306

307
        After successful email change the activation record is deleted.
308

309
        Throws ValueError if there is already
310
        """
311
        try:
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?
317
            try:
318
                AstakosUser.objects.get(email=email_change.new_email_address)
319
            except AstakosUser.DoesNotExist:
320
                pass
321
            else:
322
                raise ValueError(_('The new email address is reserved.'))
323
            # update user
324
            user = AstakosUser.objects.get(pk=email_change.user_id)
325
            user.email = email_change.new_email_address
326
            user.save()
327
            email_change.delete()
328
            return user
329
        except EmailChange.DoesNotExist:
330
            raise ValueError(_('Invalid activation key'))
331

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

    
338
    objects = EmailChangeManager()
339

    
340
    def activation_key_expired(self):
341
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
342
        return self.requested_at + expiration_date < datetime.now()
343

    
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)
352
    
353
    def save(self, **kwargs):
354
        if not self.id:
355
            self.renew_token()
356
        self.full_clean()
357
        super(Service, self).save(**kwargs)
358
    
359
    def renew_token(self):
360
        md5 = hashlib.md5()
361
        md5.update(self.name.encode('ascii', 'ignore'))
362
        md5.update(self.url.encode('ascii', 'ignore'))
363
        md5.update(asctime())
364

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

    
370
class AdditionalMail(models.Model):
371
    """
372
    Model for registring invitations
373
    """
374
    owner = models.ForeignKey(AstakosUser)
375
    email = models.EmailField(unique=True)
376

    
377
def create_astakos_user(u):
378
    try:
379
        AstakosUser.objects.get(user_ptr=u.pk)
380
    except AstakosUser.DoesNotExist:
381
        extended_user = AstakosUser(user_ptr_id=u.pk)
382
        extended_user.__dict__.update(u.__dict__)
383
        extended_user.renew_token()
384
        extended_user.save()
385
    except:
386
        pass
387

    
388
def superuser_post_syncdb(sender, **kwargs):
389
    # if there was created a superuser
390
    # associate it with an AstakosUser
391
    admins = User.objects.filter(is_superuser=True)
392
    for u in admins:
393
        create_astakos_user(u)
394

    
395
post_syncdb.connect(superuser_post_syncdb)
396

    
397
def superuser_post_save(sender, instance, **kwargs):
398
    if instance.is_superuser:
399
        create_astakos_user(instance)
400

    
401
post_save.connect(superuser_post_save, sender=User)