models.py 67.2 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
# 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.

34
import uuid
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
35
import logging
36
import json
37
import copy
38
39

from datetime import datetime, timedelta
40
import base64
41
from urllib import quote
42
from random import randint
43
import os
44

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

51
from django.db.models import Q
52
53
54
from django.core.urlresolvers import reverse
from django.utils.http import int_to_base36
from django.contrib.auth.tokens import default_token_generator
55
from django.conf import settings
56
from django.utils.importlib import import_module
57
from django.utils.safestring import mark_safe
58

59
60
from synnefo.lib.utils import dict_merge

61
from astakos.im import settings as astakos_settings
62
from astakos.im import auth_providers as auth
63

Olga Brani's avatar
Olga Brani committed
64
import astakos.im.messages as astakos_messages
65
from synnefo.lib.ordereddict import OrderedDict
66

67
from synnefo.util.text import uenc, udec
68
from synnefo.util import units
69
from astakos.im import presentation
70

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
71
72
logger = logging.getLogger(__name__)

Olga Brani's avatar
Olga Brani committed
73
DEFAULT_CONTENT_TYPE = None
74
75
_content_type = None

76

77
78
79
80
81
82
def get_content_type():
    global _content_type
    if _content_type is not None:
        return _content_type

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

90
inf = float('inf')
91

92

93
94
def generate_token():
    s = os.urandom(32)
95
    return base64.urlsafe_b64encode(s).rstrip('=')
96
97


98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def _partition_by(f, l):
    d = {}
    for x in l:
        group = f(x)
        group_l = d.get(group, [])
        group_l.append(x)
        d[group] = group_l
    return d


def first_of_group(f, l):
    Nothing = type("Nothing", (), {})
    last_group = Nothing
    d = {}
    for x in l:
        group = f(x)
        if group != last_group:
            last_group = group
            d[group] = x
    return d


120
class Component(models.Model):
121
122
    name = models.CharField(_('Name'), max_length=255, unique=True,
                            db_index=True)
123
    url = models.CharField(_('Component url'), max_length=1024, null=True,
124
                           help_text=_("URL the component is accessible from"))
125
    base_url = models.CharField(max_length=1024, null=True)
126
    auth_token = models.CharField(_('Authentication Token'), max_length=64,
127
                                  null=True, blank=True, unique=True)
128
129
130
131
    auth_token_created = models.DateTimeField(_('Token creation date'),
                                              null=True)
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
                                              null=True)
132

133
    def renew_token(self, expiration_date=None):
134
        for i in range(10):
135
            new_token = generate_token()
136
137
138
139
140
141
            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
142

143
        self.auth_token = new_token
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
144
        self.auth_token_created = datetime.now()
145
146
147
148
        if expiration_date:
            self.auth_token_expires = expiration_date
        else:
            self.auth_token_expires = None
149
150
        msg = 'Token renewed for component %s'
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.name)
151

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
152
153
154
    def __str__(self):
        return self.name

155
156
157
    @classmethod
    def catalog(cls, orderfor=None):
        catalog = {}
158
159
        components = list(cls.objects.all())
        default_metadata = presentation.COMPONENTS
160
        metadata = {}
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
161

162
163
164
165
166
167
        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
168
            else:
169
                metadata[component.name] = d
170

171
        def component_by_order(s):
172
            return s[1].get('order')
173

174
        def component_by_dashboard_order(s):
175
176
            return s[1].get('dashboard').get('order')

177
        metadata = dict_merge(metadata,
178
                              astakos_settings.COMPONENTS_META)
179

180
181
182
183
184
185
186
        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
187

188
        order_key = component_by_order
189
        if orderfor == 'dashboard':
190
            order_key = component_by_dashboard_order
191
192
193
194

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
196

197
_presentation_data = {}
198
199


200
201
def get_presentation(resource):
    global _presentation_data
202
203
204
205
206
207
    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
208
209


210
class Service(models.Model):
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
    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'),)
227
228


Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
229
class Resource(models.Model):
230
    name = models.CharField(_('Name'), max_length=255, unique=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
231
    desc = models.TextField(_('Description'), null=True)
232
    service_type = models.CharField(_('Type'), max_length=255)
233
    service_origin = models.CharField(max_length=255, db_index=True)
234
    unit = models.CharField(_('Unit'), null=True, max_length=255)
235
    uplimit = models.BigIntegerField(default=0)
236
    allow_in_projects = models.BooleanField(default=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
237

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
238
    def __str__(self):
239
        return self.name
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
240

241
242
243
    def full_name(self):
        return str(self)

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
244
    def get_info(self):
245
        return {'service': self.service_origin,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
246
247
                'description': self.desc,
                'unit': self.unit,
248
                'allow_in_projects': self.allow_in_projects,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
249
250
                }

251
252
253
254
255
    @property
    def group(self):
        default = self.name
        return get_presentation(str(self)).get('group', default)

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

261
262
    @property
    def help_text_input_each(self):
263
264
        default = "%s resource" % self.name
        return get_presentation(str(self)).get('help_text_input_each', default)
265
266
267
268
269
270
271

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

    @property
    def report_desc(self):
272
273
        default = "%s resource" % self.name
        return get_presentation(str(self)).get('report_desc', default)
274
275
276

    @property
    def placeholder(self):
277
        return get_presentation(str(self)).get('placeholder', self.unit)
278
279
280

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

283
284
285
286
287
288
289
290
291
292
293
294
295
    @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

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
296

297
298
def get_resource_names():
    _RESOURCE_NAMES = []
299
    resources = Resource.objects.select_related('service').all()
300
301
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
    return _RESOURCE_NAMES
302

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
303

304
class AstakosUserManager(UserManager):
305
306
307
308
309
310
311
312
313
314

    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)

315
316
317
    def get_by_email(self, email):
        return self.get(email=email)

318
319
320
321
322
323
324
325
326
    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
327
328
329
330
331
332
333
334
        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)
335

336
337
    def accepted(self):
        return self.filter(moderated=True, is_rejected=False)
338

339
340
341
342
343
    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.
        """
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
344
        q = self.filter(uuid__in=l) if l is not None else self
345
346
347
348
349
350
351
        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.
        """
352
353
354
        if l is not None:
            lmap = dict((x.lower(), x) for x in l)
            q = self.filter(username__in=lmap.keys())
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
355
356
            values = ((lmap[n], u)
                      for n, u in q.values_list('username', 'uuid'))
357
358
359
360
        else:
            q = self
            values = self.values_list('username', 'uuid')
        return dict(values)
361
362


Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
363
class AstakosUser(User):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
364
365
366
    """
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
    """
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
367
368
    affiliation = models.CharField(_('Affiliation'), max_length=255,
                                   blank=True, null=True)
369

370
    #for invitations
371
    user_level = astakos_settings.DEFAULT_USER_LEVEL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
372
    level = models.IntegerField(_('Inviter level'), default=user_level)
373
    invitations = models.IntegerField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
374
375
376
377
378
379
380
381
382
383
384
385
386
387
        _('Invitations left'),
        default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))

    auth_token = models.CharField(
        _('Authentication Token'),
        max_length=64,
        unique=True,
        null=True,
        blank=True,
        help_text=_('Renew your authentication '
                    'token. Make sure to set the new '
                    'token in any client you may be '
                    'using, to preserve its '
                    'functionality.'))
388
    auth_token_created = models.DateTimeField(_('Token creation date'),
Olga Brani's avatar
Olga Brani committed
389
                                              null=True)
390
    auth_token_expires = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
391
        _('Token expiration date'), null=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
392

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

395
396
397
398
399
400
401
    # 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
402

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
403
    has_credits = models.BooleanField(_('Has credits?'), default=False)
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
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
    # 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)
449
450
451
452

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
453
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
454
                                          default=False, db_index=True)
455
456

    objects = AstakosUserManager()
457

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
458
459
    def __init__(self, *args, **kwargs):
        super(AstakosUser, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
460
        if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
461
            self.is_active = False
462

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
463
464
    @property
    def realname(self):
465
        return '%s %s' % (self.first_name, self.last_name)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
466

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
467
468
469
470
471
472
    @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.
        """
473
        return '%s::%s' % (self.uuid, self.email)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
474

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
475
476
477
478
479
480
481
482
    @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
483

Olga Brani's avatar
Olga Brani committed
484
485
486
    def add_permission(self, pname):
        if self.has_perm(pname):
            return
487
        p, created = Permission.objects.get_or_create(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
488
489
490
            codename=pname,
            name=pname.capitalize(),
            content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
491
492
493
494
495
496
        self.user_permissions.add(p)

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

500
501
502
503
    def add_group(self, gname):
        group, _ = Group.objects.get_or_create(name=gname)
        self.groups.add(group)

504
505
506
    def is_accepted(self):
        return self.moderated and not self.is_rejected

507
    def is_project_admin(self, application_id=None):
508
        return self.uuid in astakos_settings.PROJECT_ADMINS
509

510
511
512
    @property
    def invitation(self):
        try:
513
            return Invitation.objects.get(username=self.email)
514
515
        except Invitation.DoesNotExist:
            return None
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
516

Olga Brani's avatar
Olga Brani committed
517
518
519
520
    @property
    def policies(self):
        return self.astakosuserquota_set.select_related().all()

521
    def get_resource_policy(self, resource):
522
        resource = Resource.objects.get(name=resource)
523
        default_capacity = resource.uplimit
524
        try:
525
526
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
            return policy, default_capacity
527
        except AstakosUserQuota.DoesNotExist:
528
            return None, default_capacity
529

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
530
531
    def update_uuid(self):
        while not self.uuid:
532
            uuid_val = str(uuid.uuid4())
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
533
534
            try:
                AstakosUser.objects.get(uuid=uuid_val)
535
            except AstakosUser.DoesNotExist:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
536
537
538
                self.uuid = uuid_val
        return self.uuid

539
540
541
    def save(self, update_timestamps=True, **kwargs):
        if update_timestamps:
            if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
542
                self.date_joined = datetime.now()
543
            self.updated = datetime.now()
544

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
545
546
        self.update_uuid()

547
548
549
        if not self.verification_code:
            self.renew_verification_code()

550
        # username currently matches email
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
551
        if self.username != self.email.lower():
552
            self.username = self.email.lower()
553

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
554
        super(AstakosUser, self).save(**kwargs)
555

556
557
558
559
    def renew_verification_code(self):
        self.verification_code = str(uuid.uuid4())
        logger.info("Verification code renewed for %s" % self.log_display)

560
    def renew_token(self, flush_sessions=False, current_key=None):
561
        for i in range(10):
562
            new_token = generate_token()
563
564
565
566
567
568
            count = AstakosUser.objects.filter(auth_token=new_token).count()
            if count == 0:
                break
            continue
        else:
            raise ValueError('Could not generate a token')
569

570
        self.auth_token = new_token
571
572
        self.auth_token_created = datetime.now()
        self.auth_token_expires = self.auth_token_created + \
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
573
            timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
574
575
        if flush_sessions:
            self.flush_sessions(current_key)
576
577
        msg = 'Token renewed for %s'
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
578

579
580
    def token_expired(self):
        return self.auth_token_expires < datetime.now()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
581

582
583
584
585
    def flush_sessions(self, current_key=None):
        q = self.sessions
        if current_key:
            q = q.exclude(session_key=current_key)
586

587
588
        keys = q.values_list('session_key', flat=True)
        if keys:
589
590
            msg = 'Flushing sessions: %s'
            logger.log(astakos_settings.LOGGING_LEVEL, msg, ','.join(keys))
591
592
593
594
595
        engine = import_module(settings.SESSION_ENGINE)
        for k in keys:
            s = engine.SessionStore(k)
            s.flush()

596
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
597
        return '%s (%s)' % (self.realname, self.email)
598

599
    def conflicting_email(self):
600
        q = AstakosUser.objects.exclude(username=self.username)
601
        q = q.filter(email__iexact=self.email)
602
603
604
        if q.count() != 0:
            return True
        return False
605

606
607
608
    def email_change_is_pending(self):
        return self.emailchanges.count() > 0

609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
    @property
    def status_display(self):
        msg = ""
        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)" % \
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
632
                    self.accepted_policy
633
634
        return msg

635
    @property
636
637
638
639
640
641
642
643
644
645
    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
646
            self.date_signed_terms = None
647
648
649
650
            self.save()
            return False
        return True

651
652
653
654
655
656
    def set_invitations_level(self):
        """
        Update user invitation level
        """
        level = self.invitation.inviter.level + 1
        self.level = level
657
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
658

659
660
    def can_change_password(self):
        return self.has_auth_provider('local', auth_backend='astakos')
661

662
663
664
    def can_change_email(self):
        if not self.has_auth_provider('local'):
            return True
665

666
667
        local = self.get_auth_provider('local')._instance
        return local.auth_backend == 'astakos'
668

669
670
671
672
    # Auth providers related methods
    def get_auth_provider(self, module=None, identifier=None, **filters):
        if not module:
            return self.auth_providers.active()[0].settings
673

674
675
676
677
678
        params = {'module': module}
        if identifier:
            params['identifier'] = identifier
        params.update(filters)
        return self.auth_providers.active().get(**params).settings
679

680
681
682
    def has_auth_provider(self, provider, **kwargs):
        return bool(self.auth_providers.active().filter(module=provider,
                                                        **kwargs).count())
683

684
685
    def get_required_providers(self, **kwargs):
        return auth.REQUIRED_PROVIDERS.keys()
686

687
688
689
    def missing_required_providers(self):
        required = self.get_required_providers()
        missing = []
690
691
        for provider in required:
            if not self.has_auth_provider(provider):
692
693
                missing.append(auth.get_provider(provider, self))
        return missing
694

695
    def get_available_auth_providers(self, **filters):
696
        """
697
        Returns a list of providers available for add by the user.
698
        """
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
        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)
731

732
        modules = astakos_settings.IM_MODULES
733

734
735
736
737
738
739
740
        def key(p):
            if not p.module in modules:
                return 100
            return modules.index(p.module)

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

742
743
744
745
746
    # 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()])
747

748
749
750
    def add_auth_provider(self, module='local', identifier=None, **params):
        provider = auth.get_provider(module, self, identifier, **params)
        provider.add_to_user()
751
752

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

755
756
    def get_activation_url(self, nxt=False):
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
757
                              quote(self.verification_code))
758
759
760
761
762
        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
763
        return reverse('astakos.im.views.target.local.password_reset_confirm',
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
764
765
                       kwargs={'uidb36': int_to_base36(self.id),
                               'token': token_generator.make_token(self)})
766

767
768
    def get_inactive_message(self, provider_module, identifier=None):
        provider = self.get_auth_provider(provider_module, identifier)
769

770
771
        msg_extra = ''
        message = ''
772
773
774
775
776
777

        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
778
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
779
780
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)

781
782
783
784
        if not self.email_verified:
            message = msg_pending
            url = self.get_resend_activation_url()
            msg_extra = msg_pending_help + \
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
785
786
                u' ' + \
                '<a href="%s">%s?</a>' % (url, msg_resend)
787
        else:
788
            if not self.moderated:
789
                message = msg_pending_mod
790
            else:
791
792
793
794
                if self.is_rejected:
                    message = msg_rejected
                else:
                    message = msg_inactive
795

796
        return mark_safe(message + u' ' + msg_extra)
797

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
798
799
800
    def owns_application(self, application):
        return application.owner == self

801
    def owns_project(self, project):
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
802
        return project.application.owner == self
803

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
804
805
806
807
808
809
810
    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

811
812
813
814
815
816
817
    def get_membership(self, project):
        try:
            return ProjectMembership.objects.get(
                project=project,
                person=self)
        except ProjectMembership.DoesNotExist:
            return None
818

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
819
820
821
822
823
824
825
    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()

826
    def non_owner_can_view(self, maybe_project):
827
828
        if self.is_project_admin():
            return True
829
830
831
832
833
834
835
836
837
        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

838

839
840
class AstakosUserAuthProviderManager(models.Manager):

841
842
    def active(self, **filters):
        return self.filter(active=True, **filters)
843

844
845
    def remove_unverified_providers(self, provider, **filters):
        try:
846
847
            existing = self.filter(module=provider, user__email_verified=False,
                                   **filters)
848
849
850
851
852
            for p in existing:
                p.user.delete()
        except:
            pass

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
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
    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
951

952
953
954
955
956

class AstakosUserAuthProvider(models.Model):
    """
    Available user authentication methods.
    """
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
957
958
    affiliation = models.CharField(_('Affiliation'), max_length=255,
                                   blank=True, null=True, default=None)
959
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
960
    module = models.CharField(_('Provider'), max_length=255, blank=False,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
961
                              default='local')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
962
    identifier = models.CharField(_('Third-party identifier'),
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
963
964
                                  max_length=255, null=True,
                                  blank=True)
965
    active = models.BooleanField(default=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
966
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
967
                                    default='astakos')
968
    info_data = models.TextField(default="", null=True, blank=True)
969
    created = models.DateTimeField('Creation date', auto_now_add=True)
970
971
972
973
974

    objects = AstakosUserAuthProviderManager()

    class Meta:
        unique_together = (('identifier', 'module', 'user'), )
975
        ordering = ('module', 'created')
976

977
978
979
980
    def __init__(self, *args, **kwargs):
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
        try:
            self.info = json.loads(self.info_data)
981
982
            if not self.info:
                self.info = {}
983
        except Exception:
984
            self.info = {}
985

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
986
        for key, value in self.info.iteritems():
987
988
            setattr(self, 'info_%s' % key, value)

989
990
    @property
    def settings(self):
991
        extra_data = {}
992

993
994
995
        info_data = {}
        if self.info_data:
            info_data = json.loads(self.info_data)
996

997
        extra_data['info'] = info_data
998

999
1000
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
            extra_data[key] = getattr(self, key)
1001

1002
1003
        extra_data['instance'] = self
        return auth.get_provider(self.module, self.user,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
1004
                                 self.identifier, **extra_data)
1005

1006
    def __repr__(self):
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
1007
1008
        return '<AstakosUserAuthProvider %s:%s>' % (
            self.module, self.identifier)
1009

1010
1011
1012
1013
1014
1015
1016
    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

1017
1018
1019
    def save(self, *args, **kwargs):
        self.info_data = json.dumps(self.info)
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1020

1021

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1022
class AstakosUserQuota(models.Model):
1023
    capacity = models.BigIntegerField()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1024
1025
    resource = models.ForeignKey(Resource)
    user = models.ForeignKey(AstakosUser)
1026

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1027
1028
    class Meta:
        unique_together = ("resource", "user")
1029

1030

1031
1032
1033
1034
class ApprovalTerms(models.Model):
    """
    Model for approval terms
    """
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
1035

1036
    date = models.DateTimeField(
1037
        _('Issue date'), db_index=True, auto_now_add=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1038
    location = models.CharField(_('Terms location'), max_length=255)
1039

1040

1041
class Invitation(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1042
1043
1044
    """
    Model for registring invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1045
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1046
                                null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1047
1048
1049
1050
1051
    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)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
1052
1053
    consumed = models.DateTimeField(_('Consumption date'),
                                    null=True, blank=True)
1054

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1055
1056
    def __init__(self, *args, **kwargs):