Automatically redirect to the password change when it's expired
Unfortunately the only way we can know the user_id at this point is by parsing the error message. I also refactored the exceptions in openstack_auth to make them use different classes (but one common superclass). Partially implements blueprint: allow-users-change-expired-password Change-Id: Ieceee09db21040b96577db19bd195dc3799e3892
This commit is contained in:
parent
1208919617
commit
9d98a0c24d
@ -31,6 +31,15 @@
|
||||
</div>
|
||||
{%endif%}
|
||||
<fieldset hz-login-finder>
|
||||
{% if request.COOKIES.logout_reason %}
|
||||
{% if request.COOKIES.logout_status == "success" %}
|
||||
<div class="form-group clearfix error help-block alert alert-success" id="logout_reason">
|
||||
{% else %}
|
||||
<div class="form-group clearfix error help-block alert alert-danger" id="logout_reason">
|
||||
{% endif %}
|
||||
<p>{{ request.COOKIES.logout_reason }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
{% endblock %}
|
||||
|
@ -79,7 +79,7 @@ class KeystoneBackend(object):
|
||||
"service appears to have expired before it was "
|
||||
"issued. This may indicate a problem with either your "
|
||||
"server or client configuration.")
|
||||
raise exceptions.KeystoneAuthException(msg)
|
||||
raise exceptions.KeystoneTokenExpiredException(msg)
|
||||
return True
|
||||
|
||||
def _get_auth_backend(self, auth_url, **kwargs):
|
||||
@ -93,7 +93,7 @@ class KeystoneBackend(object):
|
||||
LOG.warning('No authentication backend could be determined to '
|
||||
'handle the provided credentials. This is likely a '
|
||||
'configuration error that should be addressed.')
|
||||
raise exceptions.KeystoneAuthException(msg)
|
||||
raise exceptions.KeystoneNoBackendException(msg)
|
||||
|
||||
def authenticate(self, request, auth_url=None, **kwargs):
|
||||
"""Authenticates a user via the Keystone Identity API."""
|
||||
@ -150,7 +150,7 @@ class KeystoneBackend(object):
|
||||
scoped_auth_ref = domain_auth_ref
|
||||
elif not scoped_auth_ref and not domain_auth_ref:
|
||||
msg = _('You are not authorized for any projects or domains.')
|
||||
raise exceptions.KeystoneAuthException(msg)
|
||||
raise exceptions.KeystoneNoProjectsException(msg)
|
||||
|
||||
# Check expiry for our new scoped token.
|
||||
self._check_auth_expiry(scoped_auth_ref)
|
||||
|
@ -14,3 +14,35 @@
|
||||
|
||||
class KeystoneAuthException(Exception):
|
||||
"""Generic error class to identify and catch our own errors."""
|
||||
|
||||
|
||||
class KeystoneTokenExpiredException(KeystoneAuthException):
|
||||
"""The authentication token issued by the Identity service has expired."""
|
||||
|
||||
|
||||
class KeystoneNoBackendException(KeystoneAuthException):
|
||||
"""No backend could be determined to handle the provided credentials."""
|
||||
|
||||
|
||||
class KeystoneNoProjectsException(KeystoneAuthException):
|
||||
"""You are not authorized for any projects or domains."""
|
||||
|
||||
|
||||
class KeystoneRetrieveProjectsException(KeystoneAuthException):
|
||||
"""Unable to retrieve authorized projects."""
|
||||
|
||||
|
||||
class KeystoneRetrieveDomainsException(KeystoneAuthException):
|
||||
"""Unable to retrieve authorized domains."""
|
||||
|
||||
|
||||
class KeystoneConnectionException(KeystoneAuthException):
|
||||
"""Unable to establish connection to keystone endpoint."""
|
||||
|
||||
|
||||
class KeystoneCredentialsException(KeystoneAuthException):
|
||||
"""Invalid credentials."""
|
||||
|
||||
|
||||
class KeystonePassExpiredException(KeystoneAuthException):
|
||||
"""The password is expired and needs to be changed."""
|
||||
|
@ -144,6 +144,15 @@ class Login(django_auth_forms.AuthenticationForm):
|
||||
'"%(domain)s", remote address %(remote_ip)s.',
|
||||
{'username': username, 'domain': domain,
|
||||
'remote_ip': utils.get_client_ip(self.request)})
|
||||
except exceptions.KeystonePassExpiredException as exc:
|
||||
LOG.info('Login failed for user "%(username)s" using domain '
|
||||
'"%(domain)s", remote address %(remote_ip)s: password'
|
||||
' expired.',
|
||||
{'username': username, 'domain': domain,
|
||||
'remote_ip': utils.get_client_ip(self.request)})
|
||||
if utils.allow_expired_passowrd_change():
|
||||
raise
|
||||
raise forms.ValidationError(exc)
|
||||
except exceptions.KeystoneAuthException as exc:
|
||||
LOG.info('Login failed for user "%(username)s" using domain '
|
||||
'"%(domain)s", remote address %(remote_ip)s.',
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import re
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from keystoneauth1 import exceptions as keystone_exceptions
|
||||
@ -90,7 +91,7 @@ class BasePlugin(object):
|
||||
except (keystone_exceptions.ClientException,
|
||||
keystone_exceptions.AuthorizationFailure):
|
||||
msg = _('Unable to retrieve authorized projects.')
|
||||
raise exceptions.KeystoneAuthException(msg)
|
||||
raise exceptions.KeystoneRetrieveProjectsException(msg)
|
||||
|
||||
def list_domains(self, session, auth_plugin, auth_ref=None):
|
||||
try:
|
||||
@ -99,7 +100,7 @@ class BasePlugin(object):
|
||||
except (keystone_exceptions.ClientException,
|
||||
keystone_exceptions.AuthorizationFailure):
|
||||
msg = _('Unable to retrieve authorized domains.')
|
||||
raise exceptions.KeystoneAuthException(msg)
|
||||
raise exceptions.KeystoneRetrieveDomainsException(msg)
|
||||
|
||||
def get_access_info(self, keystone_auth):
|
||||
"""Get the access info from an unscoped auth
|
||||
@ -118,12 +119,21 @@ class BasePlugin(object):
|
||||
except keystone_exceptions.ConnectFailure as exc:
|
||||
LOG.error(str(exc))
|
||||
msg = _('Unable to establish connection to keystone endpoint.')
|
||||
raise exceptions.KeystoneAuthException(msg)
|
||||
raise exceptions.KeystoneConnectionException(msg)
|
||||
except (keystone_exceptions.Unauthorized,
|
||||
keystone_exceptions.Forbidden,
|
||||
keystone_exceptions.NotFound) as exc:
|
||||
LOG.debug(str(exc))
|
||||
raise exceptions.KeystoneAuthException(_('Invalid credentials.'))
|
||||
msg = str(exc)
|
||||
LOG.debug(msg)
|
||||
match = re.match(r"The password is expired and needs to be changed"
|
||||
r" for user: ([^.]*)[.].*", msg)
|
||||
if match:
|
||||
exc = exceptions.KeystonePassExpiredException(
|
||||
_('Password expired.'))
|
||||
exc.user_id = match.group(1)
|
||||
raise exc
|
||||
msg = _('Invalid credentials.')
|
||||
raise exceptions.KeystoneCredentialsException(msg)
|
||||
except (keystone_exceptions.ClientException,
|
||||
keystone_exceptions.AuthorizationFailure) as exc:
|
||||
msg = _("An error occurred authenticating. "
|
||||
|
@ -772,6 +772,35 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin,
|
||||
self.assertContains(response, 'option value="Default"')
|
||||
settings.OPENSTACK_KEYSTONE_DOMAIN_DROPDOWN = False
|
||||
|
||||
def test_password_expired(self):
|
||||
user = self.data.user
|
||||
form_data = self.get_form_data(user)
|
||||
|
||||
class ExpiredException(keystone_exceptions.Unauthorized):
|
||||
http_status = 401
|
||||
message = ("The password is expired and needs to be changed"
|
||||
" for user: %s." % user.id)
|
||||
|
||||
exc = ExpiredException()
|
||||
self._mock_client_password_auth_failure(user.name, user.password, exc)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('login')
|
||||
|
||||
# GET the page to set the test cookie.
|
||||
response = self.client.get(url, form_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# POST to the page to log in.
|
||||
response = self.client.post(url, form_data)
|
||||
|
||||
# This fails with TemplateDoesNotExist for some reason.
|
||||
# self.assertRedirects(response, reverse('password', args=[user.id]))
|
||||
# so instead we check for the redirect manually:
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/password/%s/" % user.id)
|
||||
|
||||
|
||||
class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin,
|
||||
OpenStackAuthFederatedTestsMixin,
|
||||
|
@ -29,10 +29,14 @@ urlpatterns = [
|
||||
url(r'^switch_keystone_provider/(?P<keystone_provider>[^/]+)/$',
|
||||
views.switch_keystone_provider,
|
||||
name='switch_keystone_provider'),
|
||||
url(r'^password/(?P<user_id>[^/]+)/$', views.PasswordView.as_view(),
|
||||
name='password'),
|
||||
]
|
||||
|
||||
if utils.allow_expired_passowrd_change():
|
||||
urlpatterns.append(
|
||||
url(r'^password/(?P<user_id>[^/]+)/$', views.PasswordView.as_view(),
|
||||
name='password')
|
||||
)
|
||||
|
||||
if utils.is_websso_enabled():
|
||||
urlpatterns += [
|
||||
url(r"^websso/$", views.websso, name='websso'),
|
||||
|
@ -117,6 +117,11 @@ def get_keystone_client():
|
||||
return client_v3
|
||||
|
||||
|
||||
def allow_expired_passowrd_change():
|
||||
"""Checks if users should be able to change their expired passwords."""
|
||||
return getattr(settings, 'ALLOW_USERS_CHANGE_EXPIRED_PASSWORD', True)
|
||||
|
||||
|
||||
def is_websso_enabled():
|
||||
"""Websso is supported in Keystone version 3."""
|
||||
return settings.WEBSSO_ENABLED
|
||||
|
@ -19,6 +19,7 @@ from django.contrib.auth import views as django_auth_views
|
||||
from django.contrib import messages
|
||||
from django import http as django_http
|
||||
from django import shortcuts
|
||||
from django.urls import reverse
|
||||
from django.utils import functional
|
||||
from django.utils import http
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@ -112,12 +113,18 @@ def login(request):
|
||||
else:
|
||||
template_name = 'auth/login.html'
|
||||
|
||||
res = django_auth_views.LoginView.as_view(
|
||||
template_name=template_name,
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME,
|
||||
form_class=form,
|
||||
extra_context=extra_context,
|
||||
redirect_authenticated_user=False)(request)
|
||||
try:
|
||||
res = django_auth_views.LoginView.as_view(
|
||||
template_name=template_name,
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME,
|
||||
form_class=form,
|
||||
extra_context=extra_context,
|
||||
redirect_authenticated_user=False)(request)
|
||||
except exceptions.KeystonePassExpiredException as exc:
|
||||
res = django_http.HttpResponseRedirect(
|
||||
reverse('password', args=[exc.user_id]))
|
||||
msg = _("Your password has expired. Please set a new password.")
|
||||
res.set_cookie('logout_reason', msg, max_age=10)
|
||||
|
||||
# Save the region in the cookie, this is used as the default
|
||||
# selected region next time the Login form loads.
|
||||
|
Loading…
Reference in New Issue
Block a user