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

add support for groups

parent 84da84cb
......@@ -165,13 +165,13 @@ Example reply:
::
{"username": "4ad9f34d6e7a4992b34502d40f40cb",
"uniq": "papagian@example.com"
"auth_token": "0000",
"auth_token_expires": "Tue, 11-Sep-2012 09:17:14 ",
"auth_token_created": "Sun, 11-Sep-2011 09:17:14 ",
"has_credits": false,
"has_signed_terms": true}
{"userid": "270d191e09834408b7af65885f46a3",
"email": ["user111@example.com"],
"name": "user1 User1",
"auth_token_created": 1333372365000,
"auth_token_expires": 1335964365000,
"auth_token": "uiWDLAgtJOGW4mI4q9R/8w==",
"has_credits": true}
|
......
......@@ -83,13 +83,26 @@ Available as extensions to Django's command-line management utility:
=============== ===========================
Name Description
=============== ===========================
activateuser Activates one or more users
addgroup Add new group
addterms Add new approval terms
createuser Create a user
inviteuser Invite a user
listgroups List groups
listinvitations List invitations
listusers List users
modifyuser Modify a user's attributes
sendactivation Send activation email
showinvitation Show invitation info
showuser Show user info
addterms Add new approval terms
=============== ===========================
To update user credibility from the billing system (Aquarium), enable the queue, install snf-pithos-tools and use ``pithos-dispatcher``::
pithos-dispatcher --exchange=aquarium --callback=astakos.im.queue.listener.on_creditevent
Load groups:
------------
To set the initial user groups load the followind fixture:
snf-manage loaddata groups
......@@ -35,11 +35,11 @@ from django.utils.importlib import import_module
from django.core.exceptions import ImproperlyConfigured
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.contrib.sites.models import Site
from django.contrib import messages
from django.db import transaction
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.db import transaction
from urlparse import urljoin
......@@ -57,7 +57,7 @@ logger = logging.getLogger(__name__)
def get_backend(request):
"""
Returns an instance of a registration backend,
Returns an instance of an activation backend,
according to the INVITATIONS_ENABLED setting
(if True returns ``astakos.im.activation_backends.InvitationsBackend`` and if False
returns ``astakos.im.activation_backends.SimpleBackend``).
......@@ -71,11 +71,11 @@ def get_backend(request):
try:
mod = import_module(module)
except ImportError, e:
raise ImproperlyConfigured('Error loading registration backend %s: "%s"' % (module, e))
raise ImproperlyConfigured('Error loading activation backend %s: "%s"' % (module, e))
try:
backend_class = getattr(mod, backend_class_name)
except AttributeError:
raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
raise ImproperlyConfigured('Module "%s" does not define a activation backend named "%s"' % (module, attr))
return backend_class(request)
class SignupBackend(object):
......@@ -88,7 +88,7 @@ class SignupBackend(object):
class InvitationsBackend(SignupBackend):
"""
A registration backend which implements the following workflow: a user
A activation backend which implements the following workflow: a user
supplies the necessary registation information, if the request contains a valid
inivation code the user is automatically activated otherwise an inactive user
account is created and the user is going to receive an email as soon as an
......@@ -110,7 +110,7 @@ class InvitationsBackend(SignupBackend):
invitation = self.invitation
initial_data = self.get_signup_initial_data(provider)
prefix = 'Invited' if invitation else ''
main = provider.capitalize() if provider == 'local' else 'ThirdParty'
main = provider.capitalize()
suffix = 'UserCreationForm'
formclass = '%s%s%s' % (prefix, main, suffix)
ip = self.request.META.get('REMOTE_ADDR',
......@@ -119,7 +119,7 @@ class InvitationsBackend(SignupBackend):
def get_signup_initial_data(self, provider):
"""
Returns the necassary registration form depending the user is invited or not
Returns the necassary activation form depending the user is invited or not
Throws Invitation.DoesNotExist in case ``code`` is not valid.
"""
......@@ -131,6 +131,7 @@ class InvitationsBackend(SignupBackend):
# create a tmp user with the invitation realname
# to extract first and last name
u = AstakosUser(realname = invitation.realname)
print '>>>', invitation, invitation.inviter
initial_data = {'email':invitation.username,
'inviter':invitation.inviter.realname,
'first_name':u.first_name,
......@@ -155,7 +156,6 @@ class InvitationsBackend(SignupBackend):
return True
return False
@transaction.commit_manually
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
......@@ -166,26 +166,28 @@ 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.
"""
result = None
try:
if user.is_active:
return RegistationCompleted()
if self._is_preaccepted(user):
if user.email_verified:
activate(user, greeting_template_name)
result = RegistationCompleted()
return RegistationCompleted()
else:
send_verification(user, verification_template_name)
result = VerificationSent()
return VerificationSent()
else:
send_admin_notification(user, admin_email_template_name)
result = NotificationSent()
return NotificationSent()
except Invitation.DoesNotExist, e:
raise InvitationCodeError()
else:
return result
except BaseException, e:
logger.exception(e)
raise e
class SimpleBackend(SignupBackend):
"""
A registration backend which implements the following workflow: a user
A activation backend which implements the following workflow: a user
supplies the necessary registation information, an incative user account is
created and receives an email in order to activate his/her account.
"""
......@@ -238,14 +240,23 @@ class SimpleBackend(SignupBackend):
* DEFAULT_CONTACT_EMAIL: service support email
* DEFAULT_FROM_EMAIL: from email
"""
result = None
if not self._is_preaccepted(user):
send_admin_notification(user, admin_email_template_name)
result = NotificationSent()
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:
send_verification(user, email_template_name)
result = VerificationSend()
return result
transaction.commit()
class ActivationResult(object):
def __init__(self, message):
......@@ -266,6 +277,4 @@ class NotificationSent(ActivationResult):
class RegistationCompleted(ActivationResult):
def __init__(self):
message = _('Registration completed. You can now login.')
super(RegistationCompleted, self).__init__(message)
super(RegistationCompleted, self).__init__(message)
\ No newline at end of file
......@@ -33,6 +33,7 @@
import logging
from functools import wraps
from traceback import format_exc
from time import time, mktime
from urllib import quote
......@@ -43,10 +44,10 @@ from django.http import HttpResponse
from django.utils import simplejson as json
from django.core.urlresolvers import reverse
from astakos.im.faults import BadRequest, Unauthorized, InternalServerError
from astakos.im.faults import BadRequest, Unauthorized, InternalServerError, Fault
from astakos.im.models import AstakosUser
from astakos.im.settings import CLOUD_SERVICES, INVITATIONS_ENABLED
from astakos.im.util import has_signed_terms
from astakos.im.util import has_signed_terms, epoch
logger = logging.getLogger(__name__)
......@@ -62,56 +63,109 @@ def render_fault(request, fault):
response['Content-Length'] = len(response.content)
return response
def authenticate(request):
def api_method(http_method=None, token_required=False, perms=[]):
"""Decorator function for views that implement an API method."""
def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
try:
if http_method and request.method != http_method:
raise BadRequest('Method not allowed.')
x_auth_token = request.META.get('HTTP_X_AUTH_TOKEN')
if token_required:
if not x_auth_token:
raise Unauthorized('Access denied')
try:
user = AstakosUser.objects.get(auth_token=x_auth_token)
if not user.has_perms(perms):
raise Unauthorized('Unauthorized request')
except AstakosUser.DoesNotExist, e:
raise Unauthorized('Invalid X-Auth-Token')
kwargs['user'] = user
response = func(request, *args, **kwargs)
return response
except Fault, fault:
return render_fault(request, fault)
except BaseException, e:
logger.exception('Unexpected error: %s' % e)
fault = InternalServerError('Unexpected error')
return render_fault(request, fault)
return wrapper
return decorator
@api_method(http_method='GET', token_required=True)
def authenticate_old(request, user=None):
# Normal Response Codes: 204
# Error Response Codes: internalServerError (500)
# badRequest (400)
# unauthorised (401)
try:
if request.method != 'GET':
raise BadRequest('Method not allowed.')
x_auth_token = request.META.get('HTTP_X_AUTH_TOKEN')
if not x_auth_token:
return render_fault(request, BadRequest('Missing X-Auth-Token'))
try:
user = AstakosUser.objects.get(auth_token=x_auth_token)
except AstakosUser.DoesNotExist, e:
return render_fault(request, Unauthorized('Invalid X-Auth-Token'))
# Check if the is active.
if not user.is_active:
return render_fault(request, Unauthorized('User inactive'))
# Check if the token has expired.
if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
return render_fault(request, Unauthorized('Authentication expired'))
if not has_signed_terms(user):
return render_fault(request, Unauthorized('Pending approval terms'))
response = HttpResponse()
response.status=204
user_info = {'username':user.username,
'uniq':user.email,
'auth_token':user.auth_token,
'auth_token_created':user.auth_token_created.isoformat(),
'auth_token_expires':user.auth_token_expires.isoformat(),
'has_credits':user.has_credits,
'has_signed_terms':has_signed_terms(user)}
response.content = json.dumps(user_info)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
except BaseException, e:
logger.exception(e)
fault = InternalServerError('Unexpected error')
return render_fault(request, fault)
if not user:
raise BadRequest('No user')
# Check if the is active.
if not user.is_active:
raise Unauthorized('User inactive')
def get_services(request):
if request.method != 'GET':
raise BadRequest('Method not allowed.')
# Check if the token has expired.
if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
raise Unauthorized('Authentication expired')
if not has_signed_terms(user):
raise Unauthorized('Pending approval terms')
response = HttpResponse()
response.status=204
user_info = {'username':user.username,
'uniq':user.email,
'auth_token':user.auth_token,
'auth_token_created':user.auth_token_created.isoformat(),
'auth_token_expires':user.auth_token_expires.isoformat(),
'has_credits':user.has_credits,
'has_signed_terms':has_signed_terms(user)}
response.content = json.dumps(user_info)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
@api_method(http_method='GET', token_required=True)
def authenticate(request, user=None):
# Normal Response Codes: 204
# Error Response Codes: internalServerError (500)
# badRequest (400)
# unauthorised (401)
if not user:
raise BadRequest('No user')
# Check if the is active.
if not user.is_active:
raise Unauthorized('User inactive')
# Check if the token has expired.
if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
raise Unauthorized('Authentication expired')
if not has_signed_terms(user):
raise Unauthorized('Pending approval terms')
response = HttpResponse()
response.status=204
user_info = {'userid':user.username,
'email':[user.email],
'name':user.realname,
'auth_token':user.auth_token,
'auth_token_created':epoch(user.auth_token_created),
'auth_token_expires':epoch(user.auth_token_expires),
'has_credits':user.has_credits,
'is_active':user.is_active,
'groups':[g.name for g in user.groups.all()]}
response.content = json.dumps(user_info)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
@api_method(http_method='GET')
def get_services(request):
callback = request.GET.get('callback', None)
data = json.dumps(CLOUD_SERVICES)
mimetype = 'application/json'
......@@ -122,6 +176,7 @@ def get_services(request):
return HttpResponse(content=data, mimetype=mimetype)
@api_method()
def get_menu(request, with_extra_links=False, with_signout=True):
location = request.GET.get('location', '')
exclude = []
......@@ -144,7 +199,7 @@ def get_menu(request, with_extra_links=False, with_signout=True):
l.append({ 'url': absolute(reverse('astakos.im.views.edit_profile')),
'name': "My account" })
if with_extra_links:
if request.user.password:
if request.user.has_usable_password():
l.append({ 'url': absolute(reverse('password_change')),
'name': "Change password" })
if INVITATIONS_ENABLED:
......@@ -165,3 +220,47 @@ def get_menu(request, with_extra_links=False, with_signout=True):
data = '%s(%s)' % (callback, data)
return HttpResponse(content=data, mimetype=mimetype)
@api_method(http_method='GET', token_required=True, perms=['astakos.im.can_find_userid'])
def find_userid(request):
# Normal Response Codes: 204
# Error Response Codes: internalServerError (500)
# badRequest (400)
# unauthorised (401)
email = request.GET.get('email')
if not email:
raise BadRequest('Email missing')
try:
user = AstakosUser.objects.get(email = email)
except AstakosUser.DoesNotExist, e:
raise BadRequest('Invalid email')
else:
response = HttpResponse()
response.status=204
user_info = {'userid':user.username}
response.content = json.dumps(user_info)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
@api_method(http_method='GET', token_required=True, perms=['astakos.im.can_find_email'])
def find_email(request):
# Normal Response Codes: 204
# Error Response Codes: internalServerError (500)
# badRequest (400)
# unauthorised (401)
userid = request.GET.get('userid')
if not userid:
raise BadRequest('Userid missing')
try:
user = AstakosUser.objects.get(username = userid)
except AstakosUser.DoesNotExist, e:
raise BadRequest('Invalid userid')
else:
response = HttpResponse()
response.status=204
user_info = {'userid':user.email}
response.content = json.dumps(user_info)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
[
{
"model": "auth.group",
"pk": 1,
"fields": {
"name": "default"
}
},
{
"model": "auth.group",
"pk": 2,
"fields": {
"name": "academic"
}
},
{
"model": "auth.group",
"pk": 3,
"fields": {
"name": "shibboleth"
}
}
]
# 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
......@@ -25,7 +25,7 @@
# 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
......@@ -42,6 +42,7 @@ from django.template import Context, loader
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 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
......@@ -58,19 +59,19 @@ logger = logging.getLogger(__name__)
class LocalUserCreationForm(UserCreationForm):
"""
Extends the built in UserCreationForm in several ways:
* Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
* The username field isn't visible and it is assigned a generated id.
* User created is not active.
* User created is not active.
"""
recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
class Meta:
model = AstakosUser
fields = ("email", "first_name", "last_name", "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.
......@@ -86,7 +87,15 @@ class LocalUserCreationForm(UserCreationForm):
if RECAPTCHA_ENABLED:
self.fields.keyOrder.extend(['recaptcha_challenge_field',
'recaptcha_response_field',])
if 'has_signed_terms' in self.fields:
# Overriding field label since we need to apply a link
# to the terms within the label
terms_link_html = '<a href="%s" target="_blank">%s</a>' \
% (reverse('latest_terms'), _("the terms"))
self.fields['has_signed_terms'].label = \
mark_safe("I agree with %s" % terms_link_html)
def clean_email(self):
email = self.cleaned_data['email']
if not email:
......@@ -96,13 +105,13 @@ class LocalUserCreationForm(UserCreationForm):
raise forms.ValidationError(_("This email is already used"))
except AstakosUser.DoesNotExist:
return email
def clean_has_signed_terms(self):
has_signed_terms = self.cleaned_data['has_signed_terms']
if not has_signed_terms:
raise forms.ValidationError(_('You have to agree with the terms'))
return has_signed_terms
def clean_recaptcha_response_field(self):
if 'recaptcha_challenge_field' in self.cleaned_data:
self.validate_captcha()
......@@ -119,7 +128,7 @@ class LocalUserCreationForm(UserCreationForm):
check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
if not check.is_valid:
raise forms.ValidationError(_('You have not entered the correct words'))
def save(self, commit=True):
"""
Saves the email, first_name and last_name properties, after the normal
......@@ -127,7 +136,6 @@ class LocalUserCreationForm(UserCreationForm):
"""
user = super(LocalUserCreationForm, self).save(commit=False)
user.renew_token()
user.date_signed_terms = datetime.now()
if commit:
user.save()
logger.info('Created user %s', user)
......@@ -137,25 +145,25 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
"""
Extends the LocalUserCreationForm: adds an inviter readonly field.
"""
inviter = forms.CharField(widget=forms.TextInput(), label=_('Inviter Real Name'))
class Meta:
model = AstakosUser
fields = ("email", "first_name", "last_name", "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.
"""
super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
#set readonly form fields
self.fields['inviter'].widget.attrs['readonly'] = True
self.fields['email'].widget.attrs['readonly'] = True
self.fields['username'].widget.attrs['readonly'] = True
def save(self, commit=True):
user = super(InvitedLocalUserCreationForm, self).save(commit=False)
level = user.invitation.inviter.level + 1
......@@ -166,10 +174,11 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
user.save()
return user
class ThirdPartyUserCreationForm(UserCreationForm):