Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ c630fee6

History | View | Annotate | Download (40.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
43
from urllib import quote
44
from random import randint
45
from collections import defaultdict
46

    
47
from django.db import models, IntegrityError
48
from django.contrib.auth.models import User, UserManager, Group, Permission
49
from django.utils.translation import ugettext as _
50
from django.db import transaction
51
from django.core.exceptions import ValidationError
52
from django.db.models.signals import (
53
    pre_save, post_save, post_syncdb, post_delete
54
)
55
from django.contrib.contenttypes.models import ContentType
56

    
57
from django.dispatch import Signal
58
from django.db.models import Q
59
from django.core.urlresolvers import reverse
60
from django.utils.http import int_to_base36
61
from django.contrib.auth.tokens import default_token_generator
62
from django.conf import settings
63
from django.utils.importlib import import_module
64
from django.core.validators import email_re
65

    
66
from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
67
                                 AUTH_TOKEN_DURATION, BILLING_FIELDS,
68
                                 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
69
from astakos.im.endpoints.qh import (
70
    register_users, send_quota, register_resources
71
)
72
from astakos.im import auth_providers
73
from astakos.im.endpoints.aquarium.producer import report_user_event
74
from astakos.im.functions import send_invitation
75
#from astakos.im.tasks import propagate_groupmembers_quota
76

    
77
import astakos.im.messages as astakos_messages
78

    
79
logger = logging.getLogger(__name__)
80

    
81
DEFAULT_CONTENT_TYPE = None
82
try:
83
    content_type = ContentType.objects.get(app_label='im', model='astakosuser')
84
except:
85
    content_type = DEFAULT_CONTENT_TYPE
86

    
87
RESOURCE_SEPARATOR = '.'
88

    
89
inf = float('inf')
90

    
91
class Service(models.Model):
92
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
93
    url = models.FilePathField()
94
    icon = models.FilePathField(blank=True)
95
    auth_token = models.CharField('Authentication Token', max_length=32,
96
                                  null=True, blank=True)
97
    auth_token_created = models.DateTimeField('Token creation date', null=True)
98
    auth_token_expires = models.DateTimeField(
99
        'Token expiration date', null=True)
100

    
101
    def renew_token(self):
102
        md5 = hashlib.md5()
103
        md5.update(self.name.encode('ascii', 'ignore'))
104
        md5.update(self.url.encode('ascii', 'ignore'))
105
        md5.update(asctime())
106

    
107
        self.auth_token = b64encode(md5.digest())
108
        self.auth_token_created = datetime.now()
109
        self.auth_token_expires = self.auth_token_created + \
110
            timedelta(hours=AUTH_TOKEN_DURATION)
111

    
112
    def __str__(self):
113
        return self.name
114

    
115
    @property
116
    def resources(self):
117
        return self.resource_set.all()
118

    
119
    @resources.setter
120
    def resources(self, resources):
121
        for s in resources:
122
            self.resource_set.create(**s)
123

    
124
    def add_resource(self, service, resource, uplimit, update=True):
125
        """Raises ObjectDoesNotExist, IntegrityError"""
126
        resource = Resource.objects.get(service__name=service, name=resource)
127
        if update:
128
            AstakosUserQuota.objects.update_or_create(user=self,
129
                                                      resource=resource,
130
                                                      defaults={'uplimit': uplimit})
131
        else:
132
            q = self.astakosuserquota_set
133
            q.create(resource=resource, uplimit=uplimit)
134

    
135

    
136
class ResourceMetadata(models.Model):
137
    key = models.CharField('Name', max_length=255, unique=True, db_index=True)
138
    value = models.CharField('Value', max_length=255)
139

    
140

    
141
class Resource(models.Model):
142
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
143
    meta = models.ManyToManyField(ResourceMetadata)
144
    service = models.ForeignKey(Service)
145
    desc = models.TextField('Description', null=True)
146
    unit = models.CharField('Name', null=True, max_length=255)
147
    group = models.CharField('Group', null=True, max_length=255)
148

    
149
    def __str__(self):
150
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
151

    
152

    
153
class GroupKind(models.Model):
154
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
155

    
156
    def __str__(self):
157
        return self.name
158

    
159

    
160
class AstakosGroup(Group):
161
    kind = models.ForeignKey(GroupKind)
162
    homepage = models.URLField(
163
        'Homepage Url', max_length=255, null=True, blank=True)
164
    desc = models.TextField('Description', null=True)
165
    policy = models.ManyToManyField(
166
        Resource,
167
        null=True,
168
        blank=True,
169
        through='AstakosGroupQuota'
170
    )
171
    creation_date = models.DateTimeField(
172
        'Creation date',
173
        default=datetime.now()
174
    )
175
    issue_date = models.DateTimeField('Start date', null=True)
176
    expiration_date = models.DateTimeField(
177
        'End date',
178
         null=True
179
    )
180
    moderation_enabled = models.BooleanField(
181
        'Moderated membership?',
182
        default=True
183
    )
184
    approval_date = models.DateTimeField(
185
        'Activation date',
186
        null=True,
187
        blank=True
188
    )
189
    estimated_participants = models.PositiveIntegerField(
190
        'Estimated #members',
191
        null=True,
192
        blank=True,
193
    )
194
    max_participants = models.PositiveIntegerField(
195
        'Maximum numder of participants',
196
        null=True,
197
        blank=True
198
    )
199

    
200
    @property
201
    def is_disabled(self):
202
        if not self.approval_date:
203
            return True
204
        return False
205

    
206
    @property
207
    def is_enabled(self):
208
        if self.is_disabled:
209
            return False
210
        if not self.issue_date:
211
            return False
212
        if not self.expiration_date:
213
            return True
214
        now = datetime.now()
215
        if self.issue_date > now:
216
            return False
217
        if now >= self.expiration_date:
218
            return False
219
        return True
220

    
221
    def enable(self):
222
        if self.is_enabled:
223
            return
224
        self.approval_date = datetime.now()
225
        self.save()
226
        quota_disturbed.send(sender=self, users=self.approved_members)
227
        #propagate_groupmembers_quota.apply_async(
228
        #    args=[self], eta=self.issue_date)
229
        #propagate_groupmembers_quota.apply_async(
230
        #    args=[self], eta=self.expiration_date)
231

    
232
    def disable(self):
233
        if self.is_disabled:
234
            return
235
        self.approval_date = None
236
        self.save()
237
        quota_disturbed.send(sender=self, users=self.approved_members)
238

    
239
    @transaction.commit_manually
240
    def approve_member(self, person):
241
        m, created = self.membership_set.get_or_create(person=person)
242
        try:
243
            m.approve()
244
        except:
245
            transaction.rollback()
246
            raise
247
        else:
248
            transaction.commit()
249

    
250
#     def disapprove_member(self, person):
251
#         self.membership_set.remove(person=person)
252

    
253
    @property
254
    def members(self):
255
        q = self.membership_set.select_related().all()
256
        return [m.person for m in q]
257

    
258
    @property
259
    def approved_members(self):
260
        q = self.membership_set.select_related().all()
261
        return [m.person for m in q if m.is_approved]
262

    
263
    @property
264
    def quota(self):
265
        d = defaultdict(int)
266
        for q in self.astakosgroupquota_set.select_related().all():
267
            d[q.resource] += q.uplimit or inf
268
        return d
269

    
270
    def add_policy(self, service, resource, uplimit, update=True):
271
        """Raises ObjectDoesNotExist, IntegrityError"""
272
        resource = Resource.objects.get(service__name=service, name=resource)
273
        if update:
274
            AstakosGroupQuota.objects.update_or_create(
275
                group=self,
276
                resource=resource,
277
                defaults={'uplimit': uplimit}
278
            )
279
        else:
280
            q = self.astakosgroupquota_set
281
            q.create(resource=resource, uplimit=uplimit)
282

    
283
    @property
284
    def policies(self):
285
        return self.astakosgroupquota_set.select_related().all()
286

    
287
    @policies.setter
288
    def policies(self, policies):
289
        for p in policies:
290
            service = p.get('service', None)
291
            resource = p.get('resource', None)
292
            uplimit = p.get('uplimit', 0)
293
            update = p.get('update', True)
294
            self.add_policy(service, resource, uplimit, update)
295

    
296
    @property
297
    def owners(self):
298
        return self.owner.all()
299

    
300
    @property
301
    def owner_details(self):
302
        return self.owner.select_related().all()
303

    
304
    @owners.setter
305
    def owners(self, l):
306
        self.owner = l
307
        map(self.approve_member, l)
308

    
309

    
310

    
311
class AstakosUserManager(UserManager):
312

    
313
    def get_auth_provider_user(self, provider, **kwargs):
314
        """
315
        Retrieve AstakosUser instance associated with the specified third party
316
        id.
317
        """
318
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
319
                          kwargs.iteritems()))
320
        return self.get(auth_providers__module=provider, **kwargs)
321

    
322
    def get_by_email(self, email):
323
        return self.get(email=email)
324

    
325
class AstakosUser(User):
326
    """
327
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
328
    """
329
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
330
                                   null=True)
331

    
332
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
333
    #                    AstakosUserProvider model.
334
    provider = models.CharField('Provider', max_length=255, blank=True,
335
                                null=True)
336
    # ex. screen_name for twitter, eppn for shibboleth
337
    third_party_identifier = models.CharField('Third-party identifier',
338
                                              max_length=255, null=True,
339
                                              blank=True)
340

    
341

    
342
    #for invitations
343
    user_level = DEFAULT_USER_LEVEL
344
    level = models.IntegerField('Inviter level', default=user_level)
345
    invitations = models.IntegerField(
346
        'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
347

    
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(
352
        'Token expiration date', null=True)
353

    
354
    updated = models.DateTimeField('Update date')
355
    is_verified = models.BooleanField('Is verified?', default=False)
356

    
357
    email_verified = models.BooleanField('Email verified?', default=False)
358

    
359
    has_credits = models.BooleanField('Has credits?', default=False)
360
    has_signed_terms = models.BooleanField(
361
        'I agree with the terms', default=False)
362
    date_signed_terms = models.DateTimeField(
363
        'Signed terms date', null=True, blank=True)
364

    
365
    activation_sent = models.DateTimeField(
366
        'Activation sent data', null=True, blank=True)
367

    
368
    policy = models.ManyToManyField(
369
        Resource, null=True, through='AstakosUserQuota')
370

    
371
    astakos_groups = models.ManyToManyField(
372
        AstakosGroup, verbose_name=_('agroups'), blank=True,
373
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
374
        through='Membership')
375

    
376
    __has_signed_terms = False
377
    disturbed_quota = models.BooleanField('Needs quotaholder syncing',
378
                                           default=False, db_index=True)
379

    
380
    objects = AstakosUserManager()
381

    
382
    owner = models.ManyToManyField(
383
        AstakosGroup, related_name='owner', null=True)
384

    
385
    def __init__(self, *args, **kwargs):
386
        super(AstakosUser, self).__init__(*args, **kwargs)
387
        self.__has_signed_terms = self.has_signed_terms
388
        if not self.id:
389
            self.is_active = False
390

    
391
    @property
392
    def realname(self):
393
        return '%s %s' % (self.first_name, self.last_name)
394

    
395
    @realname.setter
396
    def realname(self, value):
397
        parts = value.split(' ')
398
        if len(parts) == 2:
399
            self.first_name = parts[0]
400
            self.last_name = parts[1]
401
        else:
402
            self.last_name = parts[0]
403

    
404
    def add_permission(self, pname):
405
        if self.has_perm(pname):
406
            return
407
        p, created = Permission.objects.get_or_create(codename=pname,
408
                                                      name=pname.capitalize(),
409
                                                      content_type=content_type)
410
        self.user_permissions.add(p)
411

    
412
    def remove_permission(self, pname):
413
        if self.has_perm(pname):
414
            return
415
        p = Permission.objects.get(codename=pname,
416
                                   content_type=content_type)
417
        self.user_permissions.remove(p)
418

    
419
    @property
420
    def invitation(self):
421
        try:
422
            return Invitation.objects.get(username=self.email)
423
        except Invitation.DoesNotExist:
424
            return None
425

    
426
    def invite(self, email, realname):
427
        inv = Invitation(inviter=self, username=email, realname=realname)
428
        inv.save()
429
        send_invitation(inv)
430
        self.invitations = max(0, self.invitations - 1)
431
        self.save()
432

    
433
    @property
434
    def quota(self):
435
        """Returns a dict with the sum of quota limits per resource"""
436
        d = defaultdict(int)
437
        for q in self.policies:
438
            d[q.resource] += q.uplimit or inf
439
        for m in self.extended_groups:
440
            if not m.is_approved:
441
                continue
442
            g = m.group
443
            if not g.is_enabled:
444
                continue
445
            for r, uplimit in g.quota.iteritems():
446
                d[r] += uplimit or inf
447
        # TODO set default for remaining
448
        return d
449

    
450
    @property
451
    def policies(self):
452
        return self.astakosuserquota_set.select_related().all()
453

    
454
    @policies.setter
455
    def policies(self, policies):
456
        for p in policies:
457
            service = policies.get('service', None)
458
            resource = policies.get('resource', None)
459
            uplimit = policies.get('uplimit', 0)
460
            update = policies.get('update', True)
461
            self.add_policy(service, resource, uplimit, update)
462

    
463
    def add_policy(self, service, resource, uplimit, update=True):
464
        """Raises ObjectDoesNotExist, IntegrityError"""
465
        resource = Resource.objects.get(service__name=service, name=resource)
466
        if update:
467
            AstakosUserQuota.objects.update_or_create(user=self,
468
                                                      resource=resource,
469
                                                      defaults={'uplimit': uplimit})
470
        else:
471
            q = self.astakosuserquota_set
472
            q.create(resource=resource, uplimit=uplimit)
473

    
474
    def remove_policy(self, service, resource):
475
        """Raises ObjectDoesNotExist, IntegrityError"""
476
        resource = Resource.objects.get(service__name=service, name=resource)
477
        q = self.policies.get(resource=resource).delete()
478

    
479
    @property
480
    def extended_groups(self):
481
        return self.membership_set.select_related().all()
482

    
483
    @extended_groups.setter
484
    def extended_groups(self, groups):
485
        #TODO exceptions
486
        for name in (groups or ()):
487
            group = AstakosGroup.objects.get(name=name)
488
            self.membership_set.create(group=group)
489

    
490
    def save(self, update_timestamps=True, **kwargs):
491
        if update_timestamps:
492
            if not self.id:
493
                self.date_joined = datetime.now()
494
            self.updated = datetime.now()
495

    
496
        # update date_signed_terms if necessary
497
        if self.__has_signed_terms != self.has_signed_terms:
498
            self.date_signed_terms = datetime.now()
499

    
500
        if not self.id:
501
            # set username
502
            self.username = self.email
503

    
504
        self.validate_unique_email_isactive()
505
        if self.is_active and self.activation_sent:
506
            # reset the activation sent
507
            self.activation_sent = None
508

    
509
        super(AstakosUser, self).save(**kwargs)
510

    
511
    def renew_token(self, flush_sessions=False, current_key=None):
512
        md5 = hashlib.md5()
513
        md5.update(settings.SECRET_KEY)
514
        md5.update(self.username)
515
        md5.update(self.realname.encode('ascii', 'ignore'))
516
        md5.update(asctime())
517

    
518
        self.auth_token = b64encode(md5.digest())
519
        self.auth_token_created = datetime.now()
520
        self.auth_token_expires = self.auth_token_created + \
521
                                  timedelta(hours=AUTH_TOKEN_DURATION)
522
        if flush_sessions:
523
            self.flush_sessions(current_key)
524
        msg = 'Token renewed for %s' % self.email
525
        logger.log(LOGGING_LEVEL, msg)
526

    
527
    def flush_sessions(self, current_key=None):
528
        q = self.sessions
529
        if current_key:
530
            q = q.exclude(session_key=current_key)
531

    
532
        keys = q.values_list('session_key', flat=True)
533
        if keys:
534
            msg = 'Flushing sessions: %s' % ','.join(keys)
535
            logger.log(LOGGING_LEVEL, msg, [])
536
        engine = import_module(settings.SESSION_ENGINE)
537
        for k in keys:
538
            s = engine.SessionStore(k)
539
            s.flush()
540

    
541
    def __unicode__(self):
542
        return '%s (%s)' % (self.realname, self.email)
543

    
544
    def conflicting_email(self):
545
        q = AstakosUser.objects.exclude(username=self.username)
546
        q = q.filter(email__iexact=self.email)
547
        if q.count() != 0:
548
            return True
549
        return False
550

    
551
    def validate_unique_email_isactive(self):
552
        """
553
        Implements a unique_together constraint for email and is_active fields.
554
        """
555
        q = AstakosUser.objects.all()
556
        q = q.filter(email = self.email)
557
        q = q.filter(is_active = self.is_active)
558
        if self.id:
559
            q = q.filter(~Q(id = self.id))
560
        if q.count() != 0:
561
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
562

    
563
    @property
564
    def signed_terms(self):
565
        term = get_latest_terms()
566
        if not term:
567
            return True
568
        if not self.has_signed_terms:
569
            return False
570
        if not self.date_signed_terms:
571
            return False
572
        if self.date_signed_terms < term.date:
573
            self.has_signed_terms = False
574
            self.date_signed_terms = None
575
            self.save()
576
            return False
577
        return True
578

    
579
    def set_invitations_level(self):
580
        """
581
        Update user invitation level
582
        """
583
        level = self.invitation.inviter.level + 1
584
        self.level = level
585
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
586

    
587
    def can_login_with_auth_provider(self, provider):
588
        if not self.has_auth_provider(provider):
589
            return False
590
        else:
591
            return auth_providers.get_provider(provider).is_available_for_login()
592

    
593
    def can_add_auth_provider(self, provider, **kwargs):
594
        provider_settings = auth_providers.get_provider(provider)
595
        if not provider_settings.is_available_for_login():
596
            return False
597

    
598
        if self.has_auth_provider(provider) and \
599
           provider_settings.one_per_user:
600
            return False
601

    
602
        if 'provider_info' in kwargs:
603
            kwargs.pop('provider_info')
604

    
605
        if 'identifier' in kwargs:
606
            try:
607
                # provider with specified params already exist
608
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
609
                                                                   **kwargs)
610
            except AstakosUser.DoesNotExist:
611
                return True
612
            else:
613
                return False
614

    
615
        return True
616

    
617
    def can_remove_auth_provider(self, provider):
618
        if len(self.get_active_auth_providers()) <= 1:
619
            return False
620
        return True
621

    
622
    def can_change_password(self):
623
        return self.has_auth_provider('local', auth_backend='astakos')
624

    
625
    def has_auth_provider(self, provider, **kwargs):
626
        return bool(self.auth_providers.filter(module=provider,
627
                                               **kwargs).count())
628

    
629
    def add_auth_provider(self, provider, **kwargs):
630
        info_data = ''
631
        if 'provider_info' in kwargs:
632
            info_data = kwargs.pop('provider_info')
633
            if isinstance(info_data, dict):
634
                info_data = json.dumps(info_data)
635

    
636
        if self.can_add_auth_provider(provider, **kwargs):
637
            self.auth_providers.create(module=provider, active=True,
638
                                       info_data=info_data,
639
                                       **kwargs)
640
        else:
641
            raise Exception('Cannot add provider')
642

    
643
    def add_pending_auth_provider(self, pending):
644
        """
645
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
646
        the current user.
647
        """
648
        if not isinstance(pending, PendingThirdPartyUser):
649
            pending = PendingThirdPartyUser.objects.get(token=pending)
650

    
651
        provider = self.add_auth_provider(pending.provider,
652
                               identifier=pending.third_party_identifier,
653
                                affiliation=pending.affiliation,
654
                                          provider_info=pending.info)
655

    
656
        if email_re.match(pending.email or '') and pending.email != self.email:
657
            self.additionalmail_set.get_or_create(email=pending.email)
658

    
659
        pending.delete()
660
        return provider
661

    
662
    def remove_auth_provider(self, provider, **kwargs):
663
        self.auth_providers.get(module=provider, **kwargs).delete()
664

    
665
    # user urls
666
    def get_resend_activation_url(self):
667
        return reverse('send_activation', kwargs={'user_id': self.pk})
668

    
669
    def get_provider_remove_url(self, module, **kwargs):
670
        return reverse('remove_auth_provider', kwargs={
671
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
672

    
673
    def get_activation_url(self, nxt=False):
674
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
675
                                 quote(self.auth_token))
676
        if nxt:
677
            url += "&next=%s" % quote(nxt)
678
        return url
679

    
680
    def get_password_reset_url(self, token_generator=default_token_generator):
681
        return reverse('django.contrib.auth.views.password_reset_confirm',
682
                          kwargs={'uidb36':int_to_base36(self.id),
683
                                  'token':token_generator.make_token(self)})
684

    
685
    def get_auth_providers(self):
686
        return self.auth_providers.all()
687

    
688
    def get_available_auth_providers(self):
689
        """
690
        Returns a list of providers available for user to connect to.
691
        """
692
        providers = []
693
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
694
            if self.can_add_auth_provider(module):
695
                providers.append(provider_settings(self))
696

    
697
        return providers
698

    
699
    def get_active_auth_providers(self):
700
        providers = []
701
        for provider in self.auth_providers.active():
702
            if auth_providers.get_provider(provider.module).is_available_for_login():
703
                providers.append(provider)
704
        return providers
705

    
706
    @property
707
    def auth_providers_display(self):
708
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
709

    
710

    
711
class AstakosUserAuthProviderManager(models.Manager):
712

    
713
    def active(self):
714
        return self.filter(active=True)
715

    
716

    
717
class AstakosUserAuthProvider(models.Model):
718
    """
719
    Available user authentication methods.
720
    """
721
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
722
                                   null=True, default=None)
723
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
724
    module = models.CharField('Provider', max_length=255, blank=False,
725
                                default='local')
726
    identifier = models.CharField('Third-party identifier',
727
                                              max_length=255, null=True,
728
                                              blank=True)
729
    active = models.BooleanField(default=True)
730
    auth_backend = models.CharField('Backend', max_length=255, blank=False,
731
                                   default='astakos')
732
    info_data = models.TextField(default="", null=True, blank=True)
733
    created = models.DateTimeField('Creation date', auto_now_add=True)
734

    
735
    objects = AstakosUserAuthProviderManager()
736

    
737
    class Meta:
738
        unique_together = (('identifier', 'module', 'user'), )
739
        ordering = ('module', 'created')
740

    
741
    def __init__(self, *args, **kwargs):
742
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
743
        try:
744
            self.info = json.loads(self.info_data)
745
            if not self.info:
746
                self.info = {}
747
        except Exception, e:
748
            self.info = {}
749

    
750
        for key,value in self.info.iteritems():
751
            setattr(self, 'info_%s' % key, value)
752

    
753

    
754
    @property
755
    def settings(self):
756
        return auth_providers.get_provider(self.module)
757

    
758
    @property
759
    def details_display(self):
760
        try:
761
          return self.settings.get_details_tpl_display % self.__dict__
762
        except:
763
          return ''
764

    
765
    @property
766
    def title_display(self):
767
        title_tpl = self.settings.get_title_display
768
        try:
769
            if self.settings.get_user_title_display:
770
                title_tpl = self.settings.get_user_title_display
771
        except Exception, e:
772
            pass
773
        try:
774
          return title_tpl % self.__dict__
775
        except:
776
          return self.settings.get_title_display % self.__dict__
777

    
778
    def can_remove(self):
779
        return self.user.can_remove_auth_provider(self.module)
780

    
781
    def delete(self, *args, **kwargs):
782
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
783
        if self.module == 'local':
784
            self.user.set_unusable_password()
785
            self.user.save()
786
        return ret
787

    
788
    def __repr__(self):
789
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
790

    
791
    def __unicode__(self):
792
        if self.identifier:
793
            return "%s:%s" % (self.module, self.identifier)
794
        if self.auth_backend:
795
            return "%s:%s" % (self.module, self.auth_backend)
796
        return self.module
797

    
798
    def save(self, *args, **kwargs):
799
        self.info_data = json.dumps(self.info)
800
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
801

    
802

    
803
class Membership(models.Model):
804
    person = models.ForeignKey(AstakosUser)
805
    group = models.ForeignKey(AstakosGroup)
806
    date_requested = models.DateField(default=datetime.now(), blank=True)
807
    date_joined = models.DateField(null=True, db_index=True, blank=True)
808

    
809
    class Meta:
810
        unique_together = ("person", "group")
811

    
812
    def save(self, *args, **kwargs):
813
        if not self.id:
814
            if not self.group.moderation_enabled:
815
                self.date_joined = datetime.now()
816
        super(Membership, self).save(*args, **kwargs)
817

    
818
    @property
819
    def is_approved(self):
820
        if self.date_joined:
821
            return True
822
        return False
823

    
824
    def approve(self):
825
        if self.is_approved:
826
            return
827
        if self.group.max_participants:
828
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
829
            'Maximum participant number has been reached.'
830
        self.date_joined = datetime.now()
831
        self.save()
832
        quota_disturbed.send(sender=self, users=(self.person,))
833

    
834
    def disapprove(self):
835
        self.delete()
836
        quota_disturbed.send(sender=self, users=(self.person,))
837

    
838
class AstakosQuotaManager(models.Manager):
839
    def _update_or_create(self, **kwargs):
840
        assert kwargs, \
841
            'update_or_create() must be passed at least one keyword argument'
842
        obj, created = self.get_or_create(**kwargs)
843
        defaults = kwargs.pop('defaults', {})
844
        if created:
845
            return obj, True, False
846
        else:
847
            try:
848
                params = dict(
849
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
850
                params.update(defaults)
851
                for attr, val in params.items():
852
                    if hasattr(obj, attr):
853
                        setattr(obj, attr, val)
854
                sid = transaction.savepoint()
855
                obj.save(force_update=True)
856
                transaction.savepoint_commit(sid)
857
                return obj, False, True
858
            except IntegrityError, e:
859
                transaction.savepoint_rollback(sid)
860
                try:
861
                    return self.get(**kwargs), False, False
862
                except self.model.DoesNotExist:
863
                    raise e
864

    
865
    update_or_create = _update_or_create
866

    
867
class AstakosGroupQuota(models.Model):
868
    objects = AstakosQuotaManager()
869
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
870
    uplimit = models.BigIntegerField('Up limit', null=True)
871
    resource = models.ForeignKey(Resource)
872
    group = models.ForeignKey(AstakosGroup, blank=True)
873

    
874
    class Meta:
875
        unique_together = ("resource", "group")
876

    
877
class AstakosUserQuota(models.Model):
878
    objects = AstakosQuotaManager()
879
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
880
    uplimit = models.BigIntegerField('Up limit', null=True)
881
    resource = models.ForeignKey(Resource)
882
    user = models.ForeignKey(AstakosUser)
883

    
884
    class Meta:
885
        unique_together = ("resource", "user")
886

    
887

    
888
class ApprovalTerms(models.Model):
889
    """
890
    Model for approval terms
891
    """
892

    
893
    date = models.DateTimeField(
894
        'Issue date', db_index=True, default=datetime.now())
895
    location = models.CharField('Terms location', max_length=255)
896

    
897

    
898
class Invitation(models.Model):
899
    """
900
    Model for registring invitations
901
    """
902
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
903
                                null=True)
904
    realname = models.CharField('Real name', max_length=255)
905
    username = models.CharField('Unique ID', max_length=255, unique=True)
906
    code = models.BigIntegerField('Invitation code', db_index=True)
907
    is_consumed = models.BooleanField('Consumed?', default=False)
908
    created = models.DateTimeField('Creation date', auto_now_add=True)
909
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
910

    
911
    def __init__(self, *args, **kwargs):
912
        super(Invitation, self).__init__(*args, **kwargs)
913
        if not self.id:
914
            self.code = _generate_invitation_code()
915

    
916
    def consume(self):
917
        self.is_consumed = True
918
        self.consumed = datetime.now()
919
        self.save()
920

    
921
    def __unicode__(self):
922
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
923

    
924

    
925
class EmailChangeManager(models.Manager):
926
    @transaction.commit_on_success
927
    def change_email(self, activation_key):
928
        """
929
        Validate an activation key and change the corresponding
930
        ``User`` if valid.
931

932
        If the key is valid and has not expired, return the ``User``
933
        after activating.
934

935
        If the key is not valid or has expired, return ``None``.
936

937
        If the key is valid but the ``User`` is already active,
938
        return ``None``.
939

940
        After successful email change the activation record is deleted.
941

942
        Throws ValueError if there is already
943
        """
944
        try:
945
            email_change = self.model.objects.get(
946
                activation_key=activation_key)
947
            if email_change.activation_key_expired():
948
                email_change.delete()
949
                raise EmailChange.DoesNotExist
950
            # is there an active user with this address?
951
            try:
952
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
953
            except AstakosUser.DoesNotExist:
954
                pass
955
            else:
956
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
957
            # update user
958
            user = AstakosUser.objects.get(pk=email_change.user_id)
959
            user.email = email_change.new_email_address
960
            user.save()
961
            email_change.delete()
962
            return user
963
        except EmailChange.DoesNotExist:
964
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
965

    
966

    
967
class EmailChange(models.Model):
968
    new_email_address = models.EmailField(_(u'new e-mail address'),
969
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
970
    user = models.ForeignKey(
971
        AstakosUser, unique=True, related_name='emailchange_user')
972
    requested_at = models.DateTimeField(default=datetime.now())
973
    activation_key = models.CharField(
974
        max_length=40, unique=True, db_index=True)
975

    
976
    objects = EmailChangeManager()
977

    
978
    def activation_key_expired(self):
979
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
980
        return self.requested_at + expiration_date < datetime.now()
981

    
982

    
983
class AdditionalMail(models.Model):
984
    """
985
    Model for registring invitations
986
    """
987
    owner = models.ForeignKey(AstakosUser)
988
    email = models.EmailField()
989

    
990

    
991
def _generate_invitation_code():
992
    while True:
993
        code = randint(1, 2L ** 63 - 1)
994
        try:
995
            Invitation.objects.get(code=code)
996
            # An invitation with this code already exists, try again
997
        except Invitation.DoesNotExist:
998
            return code
999

    
1000

    
1001
def get_latest_terms():
1002
    try:
1003
        term = ApprovalTerms.objects.order_by('-id')[0]
1004
        return term
1005
    except IndexError:
1006
        pass
1007
    return None
1008

    
1009
class PendingThirdPartyUser(models.Model):
1010
    """
1011
    Model for registring successful third party user authentications
1012
    """
1013
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
1014
    provider = models.CharField('Provider', max_length=255, blank=True)
1015
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1016
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
1017
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
1018
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1019
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1020
    token = models.CharField('Token', max_length=255, null=True, blank=True)
1021
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1022
    info = models.TextField(default="", null=True, blank=True)
1023

    
1024
    class Meta:
1025
        unique_together = ("provider", "third_party_identifier")
1026

    
1027
    def get_user_instance(self):
1028
        d = self.__dict__
1029
        d.pop('_state', None)
1030
        d.pop('id', None)
1031
        d.pop('token', None)
1032
        d.pop('created', None)
1033
        d.pop('info', None)
1034
        user = AstakosUser(**d)
1035

    
1036
        return user
1037

    
1038
    @property
1039
    def realname(self):
1040
        return '%s %s' %(self.first_name, self.last_name)
1041

    
1042
    @realname.setter
1043
    def realname(self, value):
1044
        parts = value.split(' ')
1045
        if len(parts) == 2:
1046
            self.first_name = parts[0]
1047
            self.last_name = parts[1]
1048
        else:
1049
            self.last_name = parts[0]
1050

    
1051
    def save(self, **kwargs):
1052
        if not self.id:
1053
            # set username
1054
            while not self.username:
1055
                username =  uuid.uuid4().hex[:30]
1056
                try:
1057
                    AstakosUser.objects.get(username = username)
1058
                except AstakosUser.DoesNotExist, e:
1059
                    self.username = username
1060
        super(PendingThirdPartyUser, self).save(**kwargs)
1061

    
1062
    def generate_token(self):
1063
        self.password = self.third_party_identifier
1064
        self.last_login = datetime.now()
1065
        self.token = default_token_generator.make_token(self)
1066

    
1067
class SessionCatalog(models.Model):
1068
    session_key = models.CharField(_('session key'), max_length=40)
1069
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1070

    
1071

    
1072
def create_astakos_user(u):
1073
    try:
1074
        AstakosUser.objects.get(user_ptr=u.pk)
1075
    except AstakosUser.DoesNotExist:
1076
        extended_user = AstakosUser(user_ptr_id=u.pk)
1077
        extended_user.__dict__.update(u.__dict__)
1078
        extended_user.save()
1079
        if not extended_user.has_auth_provider('local'):
1080
            extended_user.add_auth_provider('local')
1081
    except BaseException, e:
1082
        logger.exception(e)
1083

    
1084

    
1085
def fix_superusers(sender, **kwargs):
1086
    # Associate superusers with AstakosUser
1087
    admins = User.objects.filter(is_superuser=True)
1088
    for u in admins:
1089
        create_astakos_user(u)
1090

    
1091

    
1092
def user_post_save(sender, instance, created, **kwargs):
1093
    if not created:
1094
        return
1095
    create_astakos_user(instance)
1096

    
1097

    
1098
def set_default_group(user):
1099
    try:
1100
        default = AstakosGroup.objects.get(name='default')
1101
        Membership(
1102
            group=default, person=user, date_joined=datetime.now()).save()
1103
    except AstakosGroup.DoesNotExist, e:
1104
        logger.exception(e)
1105

    
1106

    
1107
def astakosuser_pre_save(sender, instance, **kwargs):
1108
    instance.aquarium_report = False
1109
    instance.new = False
1110
    try:
1111
        db_instance = AstakosUser.objects.get(id=instance.id)
1112
    except AstakosUser.DoesNotExist:
1113
        # create event
1114
        instance.aquarium_report = True
1115
        instance.new = True
1116
    else:
1117
        get = AstakosUser.__getattribute__
1118
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1119
                   BILLING_FIELDS)
1120
        instance.aquarium_report = True if l else False
1121

    
1122

    
1123
def astakosuser_post_save(sender, instance, created, **kwargs):
1124
    if instance.aquarium_report:
1125
        report_user_event(instance, create=instance.new)
1126
    if not created:
1127
        return
1128
    set_default_group(instance)
1129
    # TODO handle socket.error & IOError
1130
    register_users((instance,))
1131

    
1132

    
1133
def resource_post_save(sender, instance, created, **kwargs):
1134
    if not created:
1135
        return
1136
    register_resources((instance,))
1137

    
1138

    
1139
def send_quota_disturbed(sender, instance, **kwargs):
1140
    users = []
1141
    extend = users.extend
1142
    if sender == Membership:
1143
        if not instance.group.is_enabled:
1144
            return
1145
        extend([instance.person])
1146
    elif sender == AstakosUserQuota:
1147
        extend([instance.user])
1148
    elif sender == AstakosGroupQuota:
1149
        if not instance.group.is_enabled:
1150
            return
1151
        extend(instance.group.astakosuser_set.all())
1152
    elif sender == AstakosGroup:
1153
        if not instance.is_enabled:
1154
            return
1155
    quota_disturbed.send(sender=sender, users=users)
1156

    
1157

    
1158
def on_quota_disturbed(sender, users, **kwargs):
1159
#     print '>>>', locals()
1160
    if not users:
1161
        return
1162
    send_quota(users)
1163

    
1164
def renew_token(sender, instance, **kwargs):
1165
    if not instance.auth_token:
1166
        instance.renew_token()
1167

    
1168
post_syncdb.connect(fix_superusers)
1169
post_save.connect(user_post_save, sender=User)
1170
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1171
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1172
post_save.connect(resource_post_save, sender=Resource)
1173

    
1174
quota_disturbed = Signal(providing_args=["users"])
1175
quota_disturbed.connect(on_quota_disturbed)
1176

    
1177
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1178
post_delete.connect(send_quota_disturbed, sender=Membership)
1179
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1180
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1181
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1182
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1183

    
1184
pre_save.connect(renew_token, sender=AstakosUser)
1185
pre_save.connect(renew_token, sender=Service)