Commit 5f307667 authored by Giorgos Korfiatis's avatar Giorgos Korfiatis
Browse files

astakos: Refactor user creation

Provide auth.make_user() as the single way to create a new user. This
function is responsible to set all automatically generated fields, such as
username, uuid, and token. Clean up AstakosUser.save(), that used to
update these fields, and remove AstakosUser.__init__(). Remove trigger
that renewed token on every AstakosUser update. In order to set a user's
email, use AstakosUser.set_email(); this takes care to update the
username, too.

Provide function create_user() in user creation forms, which calls
auth.make_user() with the form-provided data.

Use the wrapper auth.make_local_user() in management command `user-add'.

Use the same infrastructure to extend a django superuser to an AstakosUser
(in management command `fix-superusers').
parent 2deff05a
......@@ -90,9 +90,7 @@ class ActivationBackend(object):
>>> backend = get_backend()
>>> formCls = backend.get_signup_form(request.POST)
>>> if form.is_valid():
>>> user = form.save(commit=False)
>>> # this creates auth provider objects
>>> form.store_user(user)
>>> user = form.create_user()
>>> activation = backend.handle_registration(user)
>>> # activation.status is one of backend.Result.{*} activation result
>>> # types
......
# Copyright 2013 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 uuid
import datetime
from astakos.im import models
def _finalize_astakosuser_object(user, has_signed_terms=False):
user.fix_username()
if has_signed_terms:
user.has_signed_terms = True
user.date_signed_terms = datetime.datetime.now()
user.renew_verification_code()
user.uuid = str(uuid.uuid4())
user.renew_token()
user.save()
def set_local_auth(user):
user.add_auth_provider('local', auth_backend='astakos')
def make_user(email, first_name="", last_name="", password=None,
has_signed_terms=False):
# delete previously unverified accounts
models.AstakosUser.objects.unverified_namesakes(email).delete()
user = models.AstakosUser(
email=email, first_name=first_name, last_name=last_name,
is_active=False)
if password is None:
user.set_unusable_password()
else:
user.set_password(password)
user.date_joined = datetime.datetime.now()
_finalize_astakosuser_object(user, has_signed_terms)
return user
def make_local_user(email, **kwargs):
user = make_user(email, **kwargs)
set_local_auth(user)
return user
def extend_superuser(user):
extended_user = models.AstakosUser(user_ptr_id=user.pk)
extended_user.__dict__.update(user.__dict__)
_finalize_astakosuser_object(extended_user, has_signed_terms=True)
set_local_auth(extended_user)
return extended_user
def fix_superusers():
admins = models.User.objects.filter(is_superuser=True)
fixed = []
for u in admins:
try:
models.AstakosUser.objects.get(user_ptr=u.pk)
except models.AstakosUser.DoesNotExist:
fixed.append(extend_superuser(u))
return fixed
# Copyright 2011-2012 GRNET S.A. All rights reserved.
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
......@@ -58,6 +58,7 @@ from astakos.im.functions import send_change_email, submit_application, \
from astakos.im.util import reserved_verified_email, model_to_dict
from astakos.im import auth_providers
from astakos.im import settings
from astakos.im import auth
import astakos.im.messages as astakos_messages
......@@ -73,25 +74,7 @@ DOMAIN_VALUE_REGEX = re.compile(
re.IGNORECASE)
class StoreUserMixin(object):
def store_user(self, user, request=None):
"""
WARNING: this should be wrapped inside a transactional view/method.
"""
user.save()
self.post_store_user(user, request)
return user
def post_store_user(self, user, request):
"""
Interface method for descendant backends to be able to do stuff within
the transaction enabled by store_user.
"""
pass
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
class LocalUserCreationForm(UserCreationForm):
"""
Extends the built in UserCreationForm in several ways:
......@@ -177,30 +160,21 @@ class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
raise forms.ValidationError(_(
astakos_messages.CAPTCHA_VALIDATION_ERR))
def post_store_user(self, user, request=None):
"""
Interface method for descendant backends to be able to do stuff within
the transaction enabled by store_user.
"""
user.add_auth_provider('local', auth_backend='astakos')
user.set_password(self.cleaned_data['password1'])
def create_user(self):
try:
data = self.cleaned_data
except AttributeError:
self.is_valid()
data = self.cleaned_data
def save(self, commit=True, **kwargs):
"""
Saves the email, first_name and last_name properties, after the normal
save behavior is complete.
"""
user = super(LocalUserCreationForm, self).save(commit=False, **kwargs)
user.has_signed_terms = True
user.date_signed_terms = datetime.now()
user.renew_token()
if commit:
user.save(**kwargs)
logger.info('Created user %s', user.log_display)
user = auth.make_local_user(
email=data['email'], password=data['password1'],
first_name=data['first_name'], last_name=data['last_name'],
has_signed_terms=True)
return user
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
class ThirdPartyUserCreationForm(forms.ModelForm):
email = forms.EmailField(
label='Contact email',
help_text='This is needed for contact purposes. '
......@@ -264,22 +238,21 @@ class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
def _get_pending_user(self):
return PendingThirdPartyUser.objects.get(token=self.third_party_token)
def post_store_user(self, user, request=None):
def create_user(self):
try:
data = self.cleaned_data
except AttributeError:
self.is_valid()
data = self.cleaned_data
user = auth.make_user(
email=data["email"],
first_name=data["first_name"], last_name=data["last_name"],
has_signed_terms=True)
pending = self._get_pending_user()
provider = pending.get_provider(user)
provider.add_to_user()
pending.delete()
def save(self, commit=True, **kwargs):
user = super(ThirdPartyUserCreationForm, self).save(commit=False,
**kwargs)
user.set_unusable_password()
user.renew_token()
user.has_signed_terms = True
user.date_signed_terms = datetime.now()
if commit:
user.save(**kwargs)
logger.info('Created user %s' % user.log_display)
return user
......
......@@ -34,16 +34,23 @@
from optparse import make_option
from datetime import datetime
from django.db import transaction
from django.core.management.base import NoArgsCommand, CommandError
from astakos.im.models import fix_superusers
from astakos.im.auth import fix_superusers
class Command(NoArgsCommand):
help = "Transform superusers created by syncdb into AstakosUser instances"
@transaction.commit_on_success
def handle(self, **options):
try:
fix_superusers()
fixed = fix_superusers()
count = len(fixed)
if count != 0:
self.stdout.write("Fixed %s superuser(s).\n" % count)
else:
self.stdout.write("No superuser needed a fix.\n")
except BaseException, e:
raise CommandError(e)
# Copyright 2012 GRNET S.A. All rights reserved.
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
......@@ -40,6 +40,7 @@ from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from astakos.im.models import AstakosUser, get_latest_terms
from astakos.im.auth import make_local_user
class Command(BaseCommand):
......@@ -84,22 +85,15 @@ class Command(BaseCommand):
except ValidationError:
raise CommandError("Invalid email")
if get_latest_terms() is not None:
has_signed_terms = False
date_signed_terms = None
else:
has_signed_terms = True
date_signed_terms = datetime.now()
has_signed_terms = not(get_latest_terms())
try:
u = AstakosUser(email=email,
first_name=first_name,
last_name=last_name,
has_signed_terms=has_signed_terms,
date_signed_terms=date_signed_terms,
is_superuser=options['is_superuser'])
u.set_password(password)
u.save()
user = make_local_user(
email, first_name=first_name, last_name=last_name,
password=password, has_signed_terms=has_signed_terms)
if options['is_superuser']:
user.is_superuser = True
user.save()
except BaseException, e:
raise CommandError(e)
......@@ -111,8 +105,7 @@ class Command(BaseCommand):
self.stdout.write('\n')
try:
u.add_auth_provider('local')
map(u.add_permission, options['permissions'])
map(u.add_group, options['groups'])
map(user.add_permission, options['permissions'])
map(user.add_group, options['groups'])
except BaseException, e:
raise CommandError(e)
......@@ -361,7 +361,7 @@ class Command(BaseCommand):
m = "A user with this email address already exists."
raise CommandError(m)
user.email = newemail
user.set_email(newemail)
user.save()
def confirm(self):
......
......@@ -337,6 +337,11 @@ class AstakosUserManager(UserManager):
qextra = Q(**kwargs)
return self.filter((qemail | qusername) & qextra).exists()
def unverified_namesakes(self, email_or_username):
q = Q(email__iexact=email_or_username)
q |= Q(username__iexact=email_or_username)
return self.filter(q & Q(email_verified=False))
def verified_user_exists(self, email_or_username):
return self.user_exists(email_or_username, email_verified=True)
......@@ -465,11 +470,6 @@ class AstakosUser(User):
objects = AstakosUserManager()
def __init__(self, *args, **kwargs):
super(AstakosUser, self).__init__(*args, **kwargs)
if not self.id:
self.is_active = False
@property
def realname(self):
return '%s %s' % (self.first_name, self.last_name)
......@@ -529,30 +529,17 @@ class AstakosUser(User):
return AstakosUserQuota.objects.select_related("resource").\
get(user=self, resource__name=resource)
def update_uuid(self):
while not self.uuid:
uuid_val = str(uuid.uuid4())
try:
AstakosUser.objects.get(uuid=uuid_val)
except AstakosUser.DoesNotExist:
self.uuid = uuid_val
return self.uuid
def fix_username(self):
self.username = self.email.lower()
def set_email(self, email):
self.email = email
self.fix_username()
def save(self, update_timestamps=True, **kwargs):
if update_timestamps:
if not self.id:
self.date_joined = datetime.now()
self.updated = datetime.now()
self.update_uuid()
if not self.verification_code:
self.renew_verification_code()
# username currently matches email
if self.username != self.email.lower():
self.username = self.email.lower()
super(AstakosUser, self).save(**kwargs)
def renew_verification_code(self):
......@@ -1093,7 +1080,7 @@ class EmailChangeManager(models.Manager):
# update user
user = AstakosUser.objects.get(pk=email_change.user_id)
old_email = user.email
user.email = email_change.new_email_address
user.set_email(email_change.new_email_address)
user.save()
email_change.delete()
msg = "User %s changed email from %s to %s"
......@@ -1987,39 +1974,6 @@ class ProjectMembershipLog(models.Model):
### SIGNALS ###
################
def create_astakos_user(u):
try:
AstakosUser.objects.get(user_ptr=u.pk)
except AstakosUser.DoesNotExist:
extended_user = AstakosUser(user_ptr_id=u.pk)
extended_user.__dict__.update(u.__dict__)
extended_user.save()
if not extended_user.has_auth_provider('local'):
extended_user.add_auth_provider('local')
except BaseException, e:
logger.exception(e)
def fix_superusers():
# Associate superusers with AstakosUser
admins = User.objects.filter(is_superuser=True)
for u in admins:
create_astakos_user(u)
def user_post_save(sender, instance, created, **kwargs):
if not created:
return
create_astakos_user(instance)
post_save.connect(user_post_save, sender=User)
def astakosuser_post_save(sender, instance, created, **kwargs):
pass
post_save.connect(astakosuser_post_save, sender=AstakosUser)
def resource_post_save(sender, instance, created, **kwargs):
pass
......@@ -2029,5 +1983,4 @@ post_save.connect(resource_post_save, sender=Resource)
def renew_token(sender, instance, **kwargs):
if not instance.auth_token:
instance.renew_token()
pre_save.connect(renew_token, sender=AstakosUser)
pre_save.connect(renew_token, sender=Component)
......@@ -408,16 +408,14 @@ class TokensApiTest(TestCase):
def setUp(self):
backend = activation_backends.get_backend()
self.user1 = AstakosUser.objects.create(
email='test1', email_verified=True, moderated=True,
has_signed_terms=True,
self.user1 = get_local_user(
'test1@example.org', email_verified=True, moderated=True,
is_rejected=False)
backend.activate_user(self.user1)
assert self.user1.is_active is True
self.user2 = AstakosUser.objects.create(
email='test2', email_verified=True, moderated=True,
has_signed_terms=True,
self.user2 = get_local_user(
'test2@example.org', email_verified=True, moderated=True,
is_rejected=False)
backend.activate_user(self.user2)
assert self.user2.is_active is True
......@@ -611,10 +609,9 @@ class TokensApiTest(TestCase):
class UserCatalogsTest(TestCase):
def test_get_uuid_displayname_catalogs(self):
self.user = AstakosUser.objects.create(
email='test1', email_verified=True, moderated=True,
has_signed_terms=True,
is_rejected=False)
self.user = get_local_user(
'test1@example.org', email_verified=True, moderated=True,
is_rejected=False, is_active=False)
client = Client()
url = reverse('astakos.api.user.get_uuid_displayname_catalogs')
......
# -*- coding: utf-8 -*-
# Copyright 2011 GRNET S.A. All rights reserved.
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
......@@ -434,8 +434,7 @@ class TestLocal(TestCase):
form = forms.LocalUserCreationForm(data)
self.assertTrue(form.is_valid())
user = form.save()
form.store_user(user, {})
user = form.create_user()
u = AstakosUser.objects.get()
self.assertEqual(u.email, 'kPap@synnefo.org')
......@@ -657,6 +656,17 @@ class TestLocal(TestCase):
# she can't because account is not active yet
self.assertContains(r, "Changing password is not")
def test_fix_superuser(self):
u = User.objects.create(username="dummy", email="email@example.org",
first_name="Super", last_name="User",
is_superuser=True)
User.objects.create(username="dummy2", email="email2@example.org",
first_name="Other", last_name="User")
fixed = auth_functions.fix_superusers()
self.assertEqual(len(fixed), 1)
fuser = fixed[0]
self.assertEqual(fuser.email, fuser.username)
class UserActionsTests(TestCase):
......@@ -789,7 +799,7 @@ class TestAuthProviderViews(TestCase):
Pending = PendingThirdPartyUser
User = AstakosUser
User.objects.create(email="newuser@synnefo.org")
auth_functions.make_user("newuser@synnefo.org")
get_local_user("olduser@synnefo.org")
cl_olduser = ShibbolethClient()
get_local_user("olduser2@synnefo.org")
......@@ -946,8 +956,8 @@ class TestAuthProvidersAPI(TestCase):
@im_settings(IM_MODULES=['local', 'shibboleth'])
def test_create(self):
user = AstakosUser.objects.create(email="kpap@synnefo.org")
user2 = AstakosUser.objects.create(email="kpap2@synnefo.org")
user = auth_functions.make_user(email="kpap@synnefo.org")
user2 = auth_functions.make_user(email="kpap2@synnefo.org")
module = 'shibboleth'
identifier = 'SHIB_UUID'
......@@ -1000,7 +1010,7 @@ class TestAuthProvidersAPI(TestCase):
CREATION_GROUPS_POLICY=['localgroup-create',
'group-create'])
def test_add_groups(self):
user = AstakosUser.objects.create(email="kpap@synnefo.org")
user = auth_functions.make_user("kpap@synnefo.org")
provider = auth.get_provider('shibboleth', user, 'test123')
provider.add_to_user()
user = AstakosUser.objects.get()
......@@ -1037,14 +1047,14 @@ class TestAuthProvidersAPI(TestCase):
settings.ASTAKOS_AUTH_PROVIDER_GOOGLE_ADD_GROUPS_POLICY = \
['google-user']
user = AstakosUser.objects.create(email="kpap@synnefo.org")
user = auth_functions.make_user("kpap@synnefo.org")
user.groups.add(group_old)
user.add_auth_provider('local')
user2 = AstakosUser.objects.create(email="kpap2@synnefo.org")
user2 = auth_functions.make_user("kpap2@synnefo.org")
user2.add_auth_provider('shibboleth', identifier='shibid')
user3 = AstakosUser.objects.create(email="kpap3@synnefo.org")
user3 = auth_functions.make_user("kpap3@synnefo.org")
user3.groups.add(group_old)
user3.add_auth_provider('local')
user3.add_auth_provider('shibboleth', identifier='1234')
......@@ -1109,8 +1119,7 @@ class TestAuthProvidersAPI(TestCase):
@im_settings(IM_MODULES=['local', 'shibboleth'])
def test_create_http(self):
# this should be wrapped inside a transaction
user = AstakosUser(email="test@test.com")
user.save()
user = auth_functions.make_user(email="test@test.com")
provider = auth_providers.get_provider('shibboleth', user,
'test@academia.test')
provider.add_to_user()
......@@ -1120,8 +1129,7 @@ class TestAuthProvidersAPI(TestCase):
user.get_auth_provider('local')
settings.ASTAKOS_AUTH_PROVIDER_SHIBBOLETH_CREATE_POLICY = False
user = AstakosUser(email="test2@test.com")
user.save()
user = auth_functions.make_user("test2@test.com")
provider = auth_providers.get_provider('shibboleth', user,
'test@shibboleth.com',
**{'info': {'name':
......@@ -1255,8 +1263,7 @@ class TestActivationBackend(TestCase):
'password2': '123'
}
form = backend.get_signup_form('local', user_data)
user = form.save(commit=False)
form.store_user(user)
user = form.create_user()
self.assertEqual(user.is_active, False)
self.assertEqual(user.email_verified, False)
......@@ -1311,8 +1318,7 @@ class TestActivationBackend(TestCase):
}
form = backend.get_signup_form(provider='local',
initial_data=user_data)
user = form.save(commit=False)
form.store_user(user)
user = form.create_user()
self.assertEqual(user.is_active, False)
self.assertEqual(user.email_verified, False)
......
# Copyright 2011 GRNET S.A. All rights reserved.
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
......@@ -54,6 +54,7 @@ from astakos.im import functions
from astakos.im import settings as astakos_settings
from astakos.im import forms
from astakos.im import activation_backends
from astakos.im import auth as auth_functions
from urllib import quote
from datetime import timedelta
......@@ -145,30 +146,23 @@ def get_user_client(username, password="password"):
def get_local_user(username, **kwargs):
try:
return AstakosUser.objects.get(email=username)
except:
user_params = {
'username': username,
'email': username,
<