Commit 15efc749 authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

Change twitter & shibboleth views to accept an email request parameter to associate with the user

parent 465d5225
......@@ -39,9 +39,8 @@ from hashlib import new as newhasher
from astakos.im.models import AstakosUser
from astakos.im.util import get_or_create_user
from astakos.im.forms import ExtendedUserCreationForm
from astakos.im.forms import LocalUserCreationForm
import uuid
import logging
class AdminProfileForm(forms.ModelForm):
......@@ -64,7 +63,7 @@ class AdminProfileForm(forms.ModelForm):
for field in ro_fields:
self.fields[field].widget.attrs['readonly'] = True
class AdminUserCreationForm(ExtendedUserCreationForm):
class AdminUserCreationForm(LocalUserCreationForm):
class Meta:
model = AstakosUser
fields = ("email", "first_name", "last_name", "is_superuser",
......@@ -81,7 +80,6 @@ class AdminUserCreationForm(ExtendedUserCreationForm):
def save(self, commit=True):
user = super(AdminUserCreationForm, self).save(commit=False)
user.username = uuid.uuid4().hex[:30]
user.renew_token()
if commit:
user.save()
......
......@@ -21,7 +21,7 @@
<th>Email</th>
<th>Real Name</th>
<th>Code</th>
<th>Inviter username</th>
<th>Inviter email</th>
<th>Inviter Real Name</th>
<th>Is consumed</th>
<th>Created</th>
......@@ -32,10 +32,10 @@
{% for inv in invitations %}
<tr>
<td>{{ inv.id }}</td>
<td>{{ inv.email }}</td>
<td>{{ inv.username }}</td>
<td>{{ inv.realname }}</td>
<td>{{ inv.code }}</td>
<td>{{ inv.inviter.username }}</td>
<td>{{ inv.inviter.email }}</td>
<td>{{ inv.inviter.realname }}</td>
<td>{{ inv.is_consumed }}</td>
<td>{{ inv.created }}</td>
......
......@@ -18,7 +18,7 @@
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Real Name</th>
<th>Admin</th>
<th>Affiliation</th>
......@@ -31,7 +31,7 @@
{% for user in users %}
<tr>
<td><a href="{% url astakos.im.admin.views.users_info user.id %}">{{ user.id }}</a></td>
<td><a href="{% url astakos.im.admin.views.users_info user.id %}">{{ user.username }}</a></td>
<td><a href="{% url astakos.im.admin.views.users_info user.id %}">{{ user.email }}</a></td>
<td>{{ user.realname }}</td>
<td>{{ user.is_superuser }}</td>
<td>{{ user.affiliation }}</td>
......
......@@ -42,12 +42,15 @@ from django.contrib.auth.forms import UserCreationForm
from django.contrib.sites.models import Site
from django.contrib import messages
from django.shortcuts import redirect
from django.db import transaction
from django.core.urlresolvers import reverse
from smtplib import SMTPException
from urllib import quote
from astakos.im.models import AstakosUser, Invitation
from astakos.im.forms import ExtendedUserCreationForm, InvitedExtendedUserCreationForm
from astakos.im.forms import *
from astakos.im.util import get_invitation
import socket
import logging
......@@ -84,50 +87,52 @@ class InvitationsBackend(object):
administrator activates his/her account.
"""
def __init__(self, request):
"""
raises Invitation.DoesNotExist and ValueError if invitation is consumed
or invitation username is reserved.
"""
self.request = request
self.invitation = None
self.set_invitation()
self.invitation = get_invitation(request)
def set_invitation(self):
code = self.request.GET.get('code', '')
if not code:
code = self.request.POST.get('code', '')
if code:
self.invitation = Invitation.objects.get(code=code)
if self.invitation.is_consumed:
raise Exception('Invitation has beeen used')
def get_signup_form(self, provider):
"""
Returns the form class name
"""
invitation = self.invitation
initial_data = self.get_signup_initial_data(provider)
prefix = 'Invited' if invitation else ''
main = provider.capitalize() if provider == 'local' else 'ThirdParty'
suffix = 'UserCreationForm'
formclass = '%s%s%s' % (prefix, main, suffix)
return globals()[formclass](initial_data)
def get_signup_form(self):
def get_signup_initial_data(self, provider):
"""
Returns the necassary registration form depending the user is invited or not
Throws Invitation.DoesNotExist in case ``code`` is not valid.
"""
request = self.request
formclass = 'ExtendedUserCreationForm'
if self.invitation:
formclass = 'Invited%s' %formclass
invitation = self.invitation
initial_data = None
if request.method == 'GET':
if self.invitation:
if invitation:
# create a tmp user with the invitation realname
# to extract first and last name
u = AstakosUser(realname = self.invitation.realname)
initial_data = {'username':self.invitation.username,
'email':self.invitation.username,
'inviter':self.invitation.inviter.realname,
u = AstakosUser(realname = invitation.realname)
initial_data = {'email':invitation.username,
'inviter':invitation.inviter.realname,
'first_name':u.first_name,
'last_name':u.last_name}
elif request.method == 'POST':
initial_data = request.POST
return globals()[formclass](initial_data)
else:
if provider == request.POST.get('provider', ''):
initial_data = request.POST
return initial_data
def _is_preaccepted(self, user):
"""
If there is a valid, not-consumed invitation code for the specific user
returns True else returns False.
It should be called after ``get_signup_form`` which sets invitation if exists.
"""
invitation = self.invitation
if not invitation:
......@@ -137,31 +142,43 @@ class InvitationsBackend(object):
return True
return False
@transaction.commit_manually
def signup(self, form):
"""
Initially creates an inactive user account. If the user is preaccepted
(has a valid invitation code) the user is activated and if the request
param ``next`` is present redirects to it.
In any other case the method returns the action status and a message.
The method uses commit_manually decorator in order to ensure the user
will be created only if the procedure has been completed successfully.
"""
kwargs = {}
user = form.save()
user = None
try:
user = form.save()
if self._is_preaccepted(user):
user.is_active = True
user.save()
# get the raw password from the form
password = form.cleaned_data['password1']
user = authenticate(username=user.email, password=password)
user = authenticate(email=user.email, auth_token=user.auth_token)
login(self.request, user)
message = _('Registration completed. You can now login.')
else:
message = _('Registration completed. You will receive an email upon your account\'s activation')
message = _('Registration completed. You will receive an email upon your account\'s activation.')
status = messages.SUCCESS
except Invitation.DoesNotExist, e:
status = messages.ERROR
message = _('Invalid invitation code')
return status, message
except socket.error, e:
status = messages.ERROR
message = _(e.strerror)
# rollback in case of error
if status == messages.ERROR:
transaction.rollback()
else:
transaction.commit()
return status, message, user
class SimpleBackend(object):
"""
......@@ -172,18 +189,28 @@ class SimpleBackend(object):
def __init__(self, request):
self.request = request
def get_signup_form(self):
def get_signup_form(self, provider):
"""
Returns the UserCreationForm
Returns the form class name
"""
main = provider.capitalize() if provider == 'local' else 'ThirdParty'
suffix = 'UserCreationForm'
formclass = '%s%s' % (main, suffix)
request = self.request
initial_data = request.POST if request.method == 'POST' else None
return ExtendedUserCreationForm(initial_data)
initial_data = None
if request.method == 'POST':
if provider == request.POST.get('provider', ''):
initial_data = request.POST
return globals()[formclass](initial_data)
@transaction.commit_manually
def signup(self, form, email_template_name='activation_email.txt'):
"""
Creates an inactive user account and sends a verification email.
The method uses commit_manually decorator in order to ensure the user
will be created only if the procedure has been completed successfully.
** Arguments **
``email_template_name``
......@@ -200,18 +227,23 @@ class SimpleBackend(object):
* DEFAULT_CONTACT_EMAIL: service support email
* DEFAULT_FROM_EMAIL: from email
"""
kwargs = {}
user = form.save()
status = messages.SUCCESS
user = None
try:
user = form.save()
status = messages.SUCCESS
_send_verification(self.request, user, email_template_name)
message = _('Verification sent to %s' % user.email)
except (SMTPException, socket.error) as e:
status = messages.ERROR
name = 'strerror'
message = getattr(e, name) if hasattr(e, name) else e
return status, message
# rollback in case of error
if status == messages.ERROR:
transaction.rollback()
else:
transaction.commit()
return status, message, user
def _send_verification(request, user, template_name):
site = Site.objects.get_current()
......
......@@ -48,9 +48,8 @@ from astakos.im.models import AstakosUser
from astakos.im.util import get_current_site
import logging
import uuid
class ExtendedUserCreationForm(UserCreationForm):
class LocalUserCreationForm(UserCreationForm):
"""
Extends the built in UserCreationForm in several ways:
......@@ -67,7 +66,7 @@ class ExtendedUserCreationForm(UserCreationForm):
"""
Changes the order of fields, and removes the username field.
"""
super(ExtendedUserCreationForm, self).__init__(*args, **kwargs)
super(LocalUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'first_name', 'last_name',
'password1', 'password2']
......@@ -86,18 +85,16 @@ class ExtendedUserCreationForm(UserCreationForm):
Saves the email, first_name and last_name properties, after the normal
save behavior is complete.
"""
user = super(ExtendedUserCreationForm, self).save(commit=False)
user.username = uuid.uuid4().hex[:30]
user.is_active = False
user = super(LocalUserCreationForm, self).save(commit=False)
user.renew_token()
if commit:
user.save()
logging.info('Created user %s', user)
return user
class InvitedExtendedUserCreationForm(ExtendedUserCreationForm):
class InvitedLocalUserCreationForm(LocalUserCreationForm):
"""
Extends the ExtendedUserCreationForm: adds an inviter readonly field.
Extends the LocalUserCreationForm: adds an inviter readonly field.
"""
inviter = forms.CharField(widget=forms.TextInput(), label=_('Inviter Real Name'))
......@@ -110,7 +107,7 @@ class InvitedExtendedUserCreationForm(ExtendedUserCreationForm):
"""
Changes the order of fields, and removes the username field.
"""
super(InvitedExtendedUserCreationForm, self).__init__(*args, **kwargs)
super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'inviter', 'first_name',
'last_name', 'password1', 'password2']
#set readonly form fields
......@@ -119,7 +116,7 @@ class InvitedExtendedUserCreationForm(ExtendedUserCreationForm):
self.fields['username'].widget.attrs['readonly'] = True
def save(self, commit=True):
user = super(InvitedExtendedUserCreationForm, self).save(commit=False)
user = super(InvitedLocalUserCreationForm, self).save(commit=False)
level = user.invitation.inviter.level + 1
user.level = level
user.invitations = settings.INVITATIONS_PER_LEVEL[level]
......@@ -161,6 +158,39 @@ class ProfileForm(forms.ModelForm):
user.save()
return user
class ThirdPartyUserCreationForm(ProfileForm):
class Meta:
model = AstakosUser
fields = ('email', 'last_name', 'first_name', 'affiliation', 'provider', 'third_party_identifier')
def __init__(self, *args, **kwargs):
super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email']
def clean_email(self):
email = self.cleaned_data['email']
if not email:
raise forms.ValidationError(_("This field is required"))
try:
user = AstakosUser.objects.get(email = email)
raise forms.ValidationError(_("Email is reserved"))
except AstakosUser.DoesNotExist:
return email
def save(self, commit=True):
user = super(ThirdPartyUserCreationForm, self).save(commit=False)
user.verified = False
user.renew_token()
if commit:
user.save()
logging.info('Created user %s', user)
return user
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
def __init__(self, *args, **kwargs):
super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
#set readonly form fields
self.fields['email'].widget.attrs['readonly'] = True
class FeedbackForm(forms.Form):
"""
......@@ -208,4 +238,3 @@ class ExtendedPasswordResetForm(PasswordResetForm):
from_email = settings.DEFAULT_FROM_EMAIL % site_name
send_mail(_("Password reset on %s") % site_name,
t.render(Context(c)), from_email, [user.email])
......@@ -33,6 +33,7 @@
import logging
import hashlib
import uuid
from time import asctime
from datetime import datetime, timedelta
......@@ -67,6 +68,9 @@ class AstakosUser(User):
updated = models.DateTimeField('Update date')
is_verified = models.BooleanField('Is verified?', default=False)
# ex. screen_name for twitter, eppn for shibboleth
third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
@property
def realname(self):
return '%s %s' %(self.first_name, self.last_name)
......@@ -98,14 +102,19 @@ class AstakosUser(User):
def save(self, update_timestamps=True, **kwargs):
if update_timestamps:
if not self.id:
while not self.username:
username = uuid.uuid4().hex[:30]
try:
AstakosUser.objects.get(username = username)
except AstakosUser.DoesNotExist, e:
self.username = username
self.is_active = False
if not self.provider:
self.provider = 'local'
self.date_joined = datetime.now()
self.updated = datetime.now()
super(AstakosUser, self).save(**kwargs)
#invitation consume
if self.invitation and not self.invitation.is_consumed:
self.invitation.consume()
def renew_token(self):
md5 = hashlib.md5()
......
def create_user(request, form=None, backend=None, template_name='login.html', extra_context={}):
try:
if not backend:
backend = get_backend(request)
if not form:
form = backend.get_signup_form()
if form.is_valid():
status, message = backend.signup(form)
messages.add_message(request, status, message)
else:
messages.add_message(request, messages.ERROR, form.errors)
except (Invitation.DoesNotExist, ValueError), e:
messages.add_message(request, messages.ERROR, e)
#delete cookie
return render_response(template_name,
form = LocalUserCreationForm(),
context_instance=get_context(request, extra_context))
\ No newline at end of file
......@@ -40,13 +40,14 @@ from django.contrib.auth import authenticate
from django.contrib import messages
from django.utils.translation import ugettext as _
from astakos.im.target.util import prepare_response
from astakos.im.target.util import prepare_response, requires_anonymous
from astakos.im.models import AstakosUser
from astakos.im.forms import LoginForm
from urllib import unquote
from hashlib import new as newhasher
@requires_anonymous
def login(request, on_failure='login.html'):
"""
on_failure: the template name to render on login failure
......
......@@ -32,11 +32,15 @@
# or implied, of GRNET S.A.
from django.http import HttpResponseBadRequest
from django.core.urlresolvers import reverse
from django.contrib.auth import authenticate
from django.contrib import messages
from astakos.im.target.util import prepare_response
from astakos.im.util import get_or_create_user
from astakos.im.target.util import prepare_response, requires_anonymous
from astakos.im.util import get_or_create_user, get_context
from astakos.im.models import AstakosUser, Invitation
from astakos.im.views import render_response, create_user
from astakos.im.backends import get_backend
from astakos.im.forms import LocalUserCreationForm, ThirdPartyUserCreationForm
class Tokens:
# these are mapped by the Shibboleth SP software
......@@ -48,8 +52,12 @@ class Tokens:
SHIB_EP_AFFILIATION = "HTTP_SHIB_EP_AFFILIATION"
SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID"
@requires_anonymous
def login(request):
# store invitation code and email
request.session['email'] = request.GET.get('email')
request.session['invitation_code'] = request.GET.get('code')
tokens = request.META
try:
......@@ -68,10 +76,44 @@ def login(request):
affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '')
user = get_or_create_user(eppn, realname=realname, affiliation=affiliation, provider='shibboleth', level=0)
# in order to login the user we must call authenticate first
user = authenticate(email=user.email, auth_token=user.auth_token)
return prepare_response(request,
user,
request.GET.get('next'),
'renew' in request.GET)
next = request.GET.get('next')
# check first if user with that identifier is registered
user = None
email = request.session.pop('email')
if email:
# signup mode
if not reserved_screen_name(eppn):
try:
user = AstakosUser.objects.get(email = email)
except AstakosUser.DoesNotExist, e:
# register a new user
first_name, space, last_name = realname.partition(' ')
post_data = {'provider':'Shibboleth', 'first_name':first_name,
'last_name':last_name, 'affiliation':affiliation,
'third_party_identifier':eppn}
form = ThirdPartyUserCreationForm({'email':email})
return create_user(request, form, backend, post_data, next, template_name, extra_context)
else:
status = messages.ERROR
message = '%s@shibboleth is already registered' % eppn
messages.add_message(request, messages.ERROR, message)
else:
# login mode
if user and user.is_active:
#in order to login the user we must call authenticate first
user = authenticate(email=user.email, auth_token=user.auth_token)
return prepare_response(request, user, next)
elif user and not user.is_active:
messages.add_message(request, messages.ERROR, 'Inactive account: %s' % user.email)
return render_response(template_name,
form = LocalUserCreationForm(),
context_instance=get_context(request, extra_context))
def reserved_identifier(identifier):
try:
AstakosUser.objects.get(provider='Shibboleth',
third_party_identifier=identifier)
return True
except AstakosUser.DoesNotExist, e:
return False
\ No newline at end of file
......@@ -35,14 +35,22 @@
import oauth2 as oauth
import urlparse
import traceback
from django.conf import settings
from django.http import HttpResponse
from django.utils import simplejson as json
from django.contrib.auth import authenticate
from django.contrib import messages
from django.shortcuts import redirect
from astakos.im.target.util import prepare_response
from astakos.im.util import get_or_create_user
from astakos.im.target.util import prepare_response, requires_anonymous
from astakos.im.util import get_or_create_user, get_context
from astakos.im.models import AstakosUser, Invitation
from astakos.im.views import render_response, create_user
from astakos.im.backends import get_backend
from astakos.im.forms import LocalUserCreationForm, ThirdPartyUserCreationForm
from astakos.im.faults import BadRequest
# It's probably a good idea to put your consumer's OAuth token and
# OAuth secret into your project's settings.
......@@ -55,7 +63,12 @@ access_token_url = 'http://twitter.com/oauth/access_token'
# This is the slightly different URL used to authenticate/authorize.
authenticate_url = 'http://twitter.com/oauth/authenticate'
def login(request):
@requires_anonymous
def login(request, template_name='signup.html', extra_context={}):
# store invitation code and email
request.session['email'] = request.GET.get('email')
request.session['invitation_code'] = request.GET.get('code')
# Step 1. Get a request token from Twitter.
resp, content = client.request(request_token_url, "GET")
if resp['status'] != '200':
......@@ -66,7 +79,7 @@ def login(request):
# Step 2. Store the request token in a session for later use.
response = HttpResponse()
response.set_cookie('Twitter-Request-Token', value=json.dumps(request_token), max_age=300)
request.session['Twitter-Request-Token'] = value=json.dumps(request_token)
# Step 3. Redirect the user to the authentication URL.
url = "%s?oauth_token=%s" % (authenticate_url, request_token['oauth_token'])
......@@ -75,11 +88,14 @@ def login(request):
return response
def authenticated(request):
@requires_anonymous
def authenticated(request, backend=None, template_name='login.html', extra_context={}):
# Step 1. Use the request token in the session to build a new client.
data = request.COOKIES.get('Twitter-Request-Token', None)
data = request.session.get('Twitter-Request-Token')
if not data:
raise Exception("Request token cookie not found.")
del request.session['Twitter-Request-Token']
request_token = json.loads(data)
if not hasattr(request_token, '__getitem__'):
raise BadRequest('Invalid data formating')
......@@ -114,12 +130,50 @@ def authenticated(request):
# These two things will likely never be used. Alternatively, you
# can prompt them for their email here. Either way, the password
# should never be used.
email = '%s@twitter.com' % access_token['screen_name']
realname = access_token['screen_name']
screen_name = access_token['screen_name']
next = request_token.get('next')
# check first if user with that email is registered
# and if not create one
user = None
email = request.session.pop('email')
user = get_or_create_user(email, realname=