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'} |
Also available in: Unified diff