Revision d2633501

b/snf-astakos-app/astakos/im/auth_providers.py
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

  
35
from django.core.urlresolvers import reverse
36
from django.utils.translation import ugettext as _
37

  
38
from astakos.im import settings
39

  
40
import logging
41

  
42
logger = logging.getLogger(__name__)
43

  
44
# providers registry
45
PROVIDERS = {}
46

  
47
class AuthProviderBase(type):
48

  
49
    def __new__(cls, name, bases, dct):
50
        include = False
51
        if [b for b in bases if isinstance(b, AuthProviderBase)]:
52
            type_id = dct.get('module')
53
            if type_id:
54
                include = True
55
            if type_id in settings.IM_MODULES:
56
                dct['module_enabled'] = True
57

  
58
        newcls = super(AuthProviderBase, cls).__new__(cls, name, bases, dct)
59
        if include:
60
            PROVIDERS[type_id] = newcls
61
        return newcls
62

  
63

  
64
class AuthProvider(object):
65

  
66
    __metaclass__ = AuthProviderBase
67

  
68
    module = None
69
    module_active = False
70
    module_enabled = False
71
    one_per_user = False
72

  
73
    def __init__(self, user=None):
74
        self.user = user
75

  
76
    def get_setting(self, name, default=None):
77
        attr = 'AUTH_PROVIDER_%s_%s' % (self.module.upper(), name.upper())
78
        return getattr(settings, attr, default)
79

  
80
    def is_available_for_login(self):
81
        """ A user can login using authentication provider"""
82
        return self.is_active() and self.get_setting('CAN_LOGIN',
83
                                                     self.is_active())
84

  
85
    def is_available_for_create(self):
86
        """ A user can create an account using this provider"""
87
        return self.is_active() and self.get_setting('CAN_CREATE',
88
                                                   self.is_active())
89

  
90
    def is_available_for_add(self):
91
        """ A user can assign provider authentication method"""
92
        return self.is_active() and self.get_setting('CAN_ADD',
93
                                                   self.is_active())
94

  
95
    def is_active(self):
96
        return self.module in settings.IM_MODULES
97

  
98

  
99
class LocalAuthProvider(AuthProvider):
100
    module = 'local'
101
    title = _('Local password')
102
    description = _('Create a local password for your account')
103

  
104

  
105
    @property
106
    def add_url(self):
107
        return reverse('password_change')
108

  
109
    add_description = _('Create a local password for your account')
110
    login_template = 'auth/local_login_form.html'
111
    add_template = 'auth/local_add_action.html'
112
    one_per_user = True
113
    details_tpl = _('You can login to your account using your'
114
                    ' %(auth_backend)s password.')
115

  
116
    @property
117
    def extra_actions(self):
118
        return [(_('Change password'), reverse('password_change')), ]
119

  
120

  
121
class ShibbolethAuthProvider(AuthProvider):
122
    module = 'shibboleth'
123
    title = _('Academic credentials (Shibboleth)')
124
    description = _('Allows you to login to your account using your academic '
125
                    'credentials')
126

  
127
    @property
128
    def add_url(self):
129
        return reverse('astakos.im.target.shibboleth.login')
130

  
131
    add_description = _('Allows you to login to your account using your academic '
132
                    'credentials')
133
    login_template = 'auth/shibboleth_login_form.html'
134
    add_template = 'auth/shibboleth_add_action.html'
135
    details_tpl = _('You can login to your account using your'
136
                    ' shibboleth credentials. Shibboleth id: %(identifier)s')
137

  
138

  
139
def get_provider(id, user_obj=None, default=None):
140
    """
141
    Return a provider instance from the auth providers registry.
142
    """
143
    return PROVIDERS.get(id, default)(user_obj)
144

  
b/snf-astakos-app/astakos/im/context_processors.py
36 36
        GLOBAL_MESSAGES, PROFILE_EXTRA_LINKS
37 37
from astakos.im.api.admin import get_menu
38 38
from astakos.im.util import get_query
39
from astakos.im.auth_providers import PROVIDERS as AUTH_PROVIDERS
39 40

  
40 41
from django.conf import settings
41 42
from django.core.urlresolvers import reverse
......
44 45
def im_modules(request):
45 46
    return {'im_modules': IM_MODULES}
46 47

  
48
def auth_providers(request):
49
    return {'auth_providers': filter(lambda p:p.module_enabled,
50
                                     AUTH_PROVIDERS.itervalues())}
51

  
47 52
def next(request):
48 53
    return {'next' : get_query(request).get('next', '')}
49 54

  
b/snf-astakos-app/astakos/im/forms.py
47 47
from django.contrib import messages
48 48
from django.utils.encoding import smart_str
49 49
from django.forms.models import fields_for_model
50
from django.db import transaction
50 51

  
51 52
from astakos.im.models import (
52 53
    AstakosUser, Invitation, get_latest_terms,
......
55 56
from astakos.im.settings import (INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL,
56 57
    BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL,
57 58
    RECAPTCHA_ENABLED, LOGGING_LEVEL, PASSWORD_RESET_EMAIL_SUBJECT,
58
    NEWPASSWD_INVALIDATE_TOKEN
59
    NEWPASSWD_INVALIDATE_TOKEN, MODERATION_ENABLED
59 60
)
61
from astakos.im import settings
60 62
from astakos.im.widgets import DummyWidget, RecaptchaWidget
61 63
from astakos.im.functions import send_change_email
62 64

  
......
70 72

  
71 73
logger = logging.getLogger(__name__)
72 74

  
73
class LocalUserCreationForm(UserCreationForm):
75
class StoreUserMixin(object):
76

  
77
    @transaction.commit_on_success
78
    def store_user(self, user, request):
79
        user.save()
80
        self.post_store_user(user, request)
81
        return user
82

  
83
    def post_store_user(self, user, request):
84
        """
85
        Interface method for descendant backends to be able to do stuff within
86
        the transaction enabled by store_user.
87
        """
88
        pass
89

  
90

  
91
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
74 92
    """
75 93
    Extends the built in UserCreationForm in several ways:
76 94

  
......
113 131
                    mark_safe("I agree with %s" % terms_link_html)
114 132

  
115 133
    def clean_email(self):
116
        email = self.cleaned_data['email']
134
        email = self.cleaned_data['email'].lower()
117 135
        if not email:
118 136
            raise forms.ValidationError(_("This field is required"))
119 137
        if reserved_email(email):
......
143 161
        if not check.is_valid:
144 162
            raise forms.ValidationError(_('You have not entered the correct words'))
145 163

  
164
    def post_store_user(self, user, request):
165
        """
166
        Interface method for descendant backends to be able to do stuff within
167
        the transaction enabled by store_user.
168
        """
169
        user.add_auth_provider('local', auth_backend='astakos')
170
        user.set_password(self.cleaned_data['password1'])
171

  
146 172
    def save(self, commit=True):
147 173
        """
148 174
        Saves the email, first_name and last_name properties, after the normal
149 175
        save behavior is complete.
150 176
        """
151 177
        user = super(LocalUserCreationForm, self).save(commit=False)
178
        user.renew_token()
152 179
        if commit:
153 180
            user.save()
154 181
            logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
......
176 203

  
177 204
    def save(self, commit=True):
178 205
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
179
        level = user.invitation.inviter.level + 1
180
        user.level = level
181
        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
206
        user.update_invitations_level()
182 207
        user.email_verified = True
183 208
        if commit:
184 209
            user.save()
185 210
        return user
186 211

  
187
class ThirdPartyUserCreationForm(forms.ModelForm):
212

  
213
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
188 214
    id = forms.CharField(
189 215
        widget=forms.HiddenInput(),
190 216
        label='',
......
224 250
                    mark_safe("I agree with %s" % terms_link_html)
225 251
    
226 252
    def clean_email(self):
227
        email = self.cleaned_data['email']
253
        email = self.cleaned_data['email'].lower()
228 254
        if not email:
229 255
            raise forms.ValidationError(_("This field is required"))
230 256
        return email
......
235 261
            raise forms.ValidationError(_('You have to agree with the terms'))
236 262
        return has_signed_terms
237 263

  
264
    def post_store_user(self, user, request):
265
        pending = PendingThirdPartyUser.objects.get(
266
                                token=request.POST.get('third_party_token'),
267
                                third_party_identifier= \
268
            self.cleaned_data.get('third_party_identifier'))
269
        return user.add_pending_auth_provider(pending)
270

  
271

  
238 272
    def save(self, commit=True):
239 273
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
240 274
        user.set_unusable_password()
241
        user.provider = get_query(self.request).get('provider')
275
        user.is_local = False
276
        user.renew_token()
242 277
        if commit:
243 278
            user.save()
244 279
            logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
......
261 296

  
262 297
    def save(self, commit=True):
263 298
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
264
        level = user.invitation.inviter.level + 1
265
        user.level = level
266
        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
299
        user.set_invitation_level()
267 300
        user.email_verified = True
268 301
        if commit:
269 302
            user.save()
......
279 312
        field = self.fields[name]
280 313
        self.initial['additional_email'] = self.initial.get(name, field.initial)
281 314
        self.initial['email'] = None
282
    
315

  
283 316
    def clean_email(self):
284
        email = self.cleaned_data['email']
317
        email = self.cleaned_data['email'].lower()
285 318
        if self.instance:
286 319
            if self.instance.email == email:
287 320
                raise forms.ValidationError(_("This is your current email."))
......
296 329
                raise forms.ValidationError(_("This email is already used"))
297 330
        super(ShibbolethUserCreationForm, self).clean_email()
298 331
        return email
299
    
300
    def save(self, commit=True):
301
        user = super(ShibbolethUserCreationForm, self).save(commit=False)
302
        try:
303
            p = PendingThirdPartyUser.objects.get(
304
                provider=user.provider,
305
                third_party_identifier=user.third_party_identifier
306
            )
307
        except:
308
            pass
309
        else:
310
            p.delete()
311
        return user
332

  
312 333

  
313 334
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
314 335
    pass
......
433 454
            user = AstakosUser.objects.get(email=email, is_active=True)
434 455
            if not user.has_usable_password():
435 456
                raise forms.ValidationError(_("This account has not a usable password."))
457

  
458
            if not user.can_change_password():
459
                raise forms.ValidationError(_('Password change for this account'
460
                                              ' is not supported.'))
461

  
436 462
        except AstakosUser.DoesNotExist, e:
437
            raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
463
            raise forms.ValidationError(_('That e-mail address doesn\'t have an'
464
                                          ' associated user account. Are you sure'
465
                                          ' you\'ve registered?'))
438 466
        return email
439 467

  
440 468
    def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
......
443 471
        Generates a one-use only link for resetting password and sends to the user.
444 472
        """
445 473
        for user in self.users_cache:
446
            url = reverse('django.contrib.auth.views.password_reset_confirm',
447
                          kwargs={'uidb36':int_to_base36(user.id),
448
                                  'token':token_generator.make_token(user)})
474
            url = user.astakosuser.get_password_reset_url(token_generator)
449 475
            url = urljoin(BASEURL, url)
450 476
            t = loader.get_template(email_template_name)
451 477
            c = {
......
468 494
    def clean_new_email_address(self):
469 495
        addr = self.cleaned_data['new_email_address']
470 496
        if AstakosUser.objects.filter(email__iexact=addr):
471
            raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
497
            raise forms.ValidationError(_(u"This email address is already "
498
                                           "in use. Please supply a "
499
                                           "different email address."))
472 500
        return addr
473 501

  
474 502
    def save(self, email_template_name, request, commit=True):
......
552 580
    
553 581
    def __init__(self, user, *args, **kwargs):
554 582
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
555
    
583

  
584
    @transaction.commit_on_success()
556 585
    def save(self, commit=True):
557 586
        try:
558 587
            self.user = AstakosUser.objects.get(id=self.user.id)
559 588
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
560 589
                self.user.renew_token()
561
            self.user.flush_sessions()
590
            #self.user.flush_sessions()
591
            if not self.user.has_auth_provider('local'):
592
                self.user.add_auth_provider('local', auth_backend='astakos')
593

  
562 594
        except BaseException, e:
563 595
            logger.exception(e)
564 596
            pass
b/snf-astakos-app/astakos/im/models.py
40 40
from datetime import datetime, timedelta
41 41
from base64 import b64encode
42 42
from urlparse import urlparse
43
from urllib import quote
43 44
from random import randint
44 45

  
45 46
from django.db import models, IntegrityError
......
51 52
from django.db import transaction
52 53
from django.db.models.signals import post_save, pre_save, post_syncdb
53 54
from django.db.models import Q
55
from django.core.urlresolvers import reverse
56
from django.utils.http import int_to_base36
57
from django.contrib.auth.tokens import default_token_generator
54 58
from django.conf import settings
55 59
from django.utils.importlib import import_module
60
from django.core.validators import email_re
56 61

  
57 62
from astakos.im.settings import (
58 63
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
59 64
    AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME,
60 65
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
61 66
)
67
from astakos.im import auth_providers
62 68

  
63 69
QUEUE_CLIENT_ID = 3 # Astakos.
64 70

  
65 71
logger = logging.getLogger(__name__)
66 72

  
73

  
74
class AstakosUserManager(models.Manager):
75

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

  
85

  
67 86
class AstakosUser(User):
68 87
    """
69 88
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
......
71 90
    # Use UserManager to get the create_user method, etc.
72 91
    objects = UserManager()
73 92

  
74
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
75
    provider = models.CharField('Provider', max_length=255, blank=True)
93
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
94
                                   null=True)
95

  
96
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
97
    #                    AstakosUserProvider model.
98
    provider = models.CharField('Provider', max_length=255, blank=True,
99
                                null=True)
100
    # ex. screen_name for twitter, eppn for shibboleth
101
    third_party_identifier = models.CharField('Third-party identifier',
102
                                              max_length=255, null=True,
103
                                              blank=True)
104

  
76 105

  
77 106
    #for invitations
78 107
    user_level = DEFAULT_USER_LEVEL
......
87 116
    updated = models.DateTimeField('Update date')
88 117
    is_verified = models.BooleanField('Is verified?', default=False)
89 118

  
90
    # ex. screen_name for twitter, eppn for shibboleth
91
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
92

  
93 119
    email_verified = models.BooleanField('Email verified?', default=False)
94 120

  
95 121
    has_credits = models.BooleanField('Has credits?', default=False)
......
100 126
    
101 127
    __has_signed_terms = False
102 128
    __groupnames = []
103
    
104
    class Meta:
105
        unique_together = ("provider", "third_party_identifier")
106
    
129

  
130
    objects = AstakosUserManager()
131

  
107 132
    def __init__(self, *args, **kwargs):
108 133
        super(AstakosUser, self).__init__(*args, **kwargs)
109 134
        self.__has_signed_terms = self.has_signed_terms
......
145 170
        if not self.id:
146 171
            # set username
147 172
            while not self.username:
148
                username =  uuid.uuid4().hex[:30]
173
                username =  self.email
149 174
                try:
150 175
                    AstakosUser.objects.get(username = username)
151 176
                except AstakosUser.DoesNotExist, e:
152 177
                    self.username = username
153
            if not self.provider:
154
                self.provider = 'local'
178

  
155 179
        report_user_event(self)
156 180
        self.validate_unique_email_isactive()
157 181
        if self.is_active and self.activation_sent:
......
236 260
            return False
237 261
        return True
238 262

  
263
    def set_invitations_level(self):
264
        """
265
        Update user invitation level
266
        """
267
        level = self.invitation.inviter.level + 1
268
        self.level = level
269
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
270

  
271
    def can_login_with_auth_provider(self, provider):
272
        if not self.has_auth_provider(provider):
273
            return False
274
        else:
275
            return auth_providers.get_provider(provider).is_available_for_login()
276

  
277
    def can_add_provider(self, provider, **kwargs):
278
        provider_settings = auth_providers.get_provider(provider)
279
        if not provider_settings.is_available_for_login():
280
            return False
281
        if self.has_auth_provider(provider) and \
282
           provider_settings.one_per_user:
283
            return False
284
        return True
285

  
286
    def can_remove_auth_provider(self, provider):
287
        if len(self.get_active_auth_providers()) <= 1:
288
            return False
289
        return True
290

  
291
    def can_change_password(self):
292
        return self.has_auth_provider('local', auth_backend='astakos')
293

  
294
    def has_auth_provider(self, provider, **kwargs):
295
        return bool(self.auth_providers.filter(module=provider,
296
                                               **kwargs).count())
297

  
298
    def add_auth_provider(self, provider, **kwargs):
299
        self.auth_providers.create(module=provider, active=True, **kwargs)
300

  
301
    def add_pending_auth_provider(self, pending):
302
        """
303
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
304
        the current user.
305
        """
306
        if not isinstance(pending, PendingThirdPartyUser):
307
            pending = PendingThirdPartyUser.objects.get(token=pending)
308

  
309
        provider = self.add_auth_provider(pending.provider,
310
                               identifier=pending.third_party_identifier)
311

  
312
        if email_re.match(pending.email) and pending.email != self.email:
313
            self.additionalmail_set.get_or_create(email=pending.email)
314

  
315
        pending.delete()
316
        return provider
317

  
318
    def remove_auth_provider(self, provider, **kwargs):
319
        self.auth_providers.get(module=provider, **kwargs).delete()
320

  
321
    # user urls
322
    def get_resend_activation_url(self):
323
        return reverse('send_activation', {'user_id': self.pk})
324

  
325
    def get_activation_url(self, nxt=False):
326
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
327
                                 quote(self.auth_token))
328
        if nxt:
329
            url += "&next=%s" % quote(nxt)
330
        return url
331

  
332
    def get_password_reset_url(self, token_generator=default_token_generator):
333
        return reverse('django.contrib.auth.views.password_reset_confirm',
334
                          kwargs={'uidb36':int_to_base36(self.id),
335
                                  'token':token_generator.make_token(self)})
336

  
337
    def get_auth_providers(self):
338
        return self.auth_providers.all()
339

  
340
    def get_available_auth_providers(self):
341
        """
342
        Returns a list of providers available for user to connect to.
343
        """
344
        providers = []
345
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
346
            if self.can_add_provider(module):
347
                providers.append(provider_settings(self))
348

  
349
        return providers
350

  
351
    def get_active_auth_providers(self):
352
        providers = []
353
        for provider in self.auth_providers.active():
354
            if auth_providers.get_provider(provider.module).is_available_for_login():
355
                providers.append(provider)
356
        return providers
357

  
358

  
359
class AstakosUserAuthProviderManager(models.Manager):
360

  
361
    def active(self):
362
        return self.filter(active=True)
363

  
364

  
365
class AstakosUserAuthProvider(models.Model):
366
    """
367
    Available user authentication methods.
368
    """
369
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
370
                                   null=True, default=None)
371
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
372
    module = models.CharField('Provider', max_length=255, blank=False,
373
                                default='local')
374
    identifier = models.CharField('Third-party identifier',
375
                                              max_length=255, null=True,
376
                                              blank=True)
377
    active = models.BooleanField(default=True)
378
    auth_backend = models.CharField('Backend', max_length=255, blank=False,
379
                                   default='astakos')
380

  
381
    objects = AstakosUserAuthProviderManager()
382

  
383
    class Meta:
384
        unique_together = (('identifier', 'module', 'user'), )
385

  
386
    @property
387
    def settings(self):
388
        return auth_providers.get_provider(self.module)
389

  
390
    @property
391
    def details_display(self):
392
        print self.settings.details_tpl
393
        return self.settings.details_tpl % self.__dict__
394

  
395
    def can_remove(self):
396
        return self.user.can_remove_auth_provider(self.module)
397

  
398
    def delete(self, *args, **kwargs):
399
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
400
        self.user.set_unusable_password()
401
        self.user.save()
402
        return ret
403

  
404

  
239 405
class ApprovalTerms(models.Model):
240 406
    """
241 407
    Model for approval terms
......
407 573
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
408 574
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
409 575
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
410
    
576
    token = models.CharField('Token', max_length=255, null=True, blank=True)
577
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
578

  
411 579
    class Meta:
412 580
        unique_together = ("provider", "third_party_identifier")
413 581

  
......
435 603
                    self.username = username
436 604
        super(PendingThirdPartyUser, self).save(**kwargs)
437 605

  
606
    def generate_token(self):
607
        self.password = self.third_party_identifier
608
        self.last_login = datetime.now()
609
        self.token = default_token_generator.make_token(self)
610

  
438 611
class SessionCatalog(models.Model):
439 612
    session_key = models.CharField(_('session key'), max_length=40)
440 613
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
b/snf-astakos-app/astakos/im/synnefo_settings.py
51 51
    'django.core.context_processors.csrf',
52 52
    'astakos.im.context_processors.media',
53 53
    'astakos.im.context_processors.im_modules',
54
    'astakos.im.context_processors.auth_providers',
54 55
    'astakos.im.context_processors.next',
55 56
    'astakos.im.context_processors.code',
56 57
    'astakos.im.context_processors.invitations',
b/snf-astakos-app/astakos/im/target/local.py
43 43
from django.contrib.auth.decorators import login_required
44 44

  
45 45
from astakos.im.util import prepare_response, get_query
46
from astakos.im.views import requires_anonymous, signed_terms_required
46
from astakos.im.views import requires_anonymous, signed_terms_required, \
47
        requires_auth_provider
47 48
from astakos.im.models import AstakosUser, PendingThirdPartyUser
48
from astakos.im.forms import LoginForm, ExtendedPasswordChangeForm
49
from astakos.im.settings import RATELIMIT_RETRIES_ALLOWED
50
from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION
49
from astakos.im.forms import LoginForm, ExtendedPasswordChangeForm, \
50
        ExtendedSetPasswordForm
51
from astakos.im.settings import (RATELIMIT_RETRIES_ALLOWED,
52
                                ENABLE_LOCAL_ACCOUNT_MIGRATION)
53
from astakos.im import settings
51 54

  
52 55
from ratelimit.decorators import ratelimit
53 56

  
54 57
retries = RATELIMIT_RETRIES_ALLOWED-1
55 58
rate = str(retries)+'/m'
56 59

  
60
@requires_auth_provider('local', login=True)
57 61
@require_http_methods(["GET", "POST"])
58 62
@csrf_exempt
59 63
@requires_anonymous
......
65 69
    was_limited = getattr(request, 'limited', False)
66 70
    form = LoginForm(data=request.POST, was_limited=was_limited, request=request)
67 71
    next = get_query(request).get('next', '')
68
    username = get_query(request).get('key')
69
    
72
    third_party_token = get_query(request).get('key', False)
73

  
70 74
    if not form.is_valid():
71 75
        return render_to_response(
72 76
            on_failure,
73 77
            {'login_form':form,
74 78
             'next':next,
75
             'key':username},
79
             'key': third_party_token},
76 80
            context_instance=RequestContext(request)
77 81
        )
78 82
    # get the user from the cash
......
86 90
            message = _('Your request is pending activation')
87 91
        else:
88 92
            url = reverse('send_activation', kwargs={'user_id':user.id})
89
            message = _('You have not followed the activation link. \
90
            <a href="%s">Resend activation email?</a>' % url)
91
    elif user.provider not in ('local', ''):
93
            msg = _('You have not followed the activation link.')
94
            if settings.MODERATION_ENABLED:
95
                msg_extra = ' ' + _('Please contact support.')
96
            else:
97
                msg_extra = _('<a href="%s">Resend activation email?</a>') % url
98

  
99
            message = msg + msg_extra
100
    elif not user.can_login_with_auth_provider('local'):
92 101
        message = _(
93 102
            'Local login is not the current authentication method for this account.'
94 103
        )
95
    
104

  
96 105
    if message:
97 106
        messages.error(request, message)
98 107
        return render_to_response(on_failure,
99
                                  {'login_form':form},
108
                                  {'login_form': form},
100 109
                                  context_instance=RequestContext(request))
101
    
102
    # hook for switching account to use third party authentication
103
    if ENABLE_LOCAL_ACCOUNT_MIGRATION and username:
110

  
111
    response = prepare_response(request, user, next)
112
    if third_party_token:
113
        # use requests to assign the account he just authenticated with with
114
        # a third party provider account
104 115
        try:
105
            new = PendingThirdPartyUser.objects.get(
106
                username=username)
107
        except:
108
            messages.error(
109
                request,
110
                _('Account failed to switch to %(provider)s' % locals())
111
            )
112
            return render_to_response(
113
                on_failure,
114
                {'login_form':form,
115
                 'next':next},
116
                context_instance=RequestContext(request)
117
            )
118
        else:
119
            user.provider = new.provider
120
            user.third_party_identifier = new.third_party_identifier
121
            user.save()
122
            new.delete()
123
            messages.success(
124
                request,
125
                _('Account successfully switched to %(provider)s' % user.__dict__)
126
            )
127
    return prepare_response(request, user, next)
116
          request.user.add_pending_auth_provider(third_party_token)
117
          messages.success(request, _('Your new login method has been added'))
118
        except PendingThirdPartyUser.DoesNotExist:
119
          messages.error(request, _('Account method assignment failed'))
120

  
121
    return response
128 122

  
129 123
@require_http_methods(["GET", "POST"])
130 124
@signed_terms_required
131 125
@login_required
126
@requires_auth_provider('local', login=True)
132 127
def password_change(request, template_name='registration/password_change_form.html',
133 128
                    post_change_redirect=None, password_change_form=ExtendedPasswordChangeForm):
129

  
130
    create_password = False
131

  
132
    # no local backend user wants to create a password
133
    if not request.user.has_auth_provider('local'):
134
        create_password = True
135
        password_change_form = ExtendedSetPasswordForm
136

  
134 137
    if post_change_redirect is None:
135
        post_change_redirect = reverse('django.contrib.auth.views.password_change_done')
138
        post_change_redirect = reverse('edit_profile')
139

  
136 140
    if request.method == "POST":
137
        form = password_change_form(
141
        form_kwargs = dict(
138 142
            user=request.user,
139 143
            data=request.POST,
140
            session_key=request.session.session_key
141 144
        )
145
        if not create_password:
146
            form_kwargs['session_key'] = session_key=request.session.session_key
147

  
148
        form = password_change_form(**form_kwargs)
142 149
        if form.is_valid():
143 150
            form.save()
144 151
            return HttpResponseRedirect(post_change_redirect)
b/snf-astakos-app/astakos/im/target/shibboleth.py
40 40
from django.core.exceptions import ValidationError
41 41
from django.http import HttpResponseRedirect
42 42
from django.core.urlresolvers import reverse
43
from urlparse import urlunsplit, urlsplit
44 43
from django.utils.http import urlencode
44
from django.shortcuts import get_object_or_404
45

  
46
from urlparse import urlunsplit, urlsplit
45 47

  
46 48
from astakos.im.util import prepare_response, get_context, get_invitation
47
from astakos.im.views import requires_anonymous, render_response
49
from astakos.im.views import requires_anonymous, render_response, \
50
        requires_auth_provider
48 51
from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
49 52

  
50 53
from astakos.im.models import AstakosUser, PendingThirdPartyUser
51 54
from astakos.im.forms import LoginForm
52 55
from astakos.im.activation_backends import get_backend, SimpleBackend
56
from astakos.im import settings
53 57

  
54 58
import logging
55 59

  
......
66 70
    SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID"
67 71
    SHIB_MAIL = "HTTP_SHIB_MAIL"
68 72

  
73
@requires_auth_provider('local', login=True)
69 74
@require_http_methods(["GET", "POST"])
70
@requires_anonymous
71 75
def login(
72 76
    request,
73
    login_template='im/login.html',
74
    signup_template='im/third_party_check_local.html',
77
    template='im/third_party_check_local.html',
75 78
    extra_context=None
76 79
):
77 80
    extra_context = extra_context or {}
78 81

  
79 82
    tokens = request.META
80
    
83

  
81 84
    try:
82 85
        eppn = tokens.get(Tokens.SHIB_EPPN)
83 86
        if not eppn:
84
            raise KeyError(_('Missing unique token in request'))
87
            raise KeyError(_('Missing provider token'))
85 88
        if Tokens.SHIB_DISPLAYNAME in tokens:
86 89
            realname = tokens[Tokens.SHIB_DISPLAYNAME]
87 90
        elif Tokens.SHIB_CN in tokens:
......
89 92
        elif Tokens.SHIB_NAME in tokens and Tokens.SHIB_SURNAME in tokens:
90 93
            realname = tokens[Tokens.SHIB_NAME] + ' ' + tokens[Tokens.SHIB_SURNAME]
91 94
        else:
92
            raise KeyError(_('Missing user name in request'))
95
            raise KeyError(_('Missing provider user information'))
93 96
    except KeyError, e:
94
        extra_context['login_form'] = LoginForm(request=request)
97
        # invalid shibboleth headers, redirect to login, display message
95 98
        messages.error(request, e)
96
        return render_response(
97
            login_template,
98
            context_instance=get_context(request, extra_context)
99
        )
100
    
99
        return HttpResponseRedirect(reverse('login'))
100

  
101 101
    affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '')
102 102
    email = tokens.get(Tokens.SHIB_MAIL, '')
103
    
103

  
104
    # an existing user accessed the view
105
    if request.user.is_authenticated():
106
        if request.user.has_auth_provider('shibboleth', identifier=eppn):
107
            return HttpResponseRedirect(reverse('edit_profile'))
108

  
109
        # automatically add eppn provider to user
110
        user = request.user
111
        user.add_provider('shibboleth', identifier=eppn)
112
        return HttpResponseRedirect('edit_profile')
113

  
104 114
    try:
105
        user = AstakosUser.objects.get(
106
            provider='shibboleth',
107
            third_party_identifier=eppn
115
        # astakos user exists ?
116
        user = AstakosUser.objects.get_auth_provider_user(
117
            'shibboleth',
118
            identifier=eppn
108 119
        )
109 120
        if user.is_active:
121
            # authenticate user
110 122
            return prepare_response(request,
111 123
                                    user,
112 124
                                    request.GET.get('next'),
113 125
                                    'renew' in request.GET)
114 126
        elif not user.activation_sent:
115 127
            message = _('Your request is pending activation')
128
            if not settings.MODERATION_ENABLED:
129
                url = user.get_resend_activation_url()
130
                msg_extra = _('<a href="%s">Resend activation email?</a>') % url
131
                message = message + u' ' + msg_extra
132

  
116 133
            messages.error(request, message)
134
            return HttpResponseRedirect(reverse('login'))
135

  
117 136
        else:
118
            urls = {}
119
            urls['send_activation'] = reverse(
120
                'send_activation',
121
                kwargs={'user_id':user.id}
122
            )
123
            urls['signup'] = reverse(
124
                'shibboleth_signup',
125
                args= [user.username]
126
            )   
127
            message = _(
128
                'You have not followed the activation link. \
129
                <a href="%(send_activation)s">Resend activation email?</a> or \
130
                <a href="%(signup)s">Provide new email?</a>' % urls
131
            )
137
            message = _(u'Account disabled. Please contact support')
132 138
            messages.error(request, message)
133
        return render_response(login_template,
134
                               login_form = LoginForm(request=request),
135
                               context_instance=RequestContext(request))
139
            return HttpResponseRedirect(reverse('login'))
140

  
136 141
    except AstakosUser.DoesNotExist, e:
137
        # First time
138
        try:
139
            user, created = PendingThirdPartyUser.objects.get_or_create(
140
                third_party_identifier=eppn,
141
                provider='shibboleth',
142
                defaults=dict(
143
                    realname=realname,
144
                    affiliation=affiliation,
145
                    email=email
146
                )
147
            )
148
            user.save()
149
        except BaseException, e:
150
            logger.exception(e)
151
            template = login_template
152
            extra_context['login_form'] = LoginForm(request=request)
153
            messages.error(request, _('Something went wrong.'))
154
        else:
155
            if not ENABLE_LOCAL_ACCOUNT_MIGRATION:
156
                url = reverse(
157
                    'shibboleth_signup',
158
                    args= [user.username]
159
                )
160
                return HttpResponseRedirect(url)
161
            else:
162
                template = signup_template
163
                extra_context['username'] = user.username
164
        
165
        extra_context['provider']='shibboleth'
142
        # eppn not stored in astakos models, create pending profile
143
        user, created = PendingThirdPartyUser.objects.get_or_create(
144
            third_party_identifier=eppn,
145
            provider='shibboleth',
146
        )
147
        # update pending user
148
        user.realname = realname
149
        user.affiliation = affiliation
150
        user.email = email
151
        user.generate_token()
152
        user.save()
153

  
154
        extra_context['provider'] = 'shibboleth'
155
        extra_context['token'] = user.token
156

  
166 157
        return render_response(
167 158
            template,
168 159
            context_instance=get_context(request, extra_context)
169 160
        )
170 161

  
162

  
163
@requires_auth_provider('local', login=True, create=True)
171 164
@require_http_methods(["GET"])
172 165
@requires_anonymous
173 166
def signup(
174 167
    request,
175
    username,
168
    token,
176 169
    backend=None,
177 170
    on_creation_template='im/third_party_registration.html',
178
    extra_context=None
179
):
171
    extra_context=None):
172

  
180 173
    extra_context = extra_context or {}
181
    if not username:
174
    if not token:
182 175
        return HttpResponseBadRequest(_('Missing key parameter.'))
183
    try:
184
        pending = PendingThirdPartyUser.objects.get(username=username)
185
    except PendingThirdPartyUser.DoesNotExist:
186
        try:
187
            user = AstakosUser.objects.get(username=username)
188
        except AstakosUser.DoesNotExist:
189
            return HttpResponseBadRequest(_('Invalid key.'))
190
    else:
191
        d = pending.__dict__
192
        d.pop('_state', None)
193
        d.pop('id', None)
194
        user = AstakosUser(**d)
176

  
177
    pending = get_object_or_404(PendingThirdPartyUser, token=token)
178
    d = pending.__dict__
179
    d.pop('_state', None)
180
    d.pop('id', None)
181
    d.pop('token', None)
182
    d.pop('created', None)
183
    user = AstakosUser(**d)
184

  
195 185
    try:
196 186
        backend = backend or get_backend(request)
197 187
    except ImproperlyConfigured, e:
......
201 191
            provider='shibboleth',
202 192
            instance=user
203 193
        )
204
    extra_context['provider']='shibboleth'
194

  
195
    extra_context['provider'] = 'shibboleth'
196
    extra_context['third_party_token'] = token
205 197
    return render_response(
206 198
            on_creation_template,
207 199
            context_instance=get_context(request, extra_context)
208
    )
200
    )
201

  
b/snf-astakos-app/astakos/im/templates/im/profile.html
14 14
        <input type="submit" class="submit altcol" value="UPDATE" />
15 15
    </div>
16 16

  
17
    <div class="auth_methods">
18
      <br /><br />
19
        <div class="assigned">
20
          <h4>Authentication methods</h4>
21
          <p>You can login to your account using the following methods</p>
22
          <ul class="auth_providers">
23
            {% for provider in user_providers %}
24
            <li>
25
            <h2>
26
                {{ provider.settings.title }}
27
                <span class="actions" style="margin-left: 40px">
28
                  {% for name, url in provider.settings.extra_actions %}
29
                  <a href="{{ url }}" title="{{ name }}">{{ name }}</a>
30
                  {% endfor %}
31
                  {% if provider.can_remove %}
32
                      <a href="{% url remove_auth_provider provider.pk %}" title="disble">Remove</a>
33
                  {% endif %}
34
                </span>
35
            </h2>
36
            <p>{{ provider.details_display }}</p>
37
            <br />
38
            </li>
39
            {% empty %}
40
            <li>No available authentication methods</li>
41
            {% endfor %}
42
          </ul>
43
        </div>
44
        <div class="notassigned">
45
          <p>You can add the following authentication methods to your account </p>
46
          <ul class="auth_providers">
47
            {% for provider in user_available_providers %}
48
            <li>
49
            <h2><a href="{{ provider.add_url }}">{{ provider.title }}</a></h2>
50
            <p>{{ provider.add_description }}</p>
51
            <br />
52
            </li>
53
            {% empty %}
54
            No available providers.
55
            {% endfor %}
56
          </ul>
57
        </div>
58
    </div>
59

  
17 60
</form>
18 61
{% endblock body %}
b/snf-astakos-app/astakos/im/templates/im/signup.html
18 18
            <input type="hidden" name="next" value="{{ next }}">
19 19
            <input type="hidden" name="code" value="{{ code }}">
20 20
            <input type="hidden" name="provider" value={{ provider|default:"local" }}>
21
            {% if third_party_token %}
22
            <input type="hidden" name="third_party_token" value={{ third_party_token }}>
23
            {% endif %}
21 24
            {% with signup_form as form %}
22 25
            {% include "im/form_render.html" %}
23 26
            {% endwith %}
b/snf-astakos-app/astakos/im/templates/im/third_party_check_local.html
13 13
    {% if "local" in im_modules %}
14 14
      <div class="form-stacked">
15 15
        <h2><span>Already have an account?</span></h2>
16
        <a href="{% url astakos.im.views.index %}?key={{username}}">YES</a>
17
        <a href="{% url shibboleth_signup username %}">NO</a>
16
        <a href="{% url astakos.im.views.index %}?key={{ token }}">YES</a>
17
        <a href="{% url shibboleth_signup token %}">NO</a>
18 18
      </div>
19 19
    {% endif %}
20
{% endblock %}
20
{% endblock %}
b/snf-astakos-app/astakos/im/templates/im/third_party_registration.html
21 21
            <input type="hidden" name="next" value="{{ next }}">
22 22
            <input type="hidden" name="code" value="{{ code }}">
23 23
            <input type="hidden" name="provider" value={{ provider|default:"local" }}>
24
            <input type="hidden" name="third_party_token" value={{ third_party_token }}>
25

  
24 26
            {% include "im/form_render.html" %}
25 27
            <div class="form-row submit">
26 28
                <input type="submit" class="submit altcol" value="SUBMIT" />
b/snf-astakos-app/astakos/im/tests.py
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 datetime
35

  
36
from django.test import TestCase, Client
37
from django.conf import settings
38
from django.core import mail
39

  
40
from astakos.im.target.shibboleth import Tokens as ShibbolethTokens
41
from astakos.im.models import *
42
from astakos.im import functions
43
from astakos.im import settings as astakos_settings
44

  
45
from urllib import quote
46

  
47
class ShibbolethClient(Client):
48
    """
49
    A shibboleth agnostic client.
50
    """
51
    VALID_TOKENS = filter(lambda x: not x.startswith("_"), dir(ShibbolethTokens))
52

  
53
    def __init__(self, *args, **kwargs):
54
        self.tokens = kwargs.pop('tokens', {})
55
        super(ShibbolethClient, self).__init__(*args, **kwargs)
56

  
57
    def set_tokens(self, **kwargs):
58
        for key, value in kwargs.iteritems():
59
            key = 'SHIB_%s' % key.upper()
60
            if not key in self.VALID_TOKENS:
61
                raise Exception('Invalid shibboleth token')
62

  
63
            self.tokens[key] = value
64

  
65
    def unset_tokens(self, *keys):
66
        for key in keys:
67
            key = 'SHIB_%s' % param.upper()
68
            if key in self.tokens:
69
                del self.tokens[key]
70

  
71
    def reset_tokens(self):
72
        self.tokens = {}
73

  
74
    def get_http_token(self, key):
75
        http_header = getattr(ShibbolethTokens, key)
76
        return http_header
77

  
78
    def request(self, **request):
79
        """
80
        Transform valid shibboleth tokens to http headers
81
        """
82
        for token, value in self.tokens.iteritems():
83
            request[self.get_http_token(token)] = value
84

  
85
        for param in request.keys():
86
            key = 'SHIB_%s' % param.upper()
87
            if key in self.VALID_TOKENS:
88
                request[self.get_http_token(key)] = request[param]
89
                del request[param]
90

  
91
        return super(ShibbolethClient, self).request(**request)
92

  
93

  
94
def get_local_user(username, **kwargs):
95
        try:
96
            return AstakosUser.objects.get(email=username)
97
        except:
98
            user_params = {
99
                'username': username,
100
                'email': username,
101
                'is_active': True,
102
                'activation_sent': datetime.now(),
103
                'email_verified': True,
104
                'provider': 'local'
105
            }
106
            user_params.update(kwargs)
107
            user = AstakosUser(**user_params)
108
            user.set_password(kwargs.get('password', 'password'))
109
            user.save()
110
            user.add_auth_provider('local', auth_backend='astakos')
111
            if kwargs.get('is_active', True):
112
                user.is_active = True
113
            else:
114
                user.is_active = False
115
            user.save()
116
            return user
117

  
118

  
119
def get_mailbox(email):
120
    mails = []
121
    for sent_email in mail.outbox:
122
        for recipient in sent_email.recipients():
123
            if email in recipient:
124
                mails.append(sent_email)
125
    return mails
126

  
127

  
128
class ShibbolethTests(TestCase):
129
    """
130
    Testing shibboleth authentication.
131
    """
132

  
133
    fixtures = ['groups']
134

  
135
    def setUp(self):
136
        self.client = ShibbolethClient()
137
        settings.ASTAKOS_IM_MODULES = ['local', 'shibboleth']
138

  
139
    def test_create_account(self):
140
        client = ShibbolethClient()
141

  
142
        # shibboleth views validation
143
        # eepn required
144
        r = client.get('/im/login/shibboleth?', follow=True)
145
        self.assertContains(r, 'Missing provider token')
146
        client.set_tokens(eppn="kpapeppn")
147
        # shibboleth user info required
148
        r = client.get('/im/login/shibboleth?', follow=True)
149
        self.assertContains(r, 'Missing provider user information')
150

  
151
        # shibboleth logged us in
152
        client.set_tokens(mail="kpap@grnet.gr", eppn="kpapeppn", cn="1", )
153
        r = client.get('/im/login/shibboleth?')
154

  
155
        # astakos asks if we want to add shibboleth
156
        self.assertContains(r, "Already have an account?")
157

  
158
        # a new pending user created
159
        pending_user = PendingThirdPartyUser.objects.get(
160
            third_party_identifier="kpapeppn")
161
        self.assertEqual(PendingThirdPartyUser.objects.count(), 1)
162
        token = pending_user.token
163
        # from now on no shibboleth headers are sent to the server
164
        client.reset_tokens()
165

  
166
        # we choose to signup as a new user
167
        r = client.get('/im/shibboleth/signup/%s' % pending_user.username)
168
        self.assertEqual(r.status_code, 404)
169

  
170
        r = client.get('/im/shibboleth/signup/%s' % token)
171
        form = r.context['form']
172
        post_data = {'email': 'kpap@grnet.gr',
173
                     'third_party_identifier': pending_user.third_party_identifier,
174
                     'first_name': 'Kostas',
175
                     'third_party_token': token,
176
                     'last_name': 'Mitroglou',
177
                     'additional_email': 'kpap@grnet.gr',
178
                     'provider': 'shibboleth'
179
                    }
180
        r = client.post('/im/signup', post_data)
181
        self.assertEqual(r.status_code, 200)
182
        self.assertEqual(AstakosUser.objects.count(), 1)
183
        self.assertEqual(PendingThirdPartyUser.objects.count(), 0)
184
        self.assertEqual(AstakosUserAuthProvider.objects.count(), 1)
185

  
186

  
187
        client.set_tokens(mail="kpap@grnet.gr", eppn="kpapeppn", cn="1", )
188
        r = client.get("/im/login/shibboleth?", follow=True)
189
        self.assertContains(r, "Your request is pending activation")
190
        r = client.get("/im/profile", follow=True)
191
        self.assertRedirects(r, 'http://testserver/im/?next=%2Fim%2Fprofile')
192

  
193
        u = AstakosUser.objects.get()
194
        functions.activate(u)
195
        self.assertEqual(u.is_active, True)
196

  
197
        r = client.get("/im/login/shibboleth?")
198
        self.assertRedirects(r, '/im/profile')
199

  
200
    def test_existing(self):
201
        existing_user = get_local_user('kpap@grnet.gr')
202

  
203
        client = ShibbolethClient()
204
        # shibboleth logged us in, notice that we use different email
205
        client.set_tokens(mail="kpap@shibboleth.gr", eppn="kpapeppn", cn="1", )
206
        r = client.get("/im/login/shibboleth?")
207
        # astakos asks if we want to switch a local account to shibboleth
208
        self.assertContains(r, "Already have an account?")
209

  
210
        # a new pending user created
211
        pending_user = PendingThirdPartyUser.objects.get()
212
        self.assertEqual(PendingThirdPartyUser.objects.count(), 1)
213
        pending_key = pending_user.token
214
        client.reset_tokens()
215

  
216
        # we choose to add shibboleth to an our existing account
217
        # we get redirected to login page with the pending token set
218
        r = client.get('/im/login?key=%s' % pending_key)
219
        post_data = {'password': 'password',
220
                     'username': 'kpap@grnet.gr',
221
                     'key': pending_key}
222
        r = client.post('/im/local', post_data, follow=True)
223
        self.assertContains(r, "Your new login method has been added")
224

  
225
        user = AstakosUser.objects.get(username="kpap@grnet.gr",
226
                                       email="kpap@grnet.gr")
227
        self.assertTrue(user.has_auth_provider('shibboleth'))
228
        self.assertTrue(user.has_auth_provider('local', auth_backend='astakos'))
229
        client.logout()
230

  
231
        # again ???? show her a message
232
        r = client.get('/im/login?key=%s' % pending_key)
233
        post_data = {'password': 'password',
234
                     'username': 'kpap@grnet.gr',
235
                     'key': pending_key}
236
        r = self.client.post('/im/local', post_data, follow=True)
237
        self.assertContains(r, "Account method assignment failed")
238
        self.client.logout()
239
        client.logout()
240

  
241
        # look Ma, i can login with both my shibboleth and local account
242
        client.set_tokens(mail="kpap@shibboleth.gr", eppn="kpapeppn", cn="1")
243
        r = client.get("/im/login/shibboleth?", follow=True)
244
        self.assertTrue(r.context['request'].user.is_authenticated())
245
        self.assertTrue(r.context['request'].user.email == "kpap@grnet.gr")
246
        r = client.get("/im/profile")
247
        self.assertEquals(r.status_code,200)
248
        client.logout()
249
        client.reset_tokens()
250
        r = client.get("/im/profile", follow=True)
251
        self.assertFalse(r.context['request'].user.is_authenticated())
252

  
253
        post_data = {'password': 'password',
254
                     'username': 'kpap@grnet.gr'}
255
        r = self.client.post('/im/local', post_data, follow=True)
256
        self.assertTrue(r.context['request'].user.is_authenticated())
257
        r = self.client.get("/im/profile")
258
        self.assertEquals(r.status_code,200)
259

  
260
        r = client.post('/im/local', post_data, follow=True)
261
        client.set_tokens(mail="secondary@shibboleth.gr", eppn="kpapeppn", cn="1", )
262
        r = client.get("/im/login/shibboleth?", follow=True)
263
        client.reset_tokens()
264

  
265
        client.logout()
266
        client.set_tokens(mail="kpap@grnet.gr", eppn="kpapeppninvalid", cn="1")
267
        r = client.get("/im/login/shibboleth?", follow=True)
268
        self.assertFalse(r.context['request'].user.is_authenticated())
269

  
270

  
271
class LocalUserTests(TestCase):
272

  
273
    fixtures = ['groups']
274

  
275
    def test_invitations(self):
276
        return
277

  
278
    def test_local_provider(self):
279
        r = self.client.get("/im/signup")
280
        self.assertEqual(r.status_code, 200)
281

  
282
        data = {'email':'kpap@grnet.gr', 'password1':'password',
283
                'password2':'password', 'first_name': 'Kostas',
284
                'last_name': 'Mitroglou', 'provider': 'local'}
285
        r = self.client.post("/im/signup", data)
286
        self.assertEqual(AstakosUser.objects.count(), 1)
287
        user = AstakosUser.objects.get(username="kpap@grnet.gr",
288
                                       email="kpap@grnet.gr")
289
        self.assertEqual(user.username, 'kpap@grnet.gr')
290
        self.assertEqual(user.has_auth_provider('local'), True)
291
        self.assertFalse(user.is_active)
292

  
293
        # admin gets notified
294
        self.assertEqual(len(get_mailbox('support@cloud.grnet.gr')), 1)
295
        # and sends user activation email
296
        functions.send_activation(user)
297

  
298
        # user activation fields updated
299
        user = AstakosUser.objects.get(pk=user.pk)
300
        self.assertTrue(user.activation_sent)
301
        self.assertFalse(user.email_verified)
302
        # email sent to user
303
        self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 1)
304

  
305
        # user forgot she got registered and tries to submit registration
306
        # form. Notice the upper case in email
307
        data = {'email':'KPAP@grnet.gr', 'password1':'password',
308
                'password2':'password', 'first_name': 'Kostas',
309
                'last_name': 'Mitroglou', 'provider': 'local'}
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff