+ {% else %}
+
+ {% endif %}
{% include "horizon/common/_form_fields.html" %}
{% endblock %}
diff --git a/openstack_auth/backend.py b/openstack_auth/backend.py
index 39030e6f5f..10b865a7b5 100644
--- a/openstack_auth/backend.py
+++ b/openstack_auth/backend.py
@@ -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)
diff --git a/openstack_auth/exceptions.py b/openstack_auth/exceptions.py
index cce0d6543d..5925981c38 100644
--- a/openstack_auth/exceptions.py
+++ b/openstack_auth/exceptions.py
@@ -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."""
diff --git a/openstack_auth/forms.py b/openstack_auth/forms.py
index 186e472826..49470c8620 100644
--- a/openstack_auth/forms.py
+++ b/openstack_auth/forms.py
@@ -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.',
diff --git a/openstack_auth/plugin/base.py b/openstack_auth/plugin/base.py
index eb0451a1ff..215b5fd708 100644
--- a/openstack_auth/plugin/base.py
+++ b/openstack_auth/plugin/base.py
@@ -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. "
diff --git a/openstack_auth/tests/unit/test_auth.py b/openstack_auth/tests/unit/test_auth.py
index 386f3181bb..a668f2a7f0 100644
--- a/openstack_auth/tests/unit/test_auth.py
+++ b/openstack_auth/tests/unit/test_auth.py
@@ -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,
diff --git a/openstack_auth/urls.py b/openstack_auth/urls.py
index c030e3c3c0..f1ac47c1aa 100644
--- a/openstack_auth/urls.py
+++ b/openstack_auth/urls.py
@@ -29,10 +29,14 @@ urlpatterns = [
url(r'^switch_keystone_provider/(?P
[^/]+)/$',
views.switch_keystone_provider,
name='switch_keystone_provider'),
- url(r'^password/(?P[^/]+)/$', views.PasswordView.as_view(),
- name='password'),
]
+if utils.allow_expired_passowrd_change():
+ urlpatterns.append(
+ url(r'^password/(?P[^/]+)/$', views.PasswordView.as_view(),
+ name='password')
+ )
+
if utils.is_websso_enabled():
urlpatterns += [
url(r"^websso/$", views.websso, name='websso'),
diff --git a/openstack_auth/utils.py b/openstack_auth/utils.py
index df64f50c8c..60ce32b87b 100644
--- a/openstack_auth/utils.py
+++ b/openstack_auth/utils.py
@@ -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
diff --git a/openstack_auth/views.py b/openstack_auth/views.py
index b41eff0a9a..9e83789269 100644
--- a/openstack_auth/views.py
+++ b/openstack_auth/views.py
@@ -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.