models.py 66.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
    project_default = models.BigIntegerField()
237 238
    ui_visible = models.BooleanField(default=True)
    api_visible = models.BooleanField(default=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
239

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

243 244 245
    def full_name(self):
        return str(self)

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
246
    def get_info(self):
247
        return {'service': self.service_origin,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
248 249
                'description': self.desc,
                'unit': self.unit,
250 251
                'ui_visible': self.ui_visible,
                'api_visible': self.api_visible,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
252 253
                }

254 255 256 257 258
    @property
    def group(self):
        default = self.name
        return get_presentation(str(self)).get('group', default)

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

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

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

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

    @property
    def placeholder(self):
280
        return get_presentation(str(self)).get('placeholder', self.unit)
281 282 283

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

286 287 288 289 290 291 292 293 294 295 296 297 298
    @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
299

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

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
306

307 308 309 310 311 312 313 314
def split_realname(value):
    parts = value.split(' ')
    if len(parts) == 2:
        return parts
    else:
        return ('', value)


315
class AstakosUserManager(UserManager):
316 317 318 319 320 321 322 323 324 325

    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)

326 327 328
    def get_by_email(self, email):
        return self.get(email=email)

329 330 331 332 333 334 335 336 337
    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
338 339 340
        qextra = Q(**kwargs)
        return self.filter((qemail | qusername) & qextra).exists()

341 342 343 344 345
    def unverified_namesakes(self, email_or_username):
        q = Q(email__iexact=email_or_username)
        q |= Q(username__iexact=email_or_username)
        return self.filter(q & Q(email_verified=False))

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
346 347 348 349 350
    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)
351

352 353
    def accepted(self):
        return self.filter(moderated=True, is_rejected=False)
354

355 356 357 358 359
    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
360
        q = self.filter(uuid__in=l) if l is not None else self
361 362 363 364 365 366 367
        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.
        """
368 369 370
        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
371 372
            values = ((lmap[n], u)
                      for n, u in q.values_list('username', 'uuid'))
373 374 375 376
        else:
            q = self
            values = self.values_list('username', 'uuid')
        return dict(values)
377 378


Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
379
class AstakosUser(User):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
380 381 382
    """
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
    """
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
383 384
    affiliation = models.CharField(_('Affiliation'), max_length=255,
                                   blank=True, null=True)
385

386
    #for invitations
387
    user_level = astakos_settings.DEFAULT_USER_LEVEL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
388
    level = models.IntegerField(_('Inviter level'), default=user_level)
389
    invitations = models.IntegerField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
390 391 392 393 394 395 396 397 398 399 400 401 402 403
        _('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.'))
404
    auth_token_created = models.DateTimeField(_('Token creation date'),
Olga Brani's avatar
Olga Brani committed
405
                                              null=True)
406
    auth_token_expires = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
407
        _('Token expiration date'), null=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
408

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

411 412 413 414 415 416 417
    # 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
418

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
419
    has_credits = models.BooleanField(_('Has credits?'), default=False)
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 449 450 451 452 453 454 455 456 457 458 459 460 461 462
    # 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
463
    uuid = models.CharField(max_length=255, null=False, blank=False,
464
                            unique=True)
465 466 467 468

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
469
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
470
                                          default=False, db_index=True)
471 472

    objects = AstakosUserManager()
473

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
474 475
    @property
    def realname(self):
476
        return '%s %s' % (self.first_name, self.last_name)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
477

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
478 479 480 481 482 483
    @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.
        """
484
        return '%s::%s' % (self.uuid, self.email)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
485

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
486 487
    @realname.setter
    def realname(self, value):
488 489 490
        first, last = split_realname(value)
        self.first_name = first
        self.last_name = last
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
491

Olga Brani's avatar
Olga Brani committed
492 493 494
    def add_permission(self, pname):
        if self.has_perm(pname):
            return
495
        p, created = Permission.objects.get_or_create(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
496 497 498
            codename=pname,
            name=pname.capitalize(),
            content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
499 500 501 502 503 504
        self.user_permissions.add(p)

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

508 509 510 511
    def add_group(self, gname):
        group, _ = Group.objects.get_or_create(name=gname)
        self.groups.add(group)

512 513 514
    def is_accepted(self):
        return self.moderated and not self.is_rejected

515
    def is_project_admin(self, application_id=None):
516
        return self.uuid in astakos_settings.PROJECT_ADMINS
517

518 519 520
    @property
    def invitation(self):
        try:
521
            return Invitation.objects.get(username=self.email)
522 523
        except Invitation.DoesNotExist:
            return None
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
524

Olga Brani's avatar
Olga Brani committed
525 526 527 528
    @property
    def policies(self):
        return self.astakosuserquota_set.select_related().all()

529
    def get_resource_policy(self, resource):
530 531
        return AstakosUserQuota.objects.select_related("resource").\
            get(user=self, resource__name=resource)
532

533 534 535 536 537 538
    def fix_username(self):
        self.username = self.email.lower()

    def set_email(self, email):
        self.email = email
        self.fix_username()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
539

540 541 542
    def save(self, update_timestamps=True, **kwargs):
        if update_timestamps:
            self.updated = datetime.now()
543

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

546 547 548 549
    def renew_verification_code(self):
        self.verification_code = str(uuid.uuid4())
        logger.info("Verification code renewed for %s" % self.log_display)

550
    def renew_token(self, flush_sessions=False, current_key=None):
551
        for i in range(10):
552
            new_token = generate_token()
553 554 555 556 557 558
            count = AstakosUser.objects.filter(auth_token=new_token).count()
            if count == 0:
                break
            continue
        else:
            raise ValueError('Could not generate a token')
559

560
        self.auth_token = new_token
561 562
        self.auth_token_created = datetime.now()
        self.auth_token_expires = self.auth_token_created + \
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
563
            timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
564 565
        if flush_sessions:
            self.flush_sessions(current_key)
566
        self.delete_online_access_tokens()
567 568
        msg = 'Token renewed for %s'
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
569

570 571
    def token_expired(self):
        return self.auth_token_expires < datetime.now()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
572

573 574 575 576
    def flush_sessions(self, current_key=None):
        q = self.sessions
        if current_key:
            q = q.exclude(session_key=current_key)
577

578 579
        keys = q.values_list('session_key', flat=True)
        if keys:
580 581
            msg = 'Flushing sessions: %s'
            logger.log(astakos_settings.LOGGING_LEVEL, msg, ','.join(keys))
582 583 584 585 586
        engine = import_module(settings.SESSION_ENGINE)
        for k in keys:
            s = engine.SessionStore(k)
            s.flush()

587
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
588
        return '%s (%s)' % (self.realname, self.email)
589

590
    def conflicting_email(self):
591
        q = AstakosUser.objects.exclude(username=self.username)
592
        q = q.filter(email__iexact=self.email)
593 594 595
        if q.count() != 0:
            return True
        return False
596

597 598 599
    def email_change_is_pending(self):
        return self.emailchanges.count() > 0

600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
    @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
623
                    self.accepted_policy
624 625
        return msg

626
    @property
627
    def signed_terms(self):
628
        return self.has_signed_terms
629

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

638 639
    def can_change_password(self):
        return self.has_auth_provider('local', auth_backend='astakos')
640

641 642 643
    def can_change_email(self):
        if not self.has_auth_provider('local'):
            return True
644

645 646
        local = self.get_auth_provider('local')._instance
        return local.auth_backend == 'astakos'
647

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

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

659 660 661
    def has_auth_provider(self, provider, **kwargs):
        return bool(self.auth_providers.active().filter(module=provider,
                                                        **kwargs).count())
662

663 664
    def get_required_providers(self, **kwargs):
        return auth.REQUIRED_PROVIDERS.keys()
665

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

674
    def get_available_auth_providers(self, **filters):
675
        """
676
        Returns a list of providers available for add by the user.
677
        """
678 679 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
        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)
710

711
        modules = astakos_settings.IM_MODULES
712

713 714 715 716 717 718 719
        def key(p):
            if not p.module in modules:
                return 100
            return modules.index(p.module)

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

721 722 723
    # URL methods
    @property
    def auth_providers_display(self):
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
724
        return ",".join(["%s:%s" % (p.module, p.identifier) for p in
725
                         self.get_enabled_auth_providers()])
726

727 728 729
    def add_auth_provider(self, module='local', identifier=None, **params):
        provider = auth.get_provider(module, self, identifier, **params)
        provider.add_to_user()
730 731

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

734 735
    def get_activation_url(self, nxt=False):
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
736
                              quote(self.verification_code))
737 738 739 740 741
        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
742
        return reverse('astakos.im.views.target.local.password_reset_confirm',
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
743 744
                       kwargs={'uidb36': int_to_base36(self.id),
                               'token': token_generator.make_token(self)})
745

746
    def get_inactive_message(self, provider_module, identifier=None):
747 748 749 750
        try:
            provider = self.get_auth_provider(provider_module, identifier)
        except AstakosUserAuthProvider.DoesNotExist:
            provider = auth.get_provider(provider_module, self)
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
        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
767 768
                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 823 824 825
    def delete_online_access_tokens(self):
        offline_tokens = self.token_set.filter(access_token='online')
        logger.info('The following access tokens will be deleted: %s',
                    offline_tokens)
        offline_tokens.delete()

826

827 828
class AstakosUserAuthProviderManager(models.Manager):

829 830
    def active(self, **filters):
        return self.filter(active=True, **filters)
831

832 833
    def remove_unverified_providers(self, provider, **filters):
        try:
834 835
            existing = self.filter(module=provider, user__email_verified=False,
                                   **filters)
836 837 838 839 840
            for p in existing:
                p.user.delete()
        except:
            pass

841 842
    def unverified(self, provider, **filters):
        try:
843 844 845 846

            return self.select_for_update().get(module=provider,
                                                user__email_verified=False,
                                                **filters).settings
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 933 934 935 936 937 938 939 940
        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
941

942 943 944 945 946

class AstakosUserAuthProvider(models.Model):
    """
    Available user authentication methods.
    """
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
947 948
    affiliation =