Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / auth_providers.py @ fb9d96e9

History | View | Annotate | Download (22.7 kB)

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

    
34
import copy
35
import json
36

    
37
from synnefo.lib.ordereddict import OrderedDict
38

    
39
from django.core.urlresolvers import reverse, NoReverseMatch
40
from django.utils.translation import ugettext as _
41
from django.contrib.auth.models import Group
42
from django import template
43

    
44
from django.conf import settings
45

    
46
from astakos.im import settings as astakos_settings
47
from astakos.im import messages as astakos_messages
48

    
49
from synnefo_branding import utils as branding_utils
50

    
51
import logging
52

    
53
logger = logging.getLogger(__name__)
54

    
55
# providers registry
56
PROVIDERS = {}
57
REQUIRED_PROVIDERS = {}
58

    
59

    
60
class InvalidProvider(Exception):
61
    pass
62

    
63

    
64
class AuthProviderBase(type):
65

    
66
    def __new__(cls, name, bases, dct):
67
        include = False
68
        if [b for b in bases if isinstance(b, AuthProviderBase)]:
69
            type_id = dct.get('module')
70
            if type_id:
71
                include = True
72
            if type_id in astakos_settings.IM_MODULES:
73
                if astakos_settings.IM_MODULES.index(type_id) == 0:
74
                    dct['is_primary'] = True
75
                dct['module_enabled'] = True
76

    
77
        newcls = super(AuthProviderBase, cls).__new__(cls, name, bases, dct)
78
        if include:
79
            PROVIDERS[type_id] = newcls
80
            if newcls().get_required_policy:
81
                REQUIRED_PROVIDERS[type_id] = newcls
82
        return newcls
83

    
84

    
85
class AuthProvider(object):
86

    
87
    __metaclass__ = AuthProviderBase
88

    
89
    module = None
90
    module_enabled = False
91
    is_primary = False
92

    
93
    message_tpls = OrderedDict((
94
        ('title', '{module_title}'),
95
        ('login_title', '{title} LOGIN'),
96
        ('method_prompt', '{title} login'),
97
        ('account_prompt', '{title} account'),
98
        ('signup_title', '{title}'),
99
        ('profile_title', '{title}'),
100
        ('method_details', '{account_prompt}: {identifier}'),
101
        ('primary_login_prompt', 'Login using '),
102
        ('required', '{title} is required. You can assign it '
103
                     'from your profile page'),
104
        ('login_prompt', ''),
105
        ('add_prompt', 'Allows you to login using {title}'),
106
        ('login_extra', ''),
107
        ('username', '{username}'),
108
        ('disabled_for_create', 'It seems this is the first time you\'re '
109
                                'trying to access {service_name}. '
110
                                'Unfortunately, we are not accepting new '
111
                                'users at this point.'),
112
        ('switch_success', 'Account changed successfully.'),
113
        ('cannot_login', '{title} is not available for login. '
114
                         'Please use one of your other available methods '
115
                         'to login ({available_methods_links}'),
116

    
117
        # icons should end with _icon
118
        ('module_medium_icon', 'im/auth/icons-medium/{module}.png'),
119
        ('module_icon', 'im/auth/icons/{module}.png'))
120
    )
121

    
122
    messages = {}
123
    module_urls = {}
124

    
125
    remote_authenticate = True
126
    remote_logout_url = None
127

    
128
    # templates
129
    primary_login_template = 'im/auth/generic_primary_login.html'
130
    login_template = 'im/auth/generic_login.html'
131
    signup_template = 'im/signup.html'
132
    login_prompt_template = 'im/auth/generic_login_prompt.html'
133
    signup_prompt_template = 'im/auth/signup_prompt.html'
134

    
135
    default_policies = {
136
        'login': True,
137
        'create': True,
138
        'add': True,
139
        'remove': True,
140
        'limit': 1,
141
        'switch': True,
142
        'add_groups': [],
143
        'creation_groups': [],
144
        'required': False,
145
        'automoderate': not astakos_settings.MODERATION_ENABLED
146
    }
147

    
148
    policies = {}
149

    
150
    def __init__(self, user=None, identifier=None, **provider_params):
151
        """
152
        3 ways to initialize (no args, user, user and identifier).
153

154
        no args: Used for anonymous unauthenticated users.
155
        >>> p = auth_providers.get_provider('local')
156
        >>> # check that global settings allows us to create a new account
157
        >>> # using `local` provider.
158
        >>> print p.is_available_for_create()
159

160
        user and identifier: Used to provide details about a user's specific
161
        login method.
162
        >>> p = auth_providers.get_provider('google', user,
163
        >>>                                 identifier='1421421')
164
        >>> # provider (google) details prompt
165
        >>> print p.get_method_details()
166
        "Google account: 1421421"
167
        """
168

    
169
        # handle AnonymousUser instance
170
        self.user = None
171
        if user and hasattr(user, 'pk') and user.pk:
172
            self.user = user
173

    
174
        self.identifier = identifier
175
        self._instance = None
176
        if 'instance' in provider_params:
177
            self._instance = provider_params['instance']
178
            del provider_params['instance']
179

    
180
        # initialize policies
181
        self.module_policies = copy.copy(self.default_policies)
182
        self.module_policies['automoderate'] = not \
183
            astakos_settings.MODERATION_ENABLED
184
        for policy, value in self.policies.iteritems():
185
            setting_key = "%s_POLICY" % policy.upper()
186
            if self.has_setting(setting_key):
187
                self.module_policies[policy] = self.get_setting(setting_key)
188
            else:
189
                self.module_policies[policy] = value
190

    
191
        # messages cache
192
        self.message_tpls_compiled = OrderedDict()
193

    
194
        # module specific messages
195
        self.message_tpls = OrderedDict(self.message_tpls)
196
        for key, value in self.messages.iteritems():
197
            self.message_tpls[key] = value
198

    
199
        self._provider_details = provider_params
200

    
201
        self.resolve_available_methods = True
202

    
203
    def get_provider_model(self):
204
        from astakos.im.models import AstakosUserAuthProvider as AuthProvider
205
        return AuthProvider
206

    
207
    def remove_from_user(self):
208
        if not self.get_remove_policy:
209
            raise Exception("Provider cannot be removed")
210

    
211
        for group_name in self.get_add_groups_policy:
212
            group = Group.objects.get(name=group_name)
213
            self.user.groups.remove(group)
214
            self.log('removed from group due to add_groups_policy %s',
215
                     group.name)
216

    
217
        self._instance.delete()
218
        self.log('removed')
219

    
220
    def add_to_user(self, **params):
221
        if self._instance:
222
            raise Exception("Cannot add an existing provider")
223

    
224
        create = False
225
        if self.get_user_providers().count() == 0:
226
            create = True
227

    
228
        if create and not self.get_create_policy:
229
            raise Exception("Provider not available for create")
230

    
231
        if not self.get_add_policy:
232
            raise Exception("Provider cannot be added")
233

    
234
        if create:
235
            for group_name in self.get_creation_groups_policy:
236
                group, created = Group.objects.get_or_create(name=group_name)
237
                self.user.groups.add(group)
238
                self.log("added to %s group due to creation_groups_policy",
239
                         group_name)
240

    
241
        for group_name in self.get_add_groups_policy:
242
            group, created = Group.objects.get_or_create(name=group_name)
243
            self.user.groups.add(group)
244
            self.log("added to %s group due to add_groups_policy",
245
                     group_name)
246

    
247
        if self.identifier:
248
            pending = self.get_provider_model().objects.unverified(
249
                self.module, identifier=self.identifier)
250

    
251
            if pending:
252
                user = pending._instance.user
253
                logger.info("Removing existing unverified user (%r)",
254
                            user.log_display)
255
                user.delete()
256

    
257
        create_params = {
258
            'module': self.module,
259
            'info_data': json.dumps(self.provider_details.get('info', {})),
260
            'active': True,
261
            'identifier': self.identifier
262
        }
263
        if 'info' in self.provider_details:
264
            del self.provider_details['info']
265

    
266
        create_params.update(self.provider_details)
267
        create_params.update(params)
268
        create = self.user.auth_providers.create(**create_params)
269
        self.log("created %r" % create_params)
270
        return create
271

    
272
    def __repr__(self):
273
        r = "'%r' module" % self.__class__.__name__
274
        if self.user:
275
            r += ' (user: %r)' % self.user
276
        if self.identifier:
277
            r += '(identifier: %r)' % self.identifier
278
        return r
279

    
280
    def _message_params(self, **extra_params):
281
        """
282
        Retrieve message formating parameters.
283
        """
284
        params = {'module': self.module, 'module_title': self.module.title()}
285
        if self.identifier:
286
            params['identifier'] = self.identifier
287

    
288
        if self.user:
289
            for key, val in self.user.__dict__.iteritems():
290
                params["user_%s" % key.lower()] = val
291

    
292
        if self.provider_details:
293
            for key, val in self.provider_details.iteritems():
294
                params["provider_%s" % key.lower()] = val
295

    
296
            if 'info' in self.provider_details:
297
                if isinstance(self.provider_details['info'], basestring):
298
                    self.provider_details['info'] = \
299
                        json.loads(self.provider_details['info'])
300
                for key, val in self.provider_details['info'].iteritems():
301
                    params['provider_info_%s' % key.lower()] = val
302

    
303
        # resolve username, handle unexisting defined username key
304
        if self.user and self.username_key in params:
305
            params['username'] = params[self.username_key]
306
        else:
307
            params['username'] = self.identifier
308

    
309
        branding_params = dict(
310
            map(lambda k: (k[0].lower(), k[1]),
311
                branding_utils.get_branding_dict().iteritems()))
312
        params.update(branding_params)
313

    
314
        if not self.message_tpls_compiled:
315
            for key, message_tpl in self.message_tpls.iteritems():
316
                msg = self.messages.get(key, self.message_tpls.get(key))
317
                override_in_settings = self.get_setting(key)
318
                if override_in_settings is not None:
319
                    msg = override_in_settings
320
                try:
321
                    self.message_tpls_compiled[key] = msg.format(**params)
322
                    params.update(self.message_tpls_compiled)
323
                except KeyError, e:
324
                    continue
325
        else:
326
            params.update(self.message_tpls_compiled)
327

    
328
        for key, value in self.urls.iteritems():
329
            params['%s_url' % key] = value
330

    
331
        if self.user and self.resolve_available_methods:
332
            available_providers = self.user.get_enabled_auth_providers()
333
            for p in available_providers:
334
                p.resolve_available_methods = False
335
                if p.module == self.module and p.identifier == self.identifier:
336
                    available_providers.remove(p)
337

    
338
            get_msg = lambda p: p.get_method_prompt_msg
339
            params['available_methods'] = \
340
                ','.join(map(get_msg, available_providers))
341

    
342
            get_msg = lambda p: "<a href='%s'>%s</a>" % \
343
                (p.get_login_url, p.get_method_prompt_msg)
344

    
345
            params['available_methods_links'] = \
346
                ','.join(map(get_msg, available_providers))
347

    
348
        params.update(extra_params)
349
        return params
350

    
351
    def get_template(self, tpl):
352
        tpls = ['im/auth/%s_%s.html' % (self.module, tpl),
353
                getattr(self, '%s_template' % tpl)]
354
        found = None
355
        for tpl in tpls:
356
            try:
357
                found = template.loader.get_template(tpl)
358
                return tpl
359
            except template.TemplateDoesNotExist:
360
                continue
361
        if not found:
362
            raise template.TemplateDoesNotExist
363
        return tpl
364

    
365
    def get_username(self):
366
        return self.get_username_msg
367

    
368
    def get_user_providers(self):
369
        return self.user.auth_providers.active().filter(
370
            module__in=astakos_settings.IM_MODULES)
371

    
372
    def get_user_module_providers(self):
373
        return self.user.auth_providers.active().filter(module=self.module)
374

    
375
    def get_existing_providers(self):
376
        return ""
377

    
378
    def verified_exists(self):
379
        return self.get_provider_model().objects.verified(
380
            self.module, identifier=self.identifier)
381

    
382
    def resolve_policy(self, policy, default=None):
383

    
384
        if policy == 'switch' and default and not self.get_add_policy:
385
            return not self.get_policy('remove')
386

    
387
        if not self.user:
388
            return default
389

    
390
        if policy == 'remove' and default is True:
391
            return self.get_user_providers().count() > 1
392

    
393
        if policy == 'add' and default is True:
394
            limit = self.get_policy('limit')
395
            if limit <= self.get_user_module_providers().count():
396
                return False
397

    
398
            if self.identifier:
399
                if self.verified_exists():
400
                    return False
401

    
402
        return default
403

    
404
    def get_user_policies(self):
405
        from astakos.im.models import AuthProviderPolicyProfile
406
        return AuthProviderPolicyProfile.objects.for_user(self.user,
407
                                                          self.module)
408

    
409
    def get_policy(self, policy):
410
        module_default = self.module_policies.get(policy)
411
        settings_key = '%s_POLICY' % policy.upper()
412
        settings_default = self.get_setting(settings_key, module_default)
413

    
414
        if self.user:
415
            user_policies = self.get_user_policies()
416
            settings_default = user_policies.get(policy, settings_default)
417

    
418
        return self.resolve_policy(policy, settings_default)
419

    
420
    def get_message(self, msg, **extra_params):
421
        """
422
        Retrieve an auth provider message
423
        """
424
        if msg.endswith('_msg'):
425
            msg = msg.replace('_msg', '')
426
        params = self._message_params(**extra_params)
427

    
428
        # is message ???
429
        tpl = self.message_tpls_compiled.get(msg.lower(), None)
430
        if not tpl:
431
            msg_key = 'AUTH_PROVIDER_%s' % msg.upper()
432
            try:
433
                tpl = getattr(astakos_messages, msg_key)
434
            except AttributeError, e:
435
                try:
436
                    msg_key = msg.upper()
437
                    tpl = getattr(astakos_messages, msg_key)
438
                except AttributeError, e:
439
                    tpl = ''
440

    
441
        in_settings = self.get_setting(msg)
442
        if in_settings:
443
            tpl = in_settings
444

    
445
        return tpl.format(**params)
446

    
447
    @property
448
    def urls(self):
449
        urls = {
450
            'login': reverse(self.login_view),
451
            'add': reverse(self.login_view),
452
            'profile': reverse('edit_profile'),
453
        }
454
        if self.user:
455
            urls.update({
456
                'resend_activation': self.user.get_resend_activation_url(),
457
            })
458
        if self.identifier and self._instance:
459
            urls.update({
460
                'switch': reverse(self.login_view) + '?switch_from=%d' %
461
                self._instance.pk,
462
                'remove': reverse('remove_auth_provider',
463
                                  kwargs={'pk': self._instance.pk})
464
            })
465
        urls.update(self.module_urls)
466
        return urls
467

    
468
    def get_setting_key(self, name):
469
        return 'ASTAKOS_AUTH_PROVIDER_%s_%s' % (self.module.upper(),
470
                                                name.upper())
471

    
472
    def get_global_setting_key(self, name):
473
        return 'ASTAKOS_AUTH_PROVIDERS_%s' % name.upper()
474

    
475
    def has_global_setting(self, name):
476
        return hasattr(settings, self.get_global_setting_key(name))
477

    
478
    def has_setting(self, name):
479
        return hasattr(settings, self.get_setting_key(name))
480

    
481
    def get_setting(self, name, default=None):
482
        attr = self.get_setting_key(name)
483
        if not self.has_setting(name):
484
            return self.get_global_setting(name, default)
485
        return getattr(settings, attr, default)
486

    
487
    def get_global_setting(self, name, default=None):
488
        attr = self.get_global_setting_key(name)
489
        if not self.has_global_setting(name):
490
            return default
491
        return getattr(settings, attr, default)
492

    
493
    @property
494
    def provider_details(self):
495
        if self._provider_details:
496
            return self._provider_details
497

    
498
        self._provider_details = {}
499

    
500
        if self._instance:
501
            self._provider_details = self._instance.__dict__
502

    
503
        if self.user and self.identifier:
504
            if self.identifier:
505
                try:
506
                    self._provider_details = \
507
                        self.user.get_auth_providers().get(
508
                            module=self.module,
509
                            identifier=self.identifier).__dict__
510
                except Exception:
511
                    return {}
512
        return self._provider_details
513

    
514
    def __getattr__(self, key):
515
        if not key.startswith('get_'):
516
            return super(AuthProvider, self).__getattribute__(key)
517

    
518
        key = key.replace('get_', '')
519
        if key.endswith('_msg'):
520
            return self.get_message(key)
521

    
522
        if key.endswith('_policy'):
523
            return self.get_policy(key.replace('_policy', ''))
524

    
525
        if key.endswith('_url'):
526
            key = key.replace('_url', '')
527
            return self.urls.get(key)
528

    
529
        if key.endswith('_icon'):
530
            key = key.replace('_msg', '_icon')
531
            return settings.MEDIA_URL + self.get_message(key)
532

    
533
        if key.endswith('_setting'):
534
            key = key.replace('_setting', '')
535
            return self.get_message(key)
536

    
537
        if key.endswith('_template'):
538
            key = key.replace('_template', '')
539
            return self.get_template(key)
540

    
541
        return super(AuthProvider, self).__getattribute__(key)
542

    
543
    def is_active(self):
544
        return self.module_enabled
545

    
546
    @property
547
    def log_display(self):
548
        dsp = "%sAuth" % self.module.title()
549
        if self.user:
550
            dsp += "[%s]" % self.user.log_display
551
            if self.identifier:
552
                dsp += '[%s]' % self.identifier
553
                if self._instance and self._instance.pk:
554
                    dsp += '[%d]' % self._instance.pk
555
        return dsp
556

    
557
    def log(self, msg, *args, **kwargs):
558
        level = kwargs.pop('level', logging.INFO)
559
        message = '%s: %s' % (self.log_display, msg)
560
        logger.log(level, message, *args, **kwargs)
561

    
562

    
563
class LocalAuthProvider(AuthProvider):
564
    module = 'local'
565

    
566
    login_view = 'login'
567
    remote_authenticate = False
568
    username_key = 'user_email'
569

    
570
    messages = {
571
        'title': _('Classic'),
572
        'login_prompt': _('Classic login (username/password)'),
573
        'login_success': _('Logged in successfully.'),
574
        'method_details': 'Username: {username}',
575
        'logout_success_extra': ' '
576
    }
577

    
578
    policies = {
579
        'limit': 1,
580
        'switch': False
581
    }
582

    
583
    @property
584
    def urls(self):
585
        urls = super(LocalAuthProvider, self).urls
586

    
587
        password_change_url = None
588
        try:
589
            password_change_url = reverse('password_change')
590
        except NoReverseMatch:
591
            pass
592

    
593
        urls['change_password'] = password_change_url
594
        if self.user:
595
            urls['add'] = password_change_url
596
        if self._instance:
597
            urls.update({
598
                'remove': reverse('remove_auth_provider',
599
                                  kwargs={'pk': self._instance.pk})
600
            })
601
            if 'switch' in urls:
602
                del urls['switch']
603
        return urls
604

    
605
    def remove_from_user(self):
606
        super(LocalAuthProvider, self).remove_from_user()
607
        self.user.set_unusable_password()
608
        self.user.save()
609

    
610

    
611
class ShibbolethAuthProvider(AuthProvider):
612
    module = 'shibboleth'
613
    login_view = 'astakos.im.views.target.shibboleth.login'
614
    username_key = 'provider_info_eppn'
615

    
616
    policies = {
617
        'switch': False
618
    }
619

    
620
    messages = {
621
        'title': _('Academic'),
622
        'method_details': '{account_prompt}: {provider_info_eppn}',
623
        'login_description': _('If you are a student, professor or researcher'
624
                               ' you can login using your academic account.'),
625
        'add_prompt': _('Allows you to login using your Academic '
626
                        'account'),
627
        'method_details': 'Account: {username}',
628
        'logout_success_extra': _('You may still be logged in at your Academic'
629
                                  ' account though. Consider logging out '
630
                                  'from there too by closing all browser '
631
                                  'windows')
632
    }
633

    
634

    
635
class TwitterAuthProvider(AuthProvider):
636
    module = 'twitter'
637
    login_view = 'astakos.im.views.target.twitter.login'
638
    username_key = 'provider_info_screen_name'
639

    
640
    messages = {
641
        'title': _('Twitter'),
642
        'method_details': 'Screen name: {username}',
643
    }
644

    
645

    
646
class GoogleAuthProvider(AuthProvider):
647
    module = 'google'
648
    login_view = 'astakos.im.views.target.google.login'
649
    username_key = 'provider_info_email'
650

    
651
    messages = {
652
        'title': _('Google'),
653
        'method_details': 'Email: {username}',
654
    }
655

    
656

    
657
class LinkedInAuthProvider(AuthProvider):
658
    module = 'linkedin'
659
    login_view = 'astakos.im.views.target.linkedin.login'
660
    username_key = 'provider_info_email'
661

    
662
    messages = {
663
        'title': _('LinkedIn'),
664
        'method_details': 'Email: {username}',
665
    }
666

    
667

    
668
# Utility method
669
def get_provider(module, user_obj=None, identifier=None, **params):
670
    """
671
    Return a provider instance from the auth providers registry.
672
    """
673
    if not module in PROVIDERS:
674
        raise InvalidProvider('Invalid auth provider "%s"' % module)
675

    
676
    return PROVIDERS.get(module)(user_obj, identifier, **params)