Commit 21bbd25b authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

change authentication methods: progress I

parent 4d65a5a1
......@@ -9,7 +9,6 @@ Astakos serves as the point of authentication for GRNET (http://www.grnet.gr) se
Users in astakos can be authenticated via several identity providers:
* Local
* Twitter
* Shibboleth
It provides also a command line tool for managing user accounts.
......@@ -51,14 +50,14 @@ The following subsections describe two basic registration use cases. All the reg
Invited user
^^^^^^^^^^^^
A registered ~okeanos user, invites student Alice to subscribe to ~okeanos services. Alice receives an email and through a link is navigated to Astakos's signup page. The system prompts her to select one of the available authentication mechanisms (Shibboleth, Twitter or local authentication) in order to register to the system. Alice already has a Shibboleth account so chooses that and then she is redirected to her institution's login page. Upon successful login, her account is created.
A registered ~okeanos user, invites student Alice to subscribe to ~okeanos services. Alice receives an email and through a link is navigated to Astakos's signup page. The system prompts her to select one of the available authentication mechanisms (Shibboleth or local authentication) in order to register to the system. Alice already has a Shibboleth account so chooses that and then she is redirected to her institution's login page. Upon successful login, her account is created.
Since she is invited his account is automaticaly activated and she is redirected to Astakos's login page. As this is the first time Alice has accessed the system she is redirected to her profile page where she can edit or provide more information.
Not invited user
^^^^^^^^^^^^^^^^
Tony while browsing in the internet finds out about ~okeanos services. He visits the signup page and since his has already a twitter account selects the twitter authentication mechanism and he is redirected to twitter login page where he is promted to provide his credentials. Upon successful login, twitter redirects him back to the Astakos and the account is created.
Tony while browsing in the internet finds out about ~okeanos services. He visits the signup page and since his has not a shibboleth account selects the local authentication mechanism. Upon successful signup the account is created.
Since his not an invited user his account has to be activated from an administrator first, in order to be able to login. Upon the account's activation he receives an email and through a link he is redirected to the login page.
......@@ -99,7 +98,7 @@ Logged on users can perform a number of actions:
* access and edit their profile via: ``/im/profile``.
* change their password via: ``/im/password``
* invite somebody else via: ``/im/invite``
* send feedback for grnet services via: ``/im/send_feedback``
* send feedback for grnet services via: ``/im/feedback``
* logout (and delete cookie) via: ``/im/logout``
User entries can also be modified/added via the ``snf-manage activateuser`` command.
......
docs/source/images/signup.jpg

120 KB | W: | H:

docs/source/images/signup.jpg

143 KB | W: | H:

docs/source/images/signup.jpg
docs/source/images/signup.jpg
docs/source/images/signup.jpg
docs/source/images/signup.jpg
  • 2-up
  • Swipe
  • Onion skin
......@@ -44,14 +44,12 @@ Configure in ``settings.py`` or a ``.conf`` file in ``/etc/synnefo`` if using sn
Name Default value Description
============================== ============================================================================= ===========================================================================================
ASTAKOS_AUTH_TOKEN_DURATION one month Expiration time of newly created auth tokens
ASTAKOS_TWITTER_KEY Twitter ``oauth_token``
ASTAKOS_TWITTER_SECRET Twitter ``oauth_token_secret``
ASTAKOS_DEFAULT_USER_LEVEL 4 Default (not-invited) user level
ASTAKOS_INVITATIONS_PER_LEVEL {0:100, 1:2, 2:0, 3:0, 4:0} Number of user invitations per user level
ASTAKOS_DEFAULT_FROM_EMAIL GRNET Cloud <no-reply\@grnet.gr> ``from`` parameter passed in ``django.core.mail.send_mail``
ASTAKOS_DEFAULT_CONTACT_EMAIL support\@cloud.grnet.gr Contact email
ASTAKOS_DEFAULT_ADMIN_EMAIL support\@cloud.grnet.gr Administrator email to receive user creation notifications (if None disables notifications)
ASTAKOS_IM_MODULES ['local', 'twitter', 'shibboleth'] Signup modules
ASTAKOS_IM_MODULES ['local', 'shibboleth'] Signup modules
ASTAKOS_FORCE_PROFILE_UPDATE True Force user profile verification
ASTAKOS_INVITATIONS_ENABLED True Enable invitations
ASTAKOS_COOKIE_NAME _pithos2_a ``Key`` parameter passed in ``django.http.HttpResponse.set_cookie``
......
......@@ -41,13 +41,12 @@ from django.contrib import messages
from django.db import transaction
from django.core.urlresolvers import reverse
from smtplib import SMTPException
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_notification, activate
from astakos.im.functions import send_verification, send_admin_notification, activate
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
import socket
......@@ -60,13 +59,13 @@ def get_backend(request):
"""
Returns an instance of a registration backend,
according to the INVITATIONS_ENABLED setting
(if True returns ``astakos.im.backends.InvitationsBackend`` and if False
returns ``astakos.im.backends.SimpleBackend``).
(if True returns ``astakos.im.activation_backends.InvitationsBackend`` and if False
returns ``astakos.im.activation_backends.SimpleBackend``).
If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
is raised.
"""
module = 'astakos.im.backends'
module = 'astakos.im.activation_backends'
prefix = 'Invitations' if INVITATIONS_ENABLED else 'Simple'
backend_class_name = '%sBackend' %prefix
try:
......@@ -104,7 +103,7 @@ class InvitationsBackend(SignupBackend):
self.invitation = get_invitation(request)
super(InvitationsBackend, self).__init__()
def get_signup_form(self, provider):
def get_signup_form(self, provider='local'):
"""
Returns the form class name
"""
......@@ -157,7 +156,7 @@ class InvitationsBackend(SignupBackend):
return False
@transaction.commit_manually
def signup(self, form, verification_template_name='im/activation_email.txt', greeting_template_name='im/welcome_email.txt', admin_email_template_name='im/admin_notification.txt'):
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
......@@ -167,45 +166,22 @@ class InvitationsBackend(SignupBackend):
The method uses commit_manually decorator in order to ensure the user
will be created only if the procedure has been completed successfully.
"""
user = None
result = None
try:
user = form.save()
if self._is_preaccepted(user):
if user.email_verified:
try:
activate(user, greeting_template_name)
message = _('Registration completed. You can now login.')
except (SMTPException, socket.error) as e:
status = messages.ERROR
name = 'strerror'
message = getattr(e, 'name') if hasattr(e, 'name') else e
activate(user, greeting_template_name)
result = RegistationCompleted()
else:
try:
send_verification(user, verification_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
send_verification(user, verification_template_name)
result = VerificationSent()
else:
send_notification(user, admin_email_template_name)
message = _('Your request for an account was successfully received and is now pending \
approval. You will be notified by email in the next few days. Thanks for \
your interest in ~okeanos! The GRNET team.')
status = messages.SUCCESS
send_admin_notification(user, admin_email_template_name)
result = NotificationSent()
except Invitation.DoesNotExist, e:
status = messages.ERROR
message = _('Invalid invitation code')
except socket.error, e:
status = messages.ERROR
message = _(e.strerror)
# rollback in case of error
if status == messages.ERROR:
transaction.rollback()
raise InvitationCodeError()
else:
transaction.commit()
return status, message, user
return result
class SimpleBackend(SignupBackend):
"""
......@@ -217,7 +193,7 @@ class SimpleBackend(SignupBackend):
self.request = request
super(SimpleBackend, self).__init__()
def get_signup_form(self, provider):
def get_signup_form(self, provider='local'):
"""
Returns the form class name
"""
......@@ -240,8 +216,7 @@ class SimpleBackend(SignupBackend):
return False
return True
@transaction.commit_manually
def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
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.
......@@ -263,30 +238,34 @@ class SimpleBackend(SignupBackend):
* DEFAULT_CONTACT_EMAIL: service support email
* DEFAULT_FROM_EMAIL: from email
"""
user = form.save()
status = messages.SUCCESS
result = None
if not self._is_preaccepted(user):
try:
send_notification(user, admin_email_template_name)
message = _('Your request for an account was successfully received and is now pending \
approval. You will be notified by email in the next few days. Thanks for \
your interest in ~okeanos! The GRNET team.')
except (SMTPException, socket.error) as e:
status = messages.ERROR
name = 'strerror'
message = getattr(e, 'name') if hasattr(e, 'name') else e
send_admin_notification(user, admin_email_template_name)
result = NotificationSent()
else:
try:
send_verification(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
send_verification(user, email_template_name)
result = VerificationSend()
return result
class ActivationResult(object):
def __init__(self, message):
self.message = message
class VerificationSent(ActivationResult):
def __init__(self):
message = _('Verification sent.')
super(VerificationSent, self).__init__(message)
class NotificationSent(ActivationResult):
def __init__(self):
message = _('Your request for an account was successfully received and is now pending \
approval. You will be notified by email in the next few days. Thanks for \
your interest in ~okeanos! The GRNET team.')
super(NotificationSent, self).__init__(message)
class RegistationCompleted(ActivationResult):
def __init__(self):
message = _('Registration completed. You can now login.')
super(RegistationCompleted, self).__init__(message)
# rollback in case of error
if status == messages.ERROR:
transaction.rollback()
else:
transaction.commit()
return status, message, user
......@@ -150,7 +150,7 @@ def get_menu(request, with_extra_links=False, with_signout=True):
if INVITATIONS_ENABLED:
l.append({ 'url': absolute(reverse('astakos.im.views.invite')),
'name': "Invitations" })
l.append({ 'url': absolute(reverse('astakos.im.views.send_feedback')),
l.append({ 'url': absolute(reverse('astakos.im.views.feedback')),
'name': "Feedback" })
if with_signout:
l.append({ 'url': absolute(reverse('astakos.im.views.logout')),
......
......@@ -43,7 +43,7 @@ from django.utils.http import int_to_base36
from django.core.urlresolvers import reverse
from django.utils.functional import lazy
from astakos.im.models import AstakosUser
from astakos.im.models import AstakosUser, Invitation
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED
from astakos.im.widgets import DummyWidget, RecaptchaWidget, ApprovalTermsWidget
......@@ -166,6 +166,38 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
user.save()
return user
class ThirdPartyUserCreationForm(UserCreationForm):
class Meta:
model = AstakosUser
fields = ("email", "has_signed_terms")
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.
"""
if 'ip' in kwargs:
self.ip = kwargs['ip']
kwargs.pop('ip')
super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email']
if get_latest_terms():
self.fields.keyOrder.append('has_signed_terms')
def save(self, commit=True):
user = super(ThirdPartyUserCreationForm, self).save(commit=False)
user.set_unusable_password()
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 LoginForm(AuthenticationForm):
username = forms.EmailField(label=_("Email"))
......@@ -200,51 +232,13 @@ 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):
if 'ip' in kwargs:
self.ip = kwargs['ip']
kwargs.pop('ip')
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(_("This email is already used"))
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()
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 FeedbackForm(forms.Form):
"""
Form for writing feedback.
"""
feedback_msg = forms.CharField(widget=forms.Textarea(),
label=u'Message', required=False)
feedback_data = forms.CharField(widget=forms.HiddenInput(),
label='', required=False)
feedback_msg = forms.CharField(widget=forms.TextInput(), label=u'Message')
feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
required=False)
class SendInvitationForm(forms.Form):
"""
......@@ -301,11 +295,26 @@ class SignApprovalTermsForm(forms.ModelForm):
def save(self, commit=True):
"""
Saves the , after the normal
save behavior is complete.
Updates date_signed_terms & has_signed_terms fields.
"""
user = super(SignApprovalTermsForm, self).save(commit=False)
user.date_signed_terms = datetime.now()
if commit:
user.save()
return user
\ No newline at end of file
return user
class InvitationForm(forms.ModelForm):
username = forms.EmailField(label=_("Email"))
class Meta:
model = Invitation
fields = ('username', 'realname')
def clean_username(self):
username = self.cleaned_data['username']
try:
Invitation.objects.get(username = username)
raise forms.ValidationError(_('There is already invitation for this email.'))
except Invitation.DoesNotExist:
pass
return username
\ No newline at end of file
......@@ -32,6 +32,7 @@
# or implied, of GRNET S.A.
import logging
import socket
from django.utils.translation import ugettext as _
from django.template.loader import render_to_string
......@@ -39,7 +40,7 @@ from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from urllib import quote
from urlparse import urljoin
from random import randint
from smtplib import SMTPException
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL
from astakos.im.models import Invitation, AstakosUser
......@@ -50,7 +51,7 @@ def send_verification(user, template_name='im/activation_email.txt'):
"""
Send email to user to verify his/her email and activate his/her account.
Raises SMTPException, socket.error
Raises SendVerificationError
"""
url = '%s?auth=%s&next=%s' % (urljoin(BASEURL, reverse('astakos.im.views.activate')),
quote(user.auth_token),
......@@ -62,14 +63,19 @@ def send_verification(user, template_name='im/activation_email.txt'):
'site_name': SITENAME,
'support': DEFAULT_CONTACT_EMAIL})
sender = DEFAULT_FROM_EMAIL
send_mail('%s alpha2 testing account activation is needed' % SITENAME, message, sender, [user.email])
logger.info('Sent activation %s', user)
try:
send_mail('%s alpha2 testing account activation is needed' % SITENAME, message, sender, [user.email])
except (SMTPException, socket.error) as e:
logger.exception(e)
raise SendVerificationError()
else:
logger.info('Sent activation %s', user)
def send_notification(user, template_name='im/admin_notification.txt'):
def send_admin_notification(user, template_name='im/admin_notification.txt'):
"""
Send email to DEFAULT_ADMIN_EMAIL to notify for a new user registration.
Raises SMTPException, socket.error
Raises SendNotificationError
"""
if not DEFAULT_ADMIN_EMAIL:
return
......@@ -79,14 +85,19 @@ def send_notification(user, template_name='im/admin_notification.txt'):
'site_name': SITENAME,
'support': DEFAULT_CONTACT_EMAIL})
sender = DEFAULT_FROM_EMAIL
send_mail('%s alpha2 testing account notification' % SITENAME, message, sender, [DEFAULT_ADMIN_EMAIL])
logger.info('Sent admin notification for user %s', user)
try:
send_mail('%s alpha2 testing account notification' % SITENAME, message, sender, [DEFAULT_ADMIN_EMAIL])
except (SMTPException, socket.error) as e:
logger.exception(e)
raise SendNotificationError()
else:
logger.info('Sent admin notification for user %s', user)
def send_invitation(invitation, template_name='im/invitation.txt'):
"""
Send invitation email.
Raises SMTPException, socket.error
Raises SendInvitationError
"""
subject = _('Invitation to %s alpha2 testing' % SITENAME)
url = '%s?code=%d' % (urljoin(BASEURL, reverse('astakos.im.views.signup')), invitation.code)
......@@ -97,8 +108,13 @@ def send_invitation(invitation, template_name='im/invitation.txt'):
'site_name': SITENAME,
'support': DEFAULT_CONTACT_EMAIL})
sender = DEFAULT_FROM_EMAIL
send_mail(subject, message, sender, [invitation.username])
logger.info('Sent invitation %s', invitation)
try:
send_mail(subject, message, sender, [invitation.username])
except (SMTPException, socket.error) as e:
logger.exception(e)
raise SendInvitationError()
else:
logger.info('Sent invitation %s', invitation)
def send_greeting(user, email_template_name='im/welcome_email.txt'):
"""
......@@ -114,40 +130,46 @@ def send_greeting(user, email_template_name='im/welcome_email.txt'):
'site_name': SITENAME,
'support': DEFAULT_CONTACT_EMAIL})
sender = DEFAULT_FROM_EMAIL
send_mail(subject, message, sender, [user.email])
logger.info('Sent greeting %s', user)
try:
send_mail(subject, message, sender, [user.email])
except (SMTPException, socket.error) as e:
logger.exception(e)
raise SendGreetingError()
else:
logger.info('Sent greeting %s', user)
def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
subject = _("Feedback from %s alpha2 testing" % SITENAME)
from_email = user.email
recipient_list = [DEFAULT_CONTACT_EMAIL]
content = render_to_string(email_template_name, {
'message': msg,
'data': data,
'user': user})
try:
send_mail(subject, content, from_email, recipient_list)
except (SMTPException, socket.error) as e:
logger.exception(e)
raise SendFeedbackError()
else:
logger.info('Sent feedback from %s', user.email)
def activate(user, email_template_name='im/welcome_email.txt'):
"""
Activates the specific user and sends email.
Raises SMTPException, socket.error
Raises SendGreetingError
"""
user.is_active = True
user.save()
send_greeting(user, email_template_name)
def _generate_invitation_code():
while True:
code = randint(1, 2L**63 - 1)
try:
Invitation.objects.get(code=code)
# An invitation with this code already exists, try again
except Invitation.DoesNotExist:
return code
def invite(inviter, username, realname, email_template_name='im/welcome_email.txt'):
def invite(invitation, inviter, email_template_name='im/welcome_email.txt'):
"""
Send an invitation email and upon success reduces inviter's invitation by one.
Raises SMTPException, socket.error
Raises SendInvitationError
"""
code = _generate_invitation_code()
invitation = Invitation(inviter=inviter,
username=username,
code=code,
realname=realname)
invitation.save()
send_invitation(invitation, email_template_name)
inviter.invitations = max(0, inviter.invitations - 1)
inviter.save()
......@@ -159,3 +181,32 @@ def set_user_credibility(email, has_credits):
user.save()
except AstakosUser.DoesNotExist, e:
logger.exception(e)
class SendMailError(Exception):
def __init__(self, message):
Exception.__init__(self)
class SendAdminNotificationError(SendMailError):
def __init__(self):
self.message = _('Failed to send notification')
SendMailError.__init__(self)
class SendVerificationError(Exception):
def __init__(self):
self.message = _('Failed to send verification')
SendMailError.__init__(self)
class SendInvitationError(Exception):
def __init__(self):
self.message = _('Failed to send invitation')
SendMailError.__init__(self)
class SendGreetingError(Exception):
def __init__(self):
self.message = _('Failed to send greeting')
SendMailError.__init__(self)
class SendFeedbackError(Exception):
def __init__(self):
self.message = _('Failed to send feedback')
SendMailError.__init__(self)
\ No newline at end of file
......@@ -42,12 +42,6 @@ from django.core.exceptions import ValidationError
from astakos.im.models import AstakosUser
def generate_password():
pool = lowercase + uppercase + digits
return ''.join(choice(pool) for i in range(10))
class Command(BaseCommand):
args = "<email> <first name> <last name> <affiliation>"
help = "Create a user"
......@@ -84,7 +78,7 @@ class Command(BaseCommand):
username = uuid4().hex[:30]
password = options.get('password')
if password is None:
password = generate_password()
password = AstakosUser.objects.make_random_password()
try:
AstakosUser.objects.get(email=email)
......
......@@ -33,12 +33,10 @@
import socket
from smtplib import SMTPException
from django.core.management.base import BaseCommand, CommandError
from django.db.utils import IntegrityError
from astakos.im.functions import invite
from astakos.im.functions import invite, SendMailError
from ._common import get_user
......@@ -60,10 +58,11 @@ class Command(BaseCommand):
realname = args[2]
try: