Commit fd143fc8 authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

ask acknowledgment for switching local account to shibboleth one

* unique email and is_active combination (use django model validation)
* use different template for rendering shibboleth signup form
* do not show groups in profile page
* do not show provider in third-party signup forms

Refs: #2167
parent 3f288bc0
......@@ -46,7 +46,7 @@ from urlparse import urljoin
from astakos.im.models import AstakosUser, Invitation
from astakos.im.forms import *
from astakos.im.util import get_invitation
from astakos.im.functions import send_verification, send_admin_notification, activate
from astakos.im.functions import send_verification, send_admin_notification, activate, SendMailError
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
import socket
......@@ -78,15 +78,66 @@ def get_backend(request):
raise ImproperlyConfigured('Module "%s" does not define a activation backend named "%s"' % (module, attr))
return backend_class(request)
class SignupBackend(object):
class ActivationBackend(object):
def _is_preaccepted(self, user):
# return True if user email matches specific patterns
for pattern in RE_USER_EMAIL_PATTERNS:
if re.match(pattern, user.email):
return True
return False
def get_signup_form(self, provider='local', instance=None):
"""
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 = None
if request.method == 'POST':
if provider == request.POST.get('provider', ''):
initial_data = request.POST
return globals()[formclass](initial_data, instance=instance, request=request)
def handle_activation(self, user, \
verification_template_name='im/activation_email.txt', \
greeting_template_name='im/welcome_email.txt', \
admin_email_template_name='im/admin_notification.txt', \
switch_accounts_email_template_name='im/switch_accounts_email.txt'):
"""
If the user is already active returns immediately.
If the user is not active and there is another account associated with
the specific email, it sends an informative email to the user whether
wants to switch to this account.
If the user is preaccepted and the email is verified, the account is
activated automatically. Otherwise, if the email is not verified,
it sends a verification email to the user.
If the user is not preaccepted, it sends an email to the administrators
and informs the user that the account is pending activation.
"""
try:
if user.is_active:
return RegistationCompleted()
if user.conflicting_email():
send_verification(user, switch_accounts_email_template_name)
return SwitchAccountsVerificationSent(user.email)
if self._is_preaccepted(user):
if user.email_verified:
activate(user, greeting_template_name)
return RegistationCompleted()
else:
send_verification(user, verification_template_name)
return VerificationSent()
else:
send_admin_notification(user, admin_email_template_name)
return NotificationSent()
except BaseException, e:
logger.exception(e)
raise e
class InvitationsBackend(SignupBackend):
class InvitationsBackend(ActivationBackend):
"""
A activation backend which implements the following workflow: a user
supplies the necessary registation information, if the request contains a valid
......@@ -95,31 +146,24 @@ class InvitationsBackend(SignupBackend):
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
super(InvitationsBackend, self).__init__()
def get_signup_form(self, provider='local', instance=None):
"""
Returns the form class name
Returns the form class
raises Invitation.DoesNotExist and ValueError if invitation is consumed
or invitation username is reserved.
"""
try:
self.invitation = get_invitation(self.request)
except (Invitation, ValueError), e:
self.invitation = None
else:
invitation = self.invitation
initial_data = self.get_signup_initial_data(provider)
prefix = 'Invited' if invitation else ''
main = provider.capitalize()
suffix = 'UserCreationForm'
formclass = '%s%s%s' % (prefix, main, suffix)
ip = self.request.META.get('REMOTE_ADDR',
self.request.META.get('HTTP_X_REAL_IP', None))
return globals()[formclass](initial_data, instance=instance, ip=ip)
self.invitation = get_invitation(self.request)
invitation = self.invitation
initial_data = self.get_signup_initial_data(provider)
prefix = 'Invited' if invitation else ''
main = provider.capitalize()
suffix = 'UserCreationForm'
formclass = '%s%s%s' % (prefix, main, suffix)
return globals()[formclass](initial_data, instance=instance, request=self.request)
def get_signup_initial_data(self, provider):
"""
......@@ -160,33 +204,7 @@ class InvitationsBackend(SignupBackend):
return True
return False
def handle_activation(self, user, verification_template_name='im/activation_email.txt', greeting_template_name='im/welcome_email.txt', admin_email_template_name='im/admin_notification.txt'):
"""
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.
"""
try:
if user.is_active:
return RegistationCompleted()
if self._is_preaccepted(user):
if user.email_verified:
activate(user, greeting_template_name)
return RegistationCompleted()
else:
send_verification(user, verification_template_name)
return VerificationSent()
else:
send_admin_notification(user, admin_email_template_name)
return NotificationSent()
except Invitation.DoesNotExist, e:
raise InvitationCodeError()
except BaseException, e:
logger.exception(e)
raise e
class SimpleBackend(SignupBackend):
class SimpleBackend(ActivationBackend):
"""
A activation backend which implements the following workflow: a user
supplies the necessary registation information, an incative user account is
......@@ -195,20 +213,6 @@ class SimpleBackend(SignupBackend):
def __init__(self, request):
self.request = request
super(SimpleBackend, self).__init__()
def get_signup_form(self, provider='local', instance=None):
"""
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 = None
if request.method == 'POST':
if provider == request.POST.get('provider', ''):
initial_data = request.POST
return globals()[formclass](initial_data, instance=instance, request=request)
def _is_preaccepted(self, user):
if super(SimpleBackend, self)._is_preaccepted(user):
......@@ -216,43 +220,6 @@ class SimpleBackend(SignupBackend):
if MODERATION_ENABLED:
return False
return True
def handle_activation(self, user, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
"""
Creates an inactive user account and sends a verification email.
** Arguments **
``email_template_name``
A custom template for the verification email body to use. This is
optional; if not specified, this will default to
``im/activation_email.txt``.
** Templates **
im/activation_email.txt or ``email_template_name`` keyword argument
** Settings **
* DEFAULT_CONTACT_EMAIL: service support email
* DEFAULT_FROM_EMAIL: from email
"""
try:
if user.is_active:
return RegistrationCompeted()
if not self._is_preaccepted(user):
send_admin_notification(user, admin_email_template_name)
return NotificationSent()
else:
send_verification(user, email_template_name)
return VerificationSend()
except SendEmailError, e:
transaction.rollback()
raise e
except BaseException, e:
logger.exception(e)
raise e
else:
transaction.commit()
class ActivationResult(object):
def __init__(self, message):
......@@ -263,6 +230,14 @@ class VerificationSent(ActivationResult):
message = _('Verification sent.')
super(VerificationSent, self).__init__(message)
class SwitchAccountsVerificationSent(ActivationResult):
def __init__(self, email):
message = _('This email is already associated with another \
local account. To change this account to a shibboleth \
one follow the link in the verification email sent \
to %s. Otherwise just ignore it.' % email)
super(SwitchAccountsVerificationSent, self).__init__(message)
class NotificationSent(ActivationResult):
def __init__(self):
message = _('Your request for an account was successfully received and is now pending \
......
......@@ -186,7 +186,7 @@ def get_menu(request, with_extra_links=False, with_signout=True):
cookie = urllib.unquote(request.COOKIES.get(COOKIE_NAME, ''))
email = cookie.partition('|')[0]
try:
user = AstakosUser.objects.get(email=email)
user = AstakosUser.objects.get(email=email, is_active=True)
except AstakosUser.DoesNotExist:
pass
else:
......@@ -228,7 +228,7 @@ def find_userid(request):
if not email:
raise BadRequest('Email missing')
try:
user = AstakosUser.objects.get(email = email)
user = AstakosUser.objects.get(email = email, is_active=True)
except AstakosUser.DoesNotExist, e:
raise BadRequest('Invalid email')
else:
......
......@@ -9,7 +9,7 @@ class TokenBackend(ModelBackend):
"""
def authenticate(self, email=None, auth_token=None):
try:
user = AstakosUser.objects.get(email=email)
user = AstakosUser.objects.get(email=email, is_active=True)
if user.auth_token == auth_token:
return user
except AstakosUser.DoesNotExist:
......@@ -32,7 +32,7 @@ class EmailBackend(ModelBackend):
#If username is an email address, then try to pull it up
if email_re.search(username):
try:
user = AstakosUser.objects.get(email=username)
user = AstakosUser.objects.get(email=username, is_active=True)
except AstakosUser.DoesNotExist:
return None
else:
......
......@@ -34,6 +34,7 @@
from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED, IM_STATIC_URL, \
COOKIE_NAME, LOGIN_MESSAGES, PROFILE_EXTRA_LINKS
from astakos.im.api import get_menu
from astakos.im.util import get_query
from django.conf import settings
from django.core.urlresolvers import reverse
......@@ -43,8 +44,7 @@ def im_modules(request):
return {'im_modules': IM_MODULES}
def next(request):
query_dict = request.__getattribute__(request.method)
return {'next' : query_dict.get('next', '')}
return {'next' : get_query(request).get('next', '')}
def code(request):
return {'code' : request.GET.get('code', '')}
......
......@@ -43,13 +43,14 @@ from django.utils.http import int_to_base36
from django.core.urlresolvers import reverse
from django.utils.functional import lazy
from django.utils.safestring import mark_safe
from django.contrib import messages
from astakos.im.models import AstakosUser, Invitation
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED
from astakos.im.widgets import DummyWidget, RecaptchaWidget, ApprovalTermsWidget
# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
from astakos.im.util import reverse_lazy, get_latest_terms
from astakos.im.util import reverse_lazy, get_latest_terms, reserved_email, get_query
import logging
import recaptcha.client.captcha as captcha
......@@ -103,11 +104,9 @@ class LocalUserCreationForm(UserCreationForm):
email = self.cleaned_data['email']
if not email:
raise forms.ValidationError(_("This field is required"))
try:
AstakosUser.objects.get(email = email)
if reserved_email(email):
raise forms.ValidationError(_("This email is already used"))
except AstakosUser.DoesNotExist:
return email
return email
def clean_has_signed_terms(self):
has_signed_terms = self.cleaned_data['has_signed_terms']
......@@ -181,21 +180,22 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
class ThirdPartyUserCreationForm(forms.ModelForm):
class Meta:
model = AstakosUser
fields = ("email", "first_name", "last_name", "third_party_identifier",
"has_signed_terms", "provider")
fields = ("email", "first_name", "last_name", "third_party_identifier")
widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
def __init__(self, *args, **kwargs):
"""
Changes the order of fields, and removes the username field.
"""
self.request = kwargs.get('request', None)
if self.request:
kwargs.pop('request')
super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'first_name', 'last_name',
'provider', 'third_party_identifier']
self.fields.keyOrder = ['email', 'first_name', 'last_name', 'third_party_identifier']
if get_latest_terms():
self.fields.keyOrder.append('has_signed_terms')
#set readonly form fields
ro = ["provider", "third_party_identifier", "first_name", "last_name"]
ro = ["third_party_identifier", "first_name", "last_name"]
for f in ro:
self.fields[f].widget.attrs['readonly'] = True
......@@ -211,11 +211,9 @@ class ThirdPartyUserCreationForm(forms.ModelForm):
email = self.cleaned_data['email']
if not email:
raise forms.ValidationError(_("This field is required"))
try:
AstakosUser.objects.get(email = email)
if reserved_email(email):
raise forms.ValidationError(_("This email is already used"))
except AstakosUser.DoesNotExist:
return email
return email
def clean_has_signed_terms(self):
has_signed_terms = self.cleaned_data['has_signed_terms']
......@@ -227,17 +225,12 @@ class ThirdPartyUserCreationForm(forms.ModelForm):
user = super(ThirdPartyUserCreationForm, self).save(commit=False)
user.set_unusable_password()
user.renew_token()
user.provider = get_query(self.request).get('provider')
if commit:
user.save()
logger.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 InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
"""
Extends the LocalUserCreationForm: adds an inviter readonly field.
......@@ -270,17 +263,12 @@ class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
email = self.cleaned_data['email']
if not email:
raise forms.ValidationError(_("This field is required"))
try:
user = AstakosUser.objects.get(email = email)
if user.provider == 'local':
self.instance = user
return email
else:
for user in AstakosUser.objects.filter(email = email):
if user.provider == 'shibboleth':
raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
except AstakosUser.DoesNotExist:
return email
return email
class InvitedShibbolethUserCreationForm(InvitedThirdPartyUserCreationForm):
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
pass
class LoginForm(AuthenticationForm):
......@@ -335,12 +323,12 @@ class ProfileForm(forms.ModelForm):
class Meta:
model = AstakosUser
fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires', 'groups')
fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires')
def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
ro_fields = ('auth_token', 'auth_token_expires', 'groups')
ro_fields = ('email', 'auth_token', 'auth_token_expires')
if instance and instance.id:
for field in ro_fields:
self.fields[field].widget.attrs['readonly'] = True
......@@ -358,7 +346,7 @@ class FeedbackForm(forms.Form):
"""
Form for writing feedback.
"""
feedback_msg = forms.CharField(widget=forms.TextInput(), label=u'Message')
feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
required=False)
......@@ -382,7 +370,7 @@ class ExtendedPasswordResetForm(PasswordResetForm):
def clean_email(self):
email = super(ExtendedPasswordResetForm, self).clean_email()
try:
user = AstakosUser.objects.get(email=email)
user = AstakosUser.objects.get(email=email, is_active=True)
if not user.has_usable_password():
raise forms.ValidationError(_("This account has not a usable password."))
except AstakosUser.DoesNotExist, e:
......
......@@ -178,7 +178,7 @@ def invite(invitation, inviter, email_template_name='im/welcome_email.txt'):
def set_user_credibility(email, has_credits):
try:
user = AstakosUser.objects.get(email=email)
user = AstakosUser.objects.get(email=email, is_active=True)
user.has_credits = has_credits
user.save()
except AstakosUser.DoesNotExist, e:
......
......@@ -38,16 +38,15 @@ from django.utils.timesince import timesince, timeuntil
from astakos.im.models import AstakosUser
def get_user(email_or_id):
def get_user(email_or_id, **kwargs):
try:
if email_or_id.isdigit():
return AstakosUser.objects.get(id=int(email_or_id))
else:
return AstakosUser.objects.get(email=email_or_id)
except AstakosUser.DoesNotExist:
return AstakosUser.objects.get(email=email_or_id, **kwargs)
except AstakosUser.DoesNotExist, AstakosUser.MultipleObjectsReturned:
return None
def format_bool(b):
return 'YES' if b else 'NO'
......
......@@ -43,6 +43,7 @@ from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from astakos.im.models import AstakosUser
from astakos.im.util import reserved_email
class Command(BaseCommand):
args = "<email> <first name> <last name> <affiliation>"
......@@ -82,11 +83,8 @@ class Command(BaseCommand):
if password is None:
password = AstakosUser.objects.make_random_password()
try:
AstakosUser.objects.get(email=email)
if reserved_email(email):
raise CommandError("A user with this email already exists")
except AstakosUser.DoesNotExist:
pass
user = AstakosUser(username=username, first_name=first, last_name=last,
email=email, affiliation=affiliation,
......
......@@ -51,9 +51,9 @@ class Command(BaseCommand):
if len(args) != 3:
raise CommandError("Invalid number of arguments")
inviter = get_user(args[0])
inviter = get_user(args[0], is_active=True)
if not inviter:
raise CommandError("Unknown inviter")
raise CommandError("Unknown or inactive inviter")
if inviter.invitations > 0:
email = args[1]
......
......@@ -35,12 +35,14 @@ from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from ._common import get_user
from astakos.im.models import AstakosUser
class Command(BaseCommand):
args = "<user ID or email>"
args = "<user ID>"
help = "Modify a user's attributes"
option_list = BaseCommand.option_list + (
......@@ -91,9 +93,13 @@ class Command(BaseCommand):
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("Please provide a user ID or email")
raise CommandError("Please provide a user ID")
if args[0].isdigit():
user = AstakosUser.objects.get(id=int( args[0]))
else:
raise CommandError("Invalid ID")
user = get_user(args[0])
if not user:
raise CommandError("Unknown user")
......@@ -138,4 +144,7 @@ class Command(BaseCommand):
if options['renew_token']:
user.renew_token()
user.save()
try:
user.save()
except ValidationError, e:
raise CommandError(e.message_dict)
......@@ -33,7 +33,7 @@
from django.core.management.base import BaseCommand, CommandError
from astakos.im.functions import send_verification
from astakos.im.functions import send_verification, SendMailError
from ._common import get_user
......@@ -46,14 +46,9 @@ class Command(BaseCommand):
raise CommandError("No user was given")
for email_or_id in args:
user = get_user(email_or_id)
user = get_user(email_or_id, is_active=False)
if not user:
self.stderr.write("Unknown user '%s'\n" % (email_or_id,))
continue
if user.is_active:
msg = "User '%s' already active\n" % (user.email,)
self.stderr.write(msg)
self.stderr.write("Unknown or already active user '%s'\n" % (email_or_id,))
continue
try:
......@@ -61,4 +56,4 @@ class Command(BaseCommand):
except SendMailError, e:
raise CommandError(e.message)
self.stdout.write("Activated '%s'\n" % (user.email,))
\ No newline at end of file
self.stdout.write("Activation sent to '%s'\n" % (user.email,))
\ No newline at end of file
......@@ -47,39 +47,41 @@ class Command(BaseCommand):
raise CommandError("Please provide a user ID or email")
email_or_id = args[0]
try:
if email_or_id.isdigit():
user = AstakosUser.objects.get(id=int(email_or_id))
else:
user = AstakosUser.objects.get(email=email_or_id)
except AstakosUser.DoesNotExist:
if email_or_id.isdigit():
users = AstakosUser.objects.filter(id=int(email_or_id))
else:
users = AstakosUser.objects.filter(email=email_or_id)
if users.count() == 0:
field = 'id' if email_or_id.isdigit() else 'email'
msg = "Unknown user with %s '%s'" % (field, email_or_id)
raise CommandError(msg)
kv = {
'id': user.id,
'email': user.email,