models.py 74.3 KB
Newer Older
1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
2
#
3
4
5
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
6
#
7
8
9
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
10
#
11
12
13
14
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
15
#
16
17
18
19
20
21
22
23
24
25
26
27
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
28
#
29
30
31
32
33
34
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

import hashlib
35
import uuid
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
36
import logging
37
import json
38
import math
39
import copy
40
41

from datetime import datetime, timedelta
42
import base64
43
from urllib import quote
44
from random import randint
45
import os
46

47
from django.db import models, IntegrityError, transaction
Olga Brani's avatar
Olga Brani committed
48
from django.contrib.auth.models import User, UserManager, Group, Permission
49
from django.utils.translation import ugettext as _
50
from django.db.models.signals import pre_save, post_save
Olga Brani's avatar
Olga Brani committed
51
52
from django.contrib.contenttypes.models import ContentType

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
53
from django.db.models import Q, Max
54
55
56
from django.core.urlresolvers import reverse
from django.utils.http import int_to_base36
from django.contrib.auth.tokens import default_token_generator
57
from django.conf import settings
58
from django.utils.importlib import import_module
59
from django.utils.safestring import mark_safe
60

61
62
from synnefo.lib.utils import dict_merge

63
from astakos.im import settings as astakos_settings
64
from astakos.im import auth_providers as auth
65

Olga Brani's avatar
Olga Brani committed
66
import astakos.im.messages as astakos_messages
67
from snf_django.lib.db.managers import ForUpdateManager
68
from synnefo.lib.ordereddict import OrderedDict
69

70
from snf_django.lib.db.fields import intDecimalField
71
from synnefo.util.text import uenc, udec
72
from astakos.im import presentation
73

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
74
75
logger = logging.getLogger(__name__)

Olga Brani's avatar
Olga Brani committed
76
DEFAULT_CONTENT_TYPE = None
77
78
_content_type = None

79

80
81
82
83
84
85
def get_content_type():
    global _content_type
    if _content_type is not None:
        return _content_type

    try:
86
87
        content_type = ContentType.objects.get(app_label='im',
                                               model='astakosuser')
88
89
90
91
    except:
        content_type = DEFAULT_CONTENT_TYPE
    _content_type = content_type
    return content_type
Olga Brani's avatar
Olga Brani committed
92

93
inf = float('inf')
94

95

96
97
def generate_token():
    s = os.urandom(32)
98
    return base64.urlsafe_b64encode(s).rstrip('=')
99
100


101
class Component(models.Model):
102
103
    name = models.CharField(_('Name'), max_length=255, unique=True,
                            db_index=True)
104
    url = models.CharField(_('Component url'), max_length=1024, null=True,
105
                           help_text=_("URL the component is accessible from"))
106
    auth_token = models.CharField(_('Authentication Token'), max_length=64,
107
                                  null=True, blank=True, unique=True)
108
109
110
111
    auth_token_created = models.DateTimeField(_('Token creation date'),
                                              null=True)
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
                                              null=True)
112

113
    def renew_token(self, expiration_date=None):
114
        for i in range(10):
115
            new_token = generate_token()
116
117
118
119
120
121
            count = Component.objects.filter(auth_token=new_token).count()
            if count == 0:
                break
            continue
        else:
            raise ValueError('Could not generate a token')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
122

123
        self.auth_token = new_token
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
124
        self.auth_token_created = datetime.now()
125
126
127
128
        if expiration_date:
            self.auth_token_expires = expiration_date
        else:
            self.auth_token_expires = None
129
130
        msg = 'Token renewed for component %s' % self.name
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
131

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
132
133
134
    def __str__(self):
        return self.name

135
136
137
    @classmethod
    def catalog(cls, orderfor=None):
        catalog = {}
138
139
        components = list(cls.objects.all())
        default_metadata = presentation.COMPONENTS
140
        metadata = {}
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
141

142
143
144
145
146
147
        for component in components:
            d = {'url': component.url,
                 'name': component.name}
            if component.name in default_metadata:
                metadata[component.name] = default_metadata.get(component.name)
                metadata[component.name].update(d)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
148
            else:
149
                metadata[component.name] = d
150

Olga Brani's avatar
Olga Brani committed
151

152
        def component_by_order(s):
153
            return s[1].get('order')
154

155
        def component_by_dashboard_order(s):
156
157
            return s[1].get('dashboard').get('order')

158
        metadata = dict_merge(metadata,
159
                              astakos_settings.COMPONENTS_META)
160

161
162
163
164
165
166
167
        for component, info in metadata.iteritems():
            default_meta = presentation.component_defaults(component)
            base_meta = metadata.get(component, {})
            settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
            component_meta = dict_merge(default_meta, base_meta)
            meta = dict_merge(component_meta, settings_meta)
            catalog[component] = meta
168

169
        order_key = component_by_order
170
        if orderfor == 'dashboard':
171
            order_key = component_by_dashboard_order
172
173
174
175

        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
                                             key=order_key))
        return ordered_catalog
176

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
177

178
_presentation_data = {}
179
180


181
182
def get_presentation(resource):
    global _presentation_data
183
184
185
186
187
188
    resource_presentation = _presentation_data.get(resource, {})
    if not resource_presentation:
        resources_presentation = presentation.RESOURCES.get('resources', {})
        resource_presentation = resources_presentation.get(resource, {})
        _presentation_data[resource] = resource_presentation
    return resource_presentation
189
190


191
class Service(models.Model):
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
    component = models.ForeignKey(Component)
    name = models.CharField(max_length=255, unique=True)
    type = models.CharField(max_length=255)


class Endpoint(models.Model):
    service = models.ForeignKey(Service, related_name='endpoints')


class EndpointData(models.Model):
    endpoint = models.ForeignKey(Endpoint, related_name='data')
    key = models.CharField(max_length=255)
    value = models.CharField(max_length=1024)

    class Meta:
        unique_together = (('endpoint', 'key'),)
208
209


Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
210
class Resource(models.Model):
211
    name = models.CharField(_('Name'), max_length=255, unique=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
212
    desc = models.TextField(_('Description'), null=True)
213
    service_type = models.CharField(_('Type'), max_length=255)
214
    service_origin = models.CharField(max_length=255, db_index=True)
215
    unit = models.CharField(_('Unit'), null=True, max_length=255)
216
    uplimit = intDecimalField(default=0)
217
    allow_in_projects = models.BooleanField(default=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
218

219
    objects = ForUpdateManager()
220

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
221
    def __str__(self):
222
        return self.name
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
223

224
225
226
    def full_name(self):
        return str(self)

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
227
    def get_info(self):
228
        return {'service': self.service_origin,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
229
230
                'description': self.desc,
                'unit': self.unit,
231
                'allow_in_projects': self.allow_in_projects,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
232
233
                }

234
235
236
237
238
    @property
    def group(self):
        default = self.name
        return get_presentation(str(self)).get('group', default)

239
240
    @property
    def help_text(self):
241
242
        default = "%s resource" % self.name
        return get_presentation(str(self)).get('help_text', default)
243

244
245
    @property
    def help_text_input_each(self):
246
247
        default = "%s resource" % self.name
        return get_presentation(str(self)).get('help_text_input_each', default)
248
249
250
251
252
253
254

    @property
    def is_abbreviation(self):
        return get_presentation(str(self)).get('is_abbreviation', False)

    @property
    def report_desc(self):
255
256
        default = "%s resource" % self.name
        return get_presentation(str(self)).get('report_desc', default)
257
258
259

    @property
    def placeholder(self):
260
        return get_presentation(str(self)).get('placeholder', self.unit)
261
262
263

    @property
    def verbose_name(self):
264
        return get_presentation(str(self)).get('verbose_name', self.name)
265

266
267
268
269
270
271
272
273
274
275
276
277
278
    @property
    def display_name(self):
        name = self.verbose_name
        if self.is_abbreviation:
            name = name.upper()
        return name

    @property
    def pluralized_display_name(self):
        if not self.unit:
            return '%ss' % self.display_name
        return self.display_name

279
280
def get_resource_names():
    _RESOURCE_NAMES = []
281
    resources = Resource.objects.select_related('service').all()
282
283
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
    return _RESOURCE_NAMES
284

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
285

286
class AstakosUserManager(UserManager):
287
288
289
290
291
292
293
294
295
296

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

297
298
299
    def get_by_email(self, email):
        return self.get(email=email)

300
301
302
303
304
305
306
307
308
    def get_by_identifier(self, email_or_username, **kwargs):
        try:
            return self.get(email__iexact=email_or_username, **kwargs)
        except AstakosUser.DoesNotExist:
            return self.get(username__iexact=email_or_username, **kwargs)

    def user_exists(self, email_or_username, **kwargs):
        qemail = Q(email__iexact=email_or_username)
        qusername = Q(username__iexact=email_or_username)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
309
310
311
312
313
314
315
316
        qextra = Q(**kwargs)
        return self.filter((qemail | qusername) & qextra).exists()

    def verified_user_exists(self, email_or_username):
        return self.user_exists(email_or_username, email_verified=True)

    def verified(self):
        return self.filter(email_verified=True)
317

318
319
    def accepted(self):
        return self.filter(moderated=True, is_rejected=False)
320

321
322
323
324
325
326
327
328
329
330
331
332
333
    def uuid_catalog(self, l=None):
        """
        Returns a uuid to username mapping for the uuids appearing in l.
        If l is None returns the mapping for all existing users.
        """
        q = self.filter(uuid__in=l) if l != None else self
        return dict(q.values_list('uuid', 'username'))

    def displayname_catalog(self, l=None):
        """
        Returns a username to uuid mapping for the usernames appearing in l.
        If l is None returns the mapping for all existing users.
        """
334
335
336
337
338
339
340
341
        if l is not None:
            lmap = dict((x.lower(), x) for x in l)
            q = self.filter(username__in=lmap.keys())
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
        else:
            q = self
            values = self.values_list('username', 'uuid')
        return dict(values)
342
343


344

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
345
class AstakosUser(User):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
346
347
348
    """
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
349
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
350
351
                                   null=True)

352
    #for invitations
353
    user_level = astakos_settings.DEFAULT_USER_LEVEL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
354
    level = models.IntegerField(_('Inviter level'), default=user_level)
355
    invitations = models.IntegerField(
356
        _('Invitations left'), default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
357

358
    auth_token = models.CharField(_('Authentication Token'),
359
                                  max_length=64,
360
                                  unique=True,
361
362
                                  null=True,
                                  blank=True,
363
364
365
366
367
                                  help_text = _('Renew your authentication '
                                                'token. Make sure to set the new '
                                                'token in any client you may be '
                                                'using, to preserve its '
                                                'functionality.'))
368
    auth_token_created = models.DateTimeField(_('Token creation date'),
Olga Brani's avatar
Olga Brani committed
369
                                              null=True)
370
    auth_token_expires = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
371
        _('Token expiration date'), null=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
372

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
373
    updated = models.DateTimeField(_('Update date'))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
374

375
376
377
378
379
380
381
    # Arbitrary text to identify the reason user got deactivated.
    # To be used as a reference from administrators.
    deactivated_reason = models.TextField(
        _('Reason the user was disabled for'),
        default=None, null=True)
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
                                          blank=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
382

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
383
    has_credits = models.BooleanField(_('Has credits?'), default=False)
384

385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
    # this is set to True when user profile gets updated for the first time
    is_verified = models.BooleanField(_('Is verified?'), default=False)

    # user email is verified
    email_verified = models.BooleanField(_('Email verified?'), default=False)

    # unique string used in user email verification url
    verification_code = models.CharField(max_length=255, null=True,
                                         blank=False, unique=True)

    # date user email verified
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
                                       blank=True)

    # email verification notice was sent to the user at this time
    activation_sent = models.DateTimeField(_('Activation sent date'),
                                           null=True, blank=True)

    # user got rejected during moderation process
    is_rejected = models.BooleanField(_('Account rejected'),
                                      default=False)
    # reason user got rejected
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
                                       blank=True)
    # moderation status
    moderated = models.BooleanField(_('User moderated'), default=False)
    # date user moderated (either accepted or rejected)
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
                                        blank=True, null=True)
    # a snapshot of user instance the time got moderated
    moderated_data = models.TextField(null=True, default=None, blank=True)
    # a string which identifies how the user got moderated
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
                                       default=None, null=True, blank=True)
    # the email used to accept the user
    accepted_email = models.EmailField(null=True, default=None, blank=True)

    has_signed_terms = models.BooleanField(_('I agree with the terms'),
                                           default=False)
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
                                             null=True, blank=True)
    # permanent unique user identifier
    uuid = models.CharField(max_length=255, null=True, blank=False,
                            unique=True)
429
430
431
432

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
433
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
Olga Brani's avatar
Olga Brani committed
434
                                           default=False, db_index=True)
435
436

    objects = AstakosUserManager()
437
    forupdate = ForUpdateManager()
438

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
439
440
    def __init__(self, *args, **kwargs):
        super(AstakosUser, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
441
        if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
442
            self.is_active = False
443

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
444
445
    @property
    def realname(self):
446
        return '%s %s' % (self.first_name, self.last_name)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
447

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
448
449
450
451
452
453
    @property
    def log_display(self):
        """
        Should be used in all logger.* calls that refer to a user so that
        user display is consistent across log entries.
        """
454
        return '%s::%s' % (self.uuid, self.email)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
455

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
456
457
458
459
460
461
462
463
    @realname.setter
    def realname(self, value):
        parts = value.split(' ')
        if len(parts) == 2:
            self.first_name = parts[0]
            self.last_name = parts[1]
        else:
            self.last_name = parts[0]
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
464

Olga Brani's avatar
Olga Brani committed
465
466
467
    def add_permission(self, pname):
        if self.has_perm(pname):
            return
468
469
470
471
        p, created = Permission.objects.get_or_create(
                                    codename=pname,
                                    name=pname.capitalize(),
                                    content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
472
473
474
475
476
477
        self.user_permissions.add(p)

    def remove_permission(self, pname):
        if self.has_perm(pname):
            return
        p = Permission.objects.get(codename=pname,
478
                                   content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
479
480
        self.user_permissions.remove(p)

481
482
483
484
    def add_group(self, gname):
        group, _ = Group.objects.get_or_create(name=gname)
        self.groups.add(group)

485
486
487
    def is_accepted(self):
        return self.moderated and not self.is_rejected

488
    def is_project_admin(self, application_id=None):
489
        return self.uuid in astakos_settings.PROJECT_ADMINS
490

491
492
493
    @property
    def invitation(self):
        try:
494
            return Invitation.objects.get(username=self.email)
495
496
        except Invitation.DoesNotExist:
            return None
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
497

Olga Brani's avatar
Olga Brani committed
498
499
500
501
    @property
    def policies(self):
        return self.astakosuserquota_set.select_related().all()

502
    def get_resource_policy(self, resource):
503
        resource = Resource.objects.get(name=resource)
504
        default_capacity = resource.uplimit
505
        try:
506
507
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
            return policy, default_capacity
508
        except AstakosUserQuota.DoesNotExist:
509
            return None, default_capacity
510

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
511
512
    def update_uuid(self):
        while not self.uuid:
513
            uuid_val = str(uuid.uuid4())
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
514
515
516
517
518
519
            try:
                AstakosUser.objects.get(uuid=uuid_val)
            except AstakosUser.DoesNotExist, e:
                self.uuid = uuid_val
        return self.uuid

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
520
    def save(self, update_timestamps=True, **kwargs):
521
522
        if update_timestamps:
            if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
523
                self.date_joined = datetime.now()
524
            self.updated = datetime.now()
525

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
526
527
        self.update_uuid()

528
529
530
        if not self.verification_code:
            self.renew_verification_code()

531
        # username currently matches email
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
532
        if self.username != self.email.lower():
533
            self.username = self.email.lower()
534

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
535
        super(AstakosUser, self).save(**kwargs)
536

537
538
539
540
    def renew_verification_code(self):
        self.verification_code = str(uuid.uuid4())
        logger.info("Verification code renewed for %s" % self.log_display)

541
    def renew_token(self, flush_sessions=False, current_key=None):
542
        for i in range(10):
543
            new_token = generate_token()
544
545
546
547
548
549
            count = AstakosUser.objects.filter(auth_token=new_token).count()
            if count == 0:
                break
            continue
        else:
            raise ValueError('Could not generate a token')
550

551
        self.auth_token = new_token
552
553
        self.auth_token_created = datetime.now()
        self.auth_token_expires = self.auth_token_created + \
554
                                  timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
555
556
        if flush_sessions:
            self.flush_sessions(current_key)
557
        msg = 'Token renewed for %s' % self.log_display
558
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
559

560
561
    def token_expired(self):
        return self.auth_token_expires < datetime.now()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
562

563
564
565
566
    def flush_sessions(self, current_key=None):
        q = self.sessions
        if current_key:
            q = q.exclude(session_key=current_key)
567

568
569
570
        keys = q.values_list('session_key', flat=True)
        if keys:
            msg = 'Flushing sessions: %s' % ','.join(keys)
571
            logger.log(astakos_settings.LOGGING_LEVEL, msg, [])
572
573
574
575
576
        engine = import_module(settings.SESSION_ENGINE)
        for k in keys:
            s = engine.SessionStore(k)
            s.flush()

577
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
578
        return '%s (%s)' % (self.realname, self.email)
579

580
    def conflicting_email(self):
581
        q = AstakosUser.objects.exclude(username=self.username)
582
        q = q.filter(email__iexact=self.email)
583
584
585
        if q.count() != 0:
            return True
        return False
586

587
588
589
    def email_change_is_pending(self):
        return self.emailchanges.count() > 0

590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
    @property
    def status_display(self):
        msg = ""
        append = None
        if self.is_active:
            msg = "Accepted/Active"
        if self.is_rejected:
            msg = "Rejected"
            if self.rejected_reason:
                msg += " (%s)" % self.rejected_reason
        if not self.email_verified:
            msg = "Pending email verification"
        if not self.moderated:
            msg = "Pending moderation"
        if not self.is_active and self.email_verified:
            msg = "Accepted/Inactive"
            if self.deactivated_reason:
                msg += " (%s)" % (self.deactivated_reason)

        if self.moderated and not self.is_rejected:
            if self.accepted_policy == 'manual':
                msg += " (manually accepted)"
            else:
                msg += " (accepted policy: %s)" % \
                        self.accepted_policy
        return msg

617
    @property
618
619
620
621
622
623
624
625
626
627
    def signed_terms(self):
        term = get_latest_terms()
        if not term:
            return True
        if not self.has_signed_terms:
            return False
        if not self.date_signed_terms:
            return False
        if self.date_signed_terms < term.date:
            self.has_signed_terms = False
628
            self.date_signed_terms = None
629
630
631
632
            self.save()
            return False
        return True

633
634
635
636
637
638
    def set_invitations_level(self):
        """
        Update user invitation level
        """
        level = self.invitation.inviter.level + 1
        self.level = level
639
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
640

641
642
    def can_change_password(self):
        return self.has_auth_provider('local', auth_backend='astakos')
643

644
645
646
    def can_change_email(self):
        if not self.has_auth_provider('local'):
            return True
647

648
649
        local = self.get_auth_provider('local')._instance
        return local.auth_backend == 'astakos'
650

651
652
653
654
    # Auth providers related methods
    def get_auth_provider(self, module=None, identifier=None, **filters):
        if not module:
            return self.auth_providers.active()[0].settings
655

656
657
658
659
660
        params = {'module': module}
        if identifier:
            params['identifier'] = identifier
        params.update(filters)
        return self.auth_providers.active().get(**params).settings
661

662
663
664
    def has_auth_provider(self, provider, **kwargs):
        return bool(self.auth_providers.active().filter(module=provider,
                                                        **kwargs).count())
665

666
667
    def get_required_providers(self, **kwargs):
        return auth.REQUIRED_PROVIDERS.keys()
668

669
670
671
    def missing_required_providers(self):
        required = self.get_required_providers()
        missing = []
672
673
        for provider in required:
            if not self.has_auth_provider(provider):
674
675
                missing.append(auth.get_provider(provider, self))
        return missing
676

677
    def get_available_auth_providers(self, **filters):
678
        """
679
        Returns a list of providers available for add by the user.
680
        """
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
        modules = astakos_settings.IM_MODULES
        providers = []
        for p in modules:
            providers.append(auth.get_provider(p, self))
        available = []

        for p in providers:
            if p.get_add_policy:
                available.append(p)
        return available

    def get_disabled_auth_providers(self, **filters):
        providers = self.get_auth_providers(**filters)
        disabled = []
        for p in providers:
            if not p.get_login_policy:
                disabled.append(p)
        return disabled

    def get_enabled_auth_providers(self, **filters):
        providers = self.get_auth_providers(**filters)
        enabled = []
        for p in providers:
            if p.get_login_policy:
                enabled.append(p)
        return enabled

    def get_auth_providers(self, **filters):
        providers = []
        for provider in self.auth_providers.active(**filters):
            if provider.settings.module_enabled:
                providers.append(provider.settings)
713

714
        modules = astakos_settings.IM_MODULES
715

716
717
718
719
720
721
722
        def key(p):
            if not p.module in modules:
                return 100
            return modules.index(p.module)

        providers = sorted(providers, key=key)
        return providers
723

724
725
726
727
728
    # URL methods
    @property
    def auth_providers_display(self):
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
                         self.get_enabled_auth_providers()])
729

730
731
732
    def add_auth_provider(self, module='local', identifier=None, **params):
        provider = auth.get_provider(module, self, identifier, **params)
        provider.add_to_user()
733
734

    def get_resend_activation_url(self):
735
736
        return reverse('send_activation', kwargs={'user_id': self.pk})

737
738
    def get_activation_url(self, nxt=False):
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
739
                                 quote(self.verification_code))
740
741
742
743
744
        if nxt:
            url += "&next=%s" % quote(nxt)
        return url

    def get_password_reset_url(self, token_generator=default_token_generator):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
745
        return reverse('astakos.im.views.target.local.password_reset_confirm',
746
747
748
                          kwargs={'uidb36':int_to_base36(self.id),
                                  'token':token_generator.make_token(self)})

749
750
    def get_inactive_message(self, provider_module, identifier=None):
        provider = self.get_auth_provider(provider_module, identifier)
751

752
753
        msg_extra = ''
        message = ''
754
755
756
757
758
759

        msg_inactive = provider.get_account_inactive_msg
        msg_pending = provider.get_pending_activation_msg
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
        msg_pending_mod = provider.get_pending_moderation_msg
760
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
761
762
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)

763
764
765
766
767
768
        if not self.email_verified:
            message = msg_pending
            url = self.get_resend_activation_url()
            msg_extra = msg_pending_help + \
                        u' ' + \
                        '<a href="%s">%s?</a>' % (url, msg_resend)
769
        else:
770
            if not self.moderated:
771
                message = msg_pending_mod
772
            else:
773
774
775
776
                if self.is_rejected:
                    message = msg_rejected
                else:
                    message = msg_inactive
777

778
        return mark_safe(message + u' ' + msg_extra)
779

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
780
781
782
    def owns_application(self, application):
        return application.owner == self

783
    def owns_project(self, project):
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
784
        return project.application.owner == self
785

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
786
787
788
789
790
791
792
    def is_associated(self, project):
        try:
            m = ProjectMembership.objects.get(person=self, project=project)
            return m.state in ProjectMembership.ASSOCIATED_STATES
        except ProjectMembership.DoesNotExist:
            return False

793
794
795
796
797
798
799
    def get_membership(self, project):
        try:
            return ProjectMembership.objects.get(
                project=project,
                person=self)
        except ProjectMembership.DoesNotExist:
            return None
800

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
801
802
803
804
805
806
807
    def membership_display(self, project):
        m = self.get_membership(project)
        if m is None:
            return _('Not a member')
        else:
            return m.user_friendly_state_display()

808
    def non_owner_can_view(self, maybe_project):
809
810
        if self.is_project_admin():
            return True
811
812
813
814
815
816
817
818
819
        if maybe_project is None:
            return False
        project = maybe_project
        if self.is_associated(project):
            return True
        if project.is_deactivated():
            return False
        return True

820

821
822
class AstakosUserAuthProviderManager(models.Manager):

823
824
    def active(self, **filters):
        return self.filter(active=True, **filters)
825

826
827
    def remove_unverified_providers(self, provider, **filters):
        try:
828
829
            existing = self.filter(module=provider, user__email_verified=False,
                                   **filters)
830
831
832
833
834
            for p in existing:
                p.user.delete()
        except:
            pass

835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
    def unverified(self, provider, **filters):
        try:
            return self.get(module=provider, user__email_verified=False,
                            **filters).settings
        except AstakosUserAuthProvider.DoesNotExist:
            return None

    def verified(self, provider, **filters):
        try:
            return self.get(module=provider, user__email_verified=True,
                            **filters).settings
        except AstakosUserAuthProvider.DoesNotExist:
            return None


class AuthProviderPolicyProfileManager(models.Manager):

    def active(self):
        return self.filter(active=True)

    def for_user(self, user, provider):
        policies = {}
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
        exclusive_q = exclusive_q1 | exclusive_q2

        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
            policies.update(profile.policies)

        user_groups = user.groups.all().values('pk')
        for profile in self.active().filter(groups__in=user_groups).filter(
                exclusive_q):
            policies.update(profile.policies)
        return policies

    def add_policy(self, name, provider, group_or_user, exclusive=False,
                   **policies):
        is_group = isinstance(group_or_user, Group)
        profile, created = self.get_or_create(name=name, provider=provider,
                                              is_exclusive=exclusive)
        profile.is_exclusive = exclusive
        profile.save()
        if is_group:
            profile.groups.add(group_or_user)
        else:
            profile.users.add(group_or_user)
        profile.set_policies(policies)
        profile.save()
        return profile


class AuthProviderPolicyProfile(models.Model):
    name = models.CharField(_('Name'), max_length=255, blank=False,
                            null=False, db_index=True)
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
                                null=False)

    # apply policies to all providers excluding the one set in provider field
    is_exclusive = models.BooleanField(default=False)

    policy_add = models.NullBooleanField(null=True, default=None)
    policy_remove = models.NullBooleanField(null=True, default=None)
    policy_create = models.NullBooleanField(null=True, default=None)
    policy_login = models.NullBooleanField(null=True, default=None)
    policy_limit = models.IntegerField(null=True, default=None)
    policy_required = models.NullBooleanField(null=True, default=None)
    policy_automoderate = models.NullBooleanField(null=True, default=None)
    policy_switch = models.NullBooleanField(null=True, default=None)

    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
                     'automoderate')

    priority = models.IntegerField(null=False, default=1)
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
    users = models.ManyToManyField(AstakosUser,
                                   related_name='authpolicy_profiles')
    active = models.BooleanField(default=True)

    objects = AuthProviderPolicyProfileManager()

    class Meta:
        ordering = ['priority']

    @property
    def policies(self):
        policies = {}
        for pkey in self.POLICY_FIELDS:
            value = getattr(self, 'policy_%s' % pkey, None)
            if value is None:
                continue
            policies[pkey] = value
        return policies

    def set_policies(self, policies_dict):
        for key, value in policies_dict.iteritems():
            if key in self.POLICY_FIELDS:
                setattr(self, 'policy_%s' % key, value)
        return self.policies
933

934
935
936
937
938

class AstakosUserAuthProvider(models.Model):
    """
    Available user authentication methods.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
939
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
940
941
                                   null=True, default=None)
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
942
    module = models.CharField(_('Provider'), max_length=255, blank=False,
943
                                default='local')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
944
    identifier = models.CharField(_('Third-party identifier'),
945
946
947
                                              max_length=255, null=True,
                                              blank=True)
    active = models.BooleanField(default=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
948
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
949
                                   default='astakos')
950
    info_data = models.TextField(default="", null=True, blank=True)
951
    created = models.DateTimeField('Creation date', auto_now_add=True)
952
953
954
955
956

    objects = AstakosUserAuthProviderManager()

    class Meta:
        unique_together = (('identifier', 'module', 'user'), )
957
        ordering = ('module', 'created')
958

959
960
961
962
    def __init__(self, *args, **kwargs):
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
        try:
            self.info = json.loads(self.info_data)
963
964
965
            if not self.info:
                self.info = {}
        except Exception, e:
966
            self.info = {}
967

968
969
970
        for key,value in self.info.iteritems():
            setattr(self, 'info_%s' % key, value)

971
972
    @property
    def settings(self):
973
        extra_data = {}
974

975
976
977
        info_data = {}
        if self.info_data:
            info_data = json.loads(self.info_data)
978

979
        extra_data['info'] = info_data
980

981
982
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
            extra_data[key] = getattr(self, key)
983

984
985
986
        extra_data['instance'] = self
        return auth.get_provider(self.module, self.user,
                                           self.identifier, **extra_data)
987

988
989
990
    def __repr__(self):
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)

991
992
993
994
995
996
997
    def __unicode__(self):
        if self.identifier:
            return "%s:%s" % (self.module, self.identifier)
        if self.auth_backend:
            return "%s:%s" % (self.module, self.auth_backend)
        return self.module

998
999
1000
    def save(self, *args, **kwargs):
        self.info_data = json.dumps(self.info)
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1001

1002

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1003
class ExtendedManager(models.Manager):
Olga Brani's avatar
Olga Brani committed
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
    def _update_or_create(self, **kwargs):
        assert kwargs, \
            'update_or_create() must be passed at least one keyword argument'
        obj, created = self.get_or_create(**kwargs)
        defaults = kwargs.pop('defaults', {})
        if created:
            return obj, True, False
        else:
            try:
                params = dict(
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
                params.update(defaults)
                for attr, val in params.items():
                    if hasattr(obj, attr):
                        setattr(obj, attr, val)
                sid = transaction.savepoint()
                obj.save(force_update=True)
                transaction.savepoint_commit(sid)
                return obj, False, True
            except IntegrityError, e:
                transaction.savepoint_rollback(sid)
                try:
                    return self.get(**kwargs), False, False
                except self.model.DoesNotExist:
                    raise e

    update_or_create = _update_or_create
1031

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1032
1033

class AstakosUserQuota(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1034
    objects = ExtendedManager()
1035
    capacity = intDecimalField()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1036
1037
    resource = models.ForeignKey(Resource)
    user = models.ForeignKey(AstakosUser)
1038

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1039
1040
    class Meta:
        unique_together = ("resource", "user")
1041

1042

1043
1044
1045
1046
class ApprovalTerms(models.Model):
    """
    Model for approval terms
    """
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
1047

1048
    date = models.DateTimeField(
1049
        _('Issue date'), db_index=True, auto_now_add=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1050
    location = models.CharField(_('Terms location'), max_length=255)
1051

1052

1053
class Invitation(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1054
1055
1056
    """
    Model for registring invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1057
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1058
                                null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1059
1060
1061
1062
1063
1064
    realname = models.CharField(_('Real name'), max_length=255)
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
Sofia Papagiannaki's avatar