Commit 518bbefd authored by Kostas Papadimitriou's avatar Kostas Papadimitriou
Browse files

Authentication providers improvements

Major authentication provider refactoring to support

- Modular and easily configurable messages with common context
- Fine grained provider policies to support appling specific policies to
  users and/or groups

Key points:

- Use auth_providers.AuthProvider instances where auth provider logic is
  needed. Instances get properly initialized with the available context
  (with no user/signup view, with user/login view, with user and
  identifier/profile view).

- All authentication provider messages are now accessed using the
  get_*_msg AuthProvider attributes.

- Provider policies logic is handled from  get_*_policy attributes.

- All provider messages may be overridden globally or per provider level from
  settings::

  # global change
  ASTAKOS_AUTH_PROVIDER_NOT_ACTIVE = 'Provider not active'

  # change only applies to shibboleth provider
  ASTAKOS_AUTH_PROVIDER_SHIBBOLETH_NOT_ACTIVE = 'Shibboleth is not  active'

- Provider policies may be overridden in settings::

  # ALL users wont be able to add shibboleth login method from their
  # profile
  AUTH_PROVIDER_SHIBBOLETH_ADD_POLICY = False

- New provider policies profile model added. Profiles can be assigned to
  a group or/and a specific user.

- All tests updated to match the auth providers changes.

- New management commands included

  * user-auth-policy-{add, list, remove, set, show}
    Manage authentication provider policy profiles.

  * user-group-{add, list}
    User group management commands

- Updated user-list to optionally display auth provider information
parent 957dabba
......@@ -222,9 +222,8 @@ class SimpleBackend(ActivationBackend):
def _is_preaccepted(self, user):
if super(SimpleBackend, self)._is_preaccepted(user):
return True
if astakos_settings.MODERATION_ENABLED:
return False
return True
return user.get_auth_provider().get_automoderate_policy
class ActivationResult(object):
......
......@@ -271,19 +271,19 @@ class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
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)
mark_safe("I agree with %s" % terms_link_html)
def clean_email(self):
email = self.cleaned_data['email']
if not email:
raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
if reserved_verified_email(email):
provider = auth_providers.get_provider(self.request.REQUEST.get('provider', 'local'))
extra_message = _(astakos_messages.EXISTING_EMAIL_THIRD_PARTY_NOTIFICATION) % \
(provider.get_title_display, reverse('edit_profile'))
provider_id = self.request.REQUEST.get('provider', 'local')
provider = auth_providers.get_provider(provider_id)
extra_message = provider.get_add_to_existing_account_msg
raise forms.ValidationError(_(astakos_messages.EMAIL_USED) + ' ' + \
extra_message)
raise forms.ValidationError(mark_safe(_(astakos_messages.EMAIL_USED) + ' ' +
extra_message))
return email
def clean_has_signed_terms(self):
......@@ -294,10 +294,12 @@ class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
def post_store_user(self, user, request):
pending = PendingThirdPartyUser.objects.get(
token=request.POST.get('third_party_token'),
third_party_identifier= \
self.cleaned_data.get('third_party_identifier'))
return user.add_pending_auth_provider(pending)
token=request.POST.get('third_party_token'),
third_party_identifier=
self.cleaned_data.get('third_party_identifier'))
provider = pending.get_provider(user)
provider.add_to_user()
pending.delete()
def save(self, commit=True):
user = super(ThirdPartyUserCreationForm, self).save(commit=False)
......@@ -406,9 +408,9 @@ class LoginForm(AuthenticationForm):
try:
user = AstakosUser.objects.get_by_identifier(username)
if not user.has_auth_provider('local'):
provider = auth_providers.get_provider('local')
provider = auth_providers.get_provider('local', user)
raise forms.ValidationError(
_(provider.get_message('NOT_ACTIVE_FOR_USER')))
provider.get_login_disabled_msg)
except AstakosUser.DoesNotExist:
pass
......@@ -418,7 +420,8 @@ class LoginForm(AuthenticationForm):
if self.user_cache is None:
raise
if not self.user_cache.is_active:
raise forms.ValidationError(self.user_cache.get_inactive_message())
msg = self.user_cache.get_inactive_message('local')
raise forms.ValidationError(msg)
if self.request:
if not self.request.session.test_cookie_worked():
raise
......@@ -505,25 +508,25 @@ class ExtendedPasswordResetForm(PasswordResetForm):
clean_email: to handle local auth provider checks
"""
def clean_email(self):
email = super(ExtendedPasswordResetForm, self).clean_email()
# we override the default django auth clean_email to provide more
# detailed messages in case of inactive users
email = self.cleaned_data['email']
try:
user = AstakosUser.objects.get_by_identifier(email)
self.users_cache = [user]
if not user.is_active:
raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
raise forms.ValidationError(user.get_inactive_message('local'))
provider = auth_providers.get_provider('local', user)
if not user.has_usable_password():
provider = auth_providers.get_provider('local')
available_providers = user.auth_providers.all()
available_providers = ",".join(p.settings.get_title_display for p in \
available_providers)
message = astakos_messages.UNUSABLE_PASSWORD % \
(provider.get_method_prompt_display, available_providers)
raise forms.ValidationError(message)
msg = provider.get_unusable_password_msg
raise forms.ValidationError(msg)
if not user.can_change_password():
raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
except AstakosUser.DoesNotExist, e:
msg = provider.get_cannot_change_password_msg
raise forms.ValidationError(msg)
except AstakosUser.DoesNotExist:
raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
return email
......@@ -661,9 +664,10 @@ class ExtendedSetPasswordForm(SetPasswordForm):
self.user = AstakosUser.objects.get(id=self.user.id)
if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
self.user.renew_token()
#self.user.flush_sessions()
if not self.user.has_auth_provider('local'):
self.user.add_auth_provider('local', auth_backend='astakos')
provider = auth_providers.get_provider('local', self.user)
if provider.get_add_policy:
provider.add_to_user()
except BaseException, e:
logger.exception(e)
......
# Copyright 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
# 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 string
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from astakos.im.models import AuthProviderPolicyProfile as Profile
option_list = list(BaseCommand.option_list) + [
make_option('--update',
action='store_true',
dest='update',
default=False,
help="Update an existing profile."),
make_option('--exclusive',
action='store_true',
dest='exclusive',
default=False,
help="Apply policies to all authentication providers "
"except the one provided."),
]
POLICIES = ['add', 'remove', 'create', 'login', 'limit', 'required',
'automoderate']
for p in POLICIES:
option_list.append(make_option('--unset-%s-policy' % p,
action='store_true',
dest='unset_policy_%s' % p,
help="Unset %s policy (only when --update)"
% p.title()))
option_list.append(make_option('--%s-policy' % p,
action='store',
dest='policy_%s' % p,
help="%s policy" % p.title()))
class Command(BaseCommand):
args = "<name> <provider_name>"
help = "Create a new authentication provider policy profile"
option_list = option_list
def handle(self, *args, **options):
if len(args) < 2:
raise CommandError("Invalid number of arguments")
profile = None
update = options.get('update')
name = args[0].strip()
provider = args[1].strip()
if Profile.objects.filter(name=name).count():
if update:
profile = Profile.objects.get(name=name)
else:
raise CommandError("Profile with the same name already exists")
if not profile:
profile = Profile()
profile.name = name
profile.provider = provider
profile.is_exclusive = options.get('exclusive')
for policy, value in options.iteritems():
if policy.startswith('policy_') and value is not None:
if isinstance(value, basestring) and value[0] in string.digits:
value = int(value)
if value == 'False' or value == '0':
value = False
if value == 'True' or value == '1':
value = True
setattr(profile, policy, value)
if update and policy.startswith('unset_'):
policy = policy.replace('unset_', '')
setattr(profile, policy, None)
profile.save()
if update:
print "Profile updated"
else:
print "Profile stored"
# Copyright 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
# 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.
from optparse import make_option
from django.core.management.base import NoArgsCommand
from astakos.im.models import AuthProviderPolicyProfile
from ._common import format
class Command(NoArgsCommand):
help = "List existing authentication provider policy profiles"
option_list = NoArgsCommand.option_list + (
make_option('-c',
action='store_true',
dest='csv',
default=False,
help="Use pipes to separate values"),
)
def handle_noargs(self, **options):
profiles = AuthProviderPolicyProfile.objects.all()
labels = ['id', 'name', 'provider', 'exclusive', 'groups', 'users']
columns = [3, 20, 13, 7, 50, 50]
if not options['csv']:
line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
self.stdout.write(line + '\n')
sep = '-' * len(line)
self.stdout.write(sep + '\n')
for profile in profiles:
id = str(profile.pk)
name = profile.name
provider = profile.provider
exclusive = str(profile.is_exclusive)
groups = ",".join([g.name for g in profile.groups.all()])
users = ",".join([u.email for u in profile.users.all()])
field_values = [id, name, provider, exclusive, groups, users]
fields = (format(elem) for elem in field_values)
if options['csv']:
line = '|'.join(fields)
else:
line = ' '.join(f.rjust(w) for f, w in zip(fields, columns))
self.stdout.write(line + '\n')
# Copyright 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
# 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.
from django.core.management.base import BaseCommand, CommandError
from astakos.im.models import AuthProviderPolicyProfile as Profile
class Command(BaseCommand):
args = "<profile_name>"
help = "Remove an authentication provider policy"
option_list = BaseCommand.option_list + ()
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("Invalid number of arguments")
try:
profile = Profile.objects.get(name=args[0])
profile.delete()
except Profile.DoesNotExist:
raise CommandError("Invalid profile name")
# Copyright 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
# 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.
from optparse import make_option
from django.db import transaction
from django.core.management.base import BaseCommand, CommandError
from astakos.im.models import AuthProviderPolicyProfile as Profile
from astakos.im.models import AstakosUser, Group
option_list = BaseCommand.option_list + (
make_option('--group',
action='append',
dest='groups',
default=[],
help="Assign profile to the provided group id. Option may "
"be used more than once."),
make_option('--user',
action='append',
dest='users',
default=[],
help="Assign profile to the provided user id. Option may "
"be used more than once.")
)
@transaction.commit_on_success
def update_profile(profile, users, groups):
profile.groups.all().delete()
profile.users.all().delete()
profile.groups.add(*groups)
profile.users.add(*users)
class Command(BaseCommand):
args = "<name> <provider_name>"
help = "Assign an existing authentication provider policy profile to " + \
"a user or group. All previously set "
option_list = option_list
def handle(self, *args, **options):
if len(args) < 1:
raise CommandError("Invalid number of arguments")
name = args[0].strip()
try:
profile = Profile.objects.get(name=name)
except Profile.DoesNotExist:
raise CommandError("Invalid profile name")
users = []
try:
users = [AstakosUser.objects.get(pk=int(pk)) for pk in
options.get('users')]
except AstakosUser.DoesNotExist:
raise CommandError("Invalid user id")
groups = []
try:
groups = [Group.objects.get(pk=int(pk)) for pk in
options.get('groups')]
except Group.DoesNotExist:
raise CommandError("Invalid group id")
update_profile(profile, users, groups)
# 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
# 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.
from django.core.management.base import BaseCommand, CommandError
from astakos.im.models import AuthProviderPolicyProfile as Profile
from synnefo.lib.ordereddict import OrderedDict
from ._common import format
import uuid
class Command(BaseCommand):
args = "<profile_name>"
help = "Show authentication provider profile details"
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("Please profile name")
try:
profile = Profile.objects.get(name=args[0])
except Profile.DoesNotExist:
raise CommandError("Profile does not exist")
kv = OrderedDict(
[
('id', profile.id),
('is active', str(profile.active)),
('name', profile.name),
('is exclusive', profile.is_exclusive),
('policies', profile.policies),
('groups', profile.groups.all()),
('users', profile.users.all())
])
self.stdout.write(format(kv))
self.stdout.write('\n')
# Copyright 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
# 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