Commit 5fec991c authored by Kostas Papadimitriou's avatar Kostas Papadimitriou
Browse files

Various auth providers fixes/improvements

- Handle invalid login after auth method add request
- Fix auth method add for unauthenticated users
- Third party auth providers helper methods in astakos.im.target module
- Provider login url template tag that handles code,key,next url params
parent b41cede9
......@@ -42,6 +42,7 @@ from astakos.im import settings as astakos_settings
from astakos.im import messages as astakos_messages
import logging
import urllib
logger = logging.getLogger(__name__)
......@@ -87,8 +88,17 @@ class AuthProvider(object):
msg = 'AUTH_PROVIDER_%s' % msg
return override_msg or getattr(astakos_messages, msg, msg) % params
@property
def add_url(self):
return reverse(self.login_view)
def __init__(self, user=None):
self.user = user
for tpl in ['login_prompt', 'login', 'signup_prompt']:
tpl_name = '%s_%s' % (tpl, 'template')
override = self.get_setting(tpl_name)
if override:
setattr(self, tpl_name, override)
def __getattr__(self, key):
if not key.startswith('get_'):
......@@ -141,11 +151,7 @@ class LocalAuthProvider(AuthProvider):
login_prompt = _('if you already have a username and password')
signup_prompt = _('New to ~okeanos ?')
signup_link_prompt = _('create an account now')
@property
def add_url(self):
return reverse('password_change')
login_view = 'password_change'
one_per_user = True
......@@ -171,13 +177,10 @@ class ShibbolethAuthProvider(AuthProvider):
primary_login_prompt = _('If you are a student/researcher/faculty you can'
' login using your university-credentials in'
' the following page')
@property
def add_url(self):
return reverse('astakos.im.target.shibboleth.login')
login_view = 'astakos.im.target.shibboleth.login'
login_template = 'im/auth/shibboleth_login.html'
login_prompt_template = 'im/auth/shibboleth_login_prompt.html'
login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
class TwitterAuthProvider(AuthProvider):
......@@ -186,10 +189,7 @@ class TwitterAuthProvider(AuthProvider):
add_prompt = _('Allows you to login to your account using Twitter')
details_tpl = _('Twitter screen name: %(info_screen_name)s')
user_title = _('Twitter (%(info_screen_name)s)')
@property
def add_url(self):
return reverse('astakos.im.target.twitter.login')
login_view = 'astakos.im.target.twitter.login'
login_template = 'im/auth/third_party_provider_generic_login.html'
login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
......@@ -201,10 +201,7 @@ class GoogleAuthProvider(AuthProvider):
add_prompt = _('Allows you to login to your account using Google')
details_tpl = _('Google account: %(info_email)s')
user_title = _('Google (%(info_email)s)')
@property
def add_url(self):
return reverse('astakos.im.target.google.login')
login_view = 'astakos.im.target.google.login'
login_template = 'im/auth/third_party_provider_generic_login.html'
login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
......@@ -216,10 +213,7 @@ class LinkedInAuthProvider(AuthProvider):
add_prompt = _('Allows you to login to your account using LinkedIn')
user_title = _('LinkedIn (%(info_emailAddress)s)')
details_tpl = _('LinkedIn account: %(info_emailAddress)s')
@property
def add_url(self):
return reverse('astakos.im.target.linkedin.login')
login_view = 'astakos.im.target.linkedin.login'
login_template = 'im/auth/third_party_provider_generic_login.html'
login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
......
......@@ -468,23 +468,24 @@ class SendInvitationForm(forms.Form):
class ExtendedPasswordResetForm(PasswordResetForm):
"""
Extends PasswordResetForm by overriding save method:
passes a custom from_email in send_mail.
Extends PasswordResetForm by overriding
Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
accepts a from_email argument.
save method: to pass a custom from_email in send_mail.
clean_email: to handle local auth provider checks
"""
def clean_email(self):
email = super(ExtendedPasswordResetForm, self).clean_email()
try:
user = AstakosUser.objects.get(email__iexact=email)
user = AstakosUser.objects.get_by_identifier(email)
if not user.is_active:
raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
if not user.has_usable_password():
raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
if not user.can_change_password():
raise forms.ValidationError(_('Password change for this account'
' is not supported.'))
raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
except AstakosUser.DoesNotExist, e:
raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
return email
......@@ -619,7 +620,7 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# )
# desc = forms.CharField(
# label= 'Description',
# widget=forms.Textarea,
# widget=forms.Textarea,
# help_text= "Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. "
# )
# issue_date = forms.DateTimeField(
......@@ -641,22 +642,22 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# required=True, min_value=1,
# help_text="Here you specify the number of members this Project is going to have. This means that this number of people will be granted the resources you will specify in the next step. This can be '1' if you are the only one wanting to get resources. "
# )
#
#
# class Meta:
# model = AstakosGroup
#
#
# def __init__(self, *args, **kwargs):
# #update QueryDict
# args = list(args)
# qd = args.pop(0).copy()
# members_unlimited = qd.pop('members_unlimited', False)
# members_uplimit = qd.pop('members_uplimit', None)
#
#
# #substitue QueryDict
# args.insert(0, qd)
#
#
# super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
#
#
# self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
# 'issue_date', 'expiration_date',
# 'moderation_enabled', 'max_participants']
......@@ -670,7 +671,7 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# map(add_fields,
# ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
# )
#
#
# def add_fields((k, v)):
# self.fields[k] = forms.BooleanField(
# required=False,
......@@ -679,26 +680,26 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# map(add_fields,
# ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
# )
#
#
# def policies(self):
# self.clean()
# policies = []
# append = policies.append
# for name, uplimit in self.cleaned_data.iteritems():
#
#
# subs = name.split('_uplimit')
# if len(subs) == 2:
# prefix, suffix = subs
# s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
# resource = Resource.objects.get(service__name=s, name=r)
#
#
# # keep only resource limits for selected resource groups
# if self.cleaned_data.get(
# 'is_selected_%s' % resource.group, False
# ):
# append(dict(service=s, resource=r, uplimit=uplimit))
# return policies
#
#
# class AstakosGroupCreationSummaryForm(forms.ModelForm):
# kind = forms.ModelChoiceField(
# queryset=GroupKind.objects.all(),
......@@ -717,20 +718,20 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# max_participants = forms.IntegerField(
# required=False, min_value=1
# )
#
#
# class Meta:
# model = AstakosGroup
#
#
# def __init__(self, *args, **kwargs):
# #update QueryDict
# args = list(args)
# qd = args.pop(0).copy()
# members_unlimited = qd.pop('members_unlimited', False)
# members_uplimit = qd.pop('members_uplimit', None)
#
#
# #substitue QueryDict
# args.insert(0, qd)
#
#
# super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
# self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
# 'issue_date', 'expiration_date',
......@@ -744,7 +745,7 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# map(add_fields,
# ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
# )
#
#
# def add_fields((k, v)):
# self.fields[k] = forms.BooleanField(
# required=False,
......@@ -755,7 +756,7 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# )
# for f in self.fields.values():
# f.widget = forms.HiddenInput()
#
#
# def clean(self):
# super(AstakosGroupCreationSummaryForm, self).clean()
# self.cleaned_data['policies'] = []
......@@ -769,7 +770,7 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# prefix, suffix = subs
# s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
# resource = Resource.objects.get(service__name=s, name=r)
#
#
# # keep only resource limits for selected resource groups
# if self.cleaned_data.get(
# 'is_selected_%s' % resource.group, False
......@@ -778,19 +779,19 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# for name in tbd:
# self.cleaned_data.pop(name, None)
# return self.cleaned_data
#
#
# class AstakosGroupUpdateForm(forms.ModelForm):
# class Meta:
# model = AstakosGroup
# fields = ( 'desc','homepage', 'moderation_enabled')
#
#
#
#
# class AddGroupMembersForm(forms.Form):
# q = forms.CharField(
# max_length=800, widget=forms.Textarea, label=_('Add members'),
# help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
# required=True)
#
#
# def clean(self):
# q = self.cleaned_data.get('q') or ''
# users = q.split(',')
......@@ -801,19 +802,19 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
# self.valid_users = db_entries
# return self.cleaned_data
#
#
# def get_valid_users(self):
# """Should be called after form cleaning"""
# try:
# return self.valid_users
# except:
# return ()
#
#
#
#
# class AstakosGroupSearchForm(forms.Form):
# q = forms.CharField(max_length=200, label='Search project')
#
#
#
#
# class TimelineForm(forms.Form):
# entity = forms.ModelChoiceField(
# queryset=AstakosUser.objects.filter(is_active=True)
......@@ -830,7 +831,7 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# ('charge_usage', 'Charge Usage'),
# ('charge_traffic', 'Charge Traffic'), )
# )
#
#
# def clean(self):
# super(TimelineForm, self).clean()
# d = self.cleaned_data
......@@ -844,8 +845,8 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# if 'entity' in d:
# d['entity'] = d['entity'].email
# return d
#
#
#
#
# class AstakosGroupSortForm(forms.Form):
# sorting = forms.ChoiceField(
# label='Sort by',
......@@ -859,7 +860,7 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# ),
# required=True
# )
#
#
# class MembersSortForm(forms.Form):
# sorting = forms.ChoiceField(
# label='Sort by',
......@@ -869,7 +870,7 @@ class ExtendedPasswordChangeForm(PasswordChangeForm):
# ),
# required=True
# )
#
#
# class PickResourceForm(forms.Form):
# resource = forms.ModelChoiceField(
# queryset=Resource.objects.select_related().all()
......@@ -920,11 +921,11 @@ class ProjectApplicationForm(forms.ModelForm):
homepage = forms.URLField(
help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",
widget=forms.TextInput(attrs={'placeholder': 'http://myproject.com'}),
required=False
)
comments = forms.CharField(widget=forms.Textarea, required=False)
class Meta:
model = ProjectApplication
exclude = (
......@@ -935,7 +936,7 @@ class ProjectApplicationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.precursor_application = kwargs.get('instance')
super(ProjectApplicationForm, self).__init__(*args, **kwargs)
def clean(self):
userid = self.data.get('user', None)
self.user = None
......@@ -948,7 +949,7 @@ class ProjectApplicationForm(forms.ModelForm):
raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
super(ProjectApplicationForm, self).clean()
return self.cleaned_data
@property
def resource_policies(self):
policies = []
......@@ -971,9 +972,9 @@ class ProjectApplicationForm(forms.ModelForm):
append(dict(service=s, resource=r, uplimit=uplimit))
else:
append(dict(service=s, resource=r, uplimit=None))
return policies
def save(self, commit=True):
application = super(ProjectApplicationForm, self).save(commit=False)
......
# Copyright 2011-2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import json
from django.contrib import messages
from django.utils.translation import ugettext as _
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from astakos.im.models import PendingThirdPartyUser
from astakos.im.util import get_query
from astakos.im import messages as astakos_messages
from astakos.im import auth_providers
from astakos.im.util import prepare_response, get_context
from astakos.im.views import requires_anonymous, render_response
def add_pending_auth_provider(request, third_party_token):
if third_party_token:
# use requests to assign the account he just authenticated with with
# a third party provider account
try:
request.user.add_pending_auth_provider(third_party_token)
messages.success(request, _(astakos_messages.AUTH_PROVIDER_ADDED))
except PendingThirdPartyUser.DoesNotExist:
messages.error(request, _(astakos_messages.AUTH_PROVIDER_ADD_FAILED))
def get_pending_key(request):
third_party_token = get_query(request).get('key', False)
if not third_party_token:
third_party_token = request.session.get('pending_key', None)
if third_party_token:
del request.session['pending_key']
return third_party_token
def handle_third_party_signup(request, userid, provider_module, third_party_key,
provider_info={},
pending_user_params={},
template="im/third_party_check_local.html",
extra_context={}):
# user wants to add another third party login method
if third_party_key:
messages.error(request, _(astakos_messages.AUTH_PROVIDER_INVALID_LOGIN))
return HttpResponseRedirect(reverse('login') + "?key=%s" % third_party_key)
provider = auth_providers.get_provider(provider_module)
if not provider.is_available_for_create():
messages.error(request,
_(astakos_messages.AUTH_PROVIDER_INVALID_LOGIN))
return HttpResponseRedirect(reverse('login'))
# eppn not stored in astakos models, create pending profile
user, created = PendingThirdPartyUser.objects.get_or_create(
third_party_identifier=userid,
provider=provider_module,
)
# update pending user
for param, value in pending_user_params.iteritems():
setattr(user, param, value)
user.info = json.dumps(provider_info)
user.generate_token()
user.save()
extra_context['provider'] = provider_module
extra_context['provider_title'] = provider.get_title_display
extra_context['token'] = user.token
extra_context['signup_url'] = reverse('signup') + \
"?third_party_token=%s" % user.token
extra_context['add_url'] = reverse('index') + \
"?key=%s#other-login-methods" % user.token
extra_context['can_create'] = provider.is_available_for_create()
extra_context['can_add'] = provider.is_available_for_add()
return render_response(
template,
context_instance=get_context(request, extra_context)
)
......@@ -45,7 +45,7 @@ from django.shortcuts import get_object_or_404
from urlparse import urlunsplit, urlsplit
from astakos.im.util import prepare_response, get_context
from astakos.im.util import prepare_response, get_context, login_url
from astakos.im.views import requires_anonymous, render_response, \
requires_auth_provider
from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
......@@ -54,6 +54,8 @@ from astakos.im.forms import LoginForm
from astakos.im.activation_backends import get_backend, SimpleBackend
from astakos.im import settings
from astakos.im import auth_providers
from astakos.im.target import add_pending_auth_provider, get_pending_key, \
handle_third_party_signup
import logging
import time
......@@ -97,6 +99,9 @@ def login(request):
if force_login:
params['approval_prompt'] = 'force'
if request.GET.get('key', None):
request.session['pending_key'] = request.GET.get('key')
url = "%s?%s" % (authenticate_url, urllib.urlencode(params))
return HttpResponseRedirect(url)
......@@ -143,6 +148,8 @@ def authenticated(
provider_info = access_token_data
affiliation = 'Google.com'
third_party_key = get_pending_key(request)
# an existing user accessed the view
if request.user.is_authenticated():
if request.user.has_auth_provider('google', identifier=userid):
......@@ -172,48 +179,24 @@ def authenticated(
# authenticate user
response = prepare_response(request,
user,
userid,
request.GET.get('next'),
'renew' in request.GET)
messages.success(request, _(astakos_messages.LOGIN_SUCCESS))
add_pending_auth_provider(request, third_party_key)
response.set_cookie('astakos_last_login_method', 'google')
return response
else:
message = user.get_inactive_message()
messages.error(request, message)
return HttpResponseRedirect(reverse('login'))
return HttpResponseRedirect(login_url(request))
except AstakosUser.DoesNotExist, e:
provider = auth_providers.get_provider('google')
if not provider.is_available_for_create():
messages.error(request,
_(astakos_messages.AUTH_PROVIDER_INVALID_LOGIN))
return HttpResponseRedirect(reverse('login'))
# eppn not stored in astakos models, create pending profile
user, created = PendingThirdPartyUser.objects.get_or_create(
third_party_identifier=userid,
provider='google',
)
# update pending user
user.affiliation = affiliation
user.info = json.dumps(provider_info)
user.generate_token()
user.save()
extra_context['provider'] = 'google'
extra_context['provider_title'] = provider.get_title_display
extra_context['token'] = user.token
extra_context['signup_url'] = reverse('signup') + \
"?third_party_token=%s" % user.token
extra_context['add_url'] = reverse('index') + \
"?key=%s#other-login-methods" % user.token
extra_context['can_create'] = provider.is_available_for_create()
extra_context['can_add'] = provider.is_available_for_add()
return render_response(
template,
context_instance=get_context(request, extra_context)
)
user_info = {'affiliation': affiliation}
return handle_third_party_signup(request, userid, 'google',
third_party_key,
provider_info,
user_info,
template,
extra_context)
......@@ -45,7 +45,7 @@ from django.shortcuts import get_object_or_404
from urlparse import urlunsplit, urlsplit
from astakos.im.util import prepare_response, get_context
from astakos.im.util import prepare_response, get_context, login_url
from astakos.im.views import requires_anonymous, render_response, \
requires_auth_provider
from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
......@@ -54,6 +54,8 @@ from astakos.im.forms import LoginForm
from astakos.im.activation_backends import get_backend, SimpleBackend
from astakos.im import settings
from astakos.im import auth_providers
from astakos.im.target import add_pending_auth_provider, get_pending_key, \
handle_third_party_signup
import astakos.im.messages as astakos_messages
......@@ -85,6 +87,9 @@ def login(request):
url = request_token.get('xoauth_request_auth_url') + "?oauth_token=%s" % request_token.get('oauth_token')
if request.GET.get('key', None):
request.session['pending_key'] = request.GET.get('key')
return HttpResponseRedirect(url)
......@@ -116,14 +121,12 @@ def authenticated(
messages.error(request, 'Invalid linkedin token response')
return HttpResponseRedirect(reverse('edit_profile'))
access_token = dict(cgi.parse_qsl(content))
print "ACCESS", access_token
token = oauth.Token(access_token['oauth_token'],
access_token['oauth_token_secret'])
client = oauth.Client(consumer, token)
resp, content = client.request("http://api.linkedin.com/v1/people/~:(id,first-name,last-name,industry,email-address)?format=json", "GET")