Allow users to change their passwords

Add change password panel on settings dashboard to enable users
to change their passwords

Change-Id: Ibfea2592e13aab3cc4892dce77ab62dcba65eacc
Implements: blueprint change-user-passwords
This commit is contained in:
Zhenguo Niu 2013-03-08 17:04:41 +08:00
parent ff573dae88
commit 2a97ce9602
10 changed files with 252 additions and 1 deletions

View File

@ -163,6 +163,7 @@ def keystoneclient(request, admin=False):
endpoint=endpoint,
original_ip=remote_addr,
insecure=insecure,
auth_url=endpoint,
debug=settings.DEBUG)
setattr(request, cache_attr, conn)
return conn
@ -314,6 +315,15 @@ def user_update_password(request, user, password, admin=True):
return manager.update(user, password=password)
def user_update_own_password(request, origpassword, password):
client = keystoneclient(request, admin=False)
if VERSIONS.active < 3:
client.user_id = request.user.id
return client.users.update_own_password(origpassword, password)
else:
return client.users.update(request.user.id, password=password)
def user_update_tenant(request, user, project, admin=True):
manager = keystoneclient(request, admin=admin).users
if VERSIONS.active < 3:

View File

@ -23,7 +23,7 @@ import horizon
class Settings(horizon.Dashboard):
name = _("Settings")
slug = "settings"
panels = ('user',)
panels = ('user', 'password', )
default_panel = 'user'
nav = False

View File

@ -0,0 +1,68 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Centrin Data Systems Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.forms import ValidationError
from django.views.decorators.debug import sensitive_variables
from horizon import forms
from horizon import messages
from horizon import exceptions
from horizon.utils import validators
from openstack_dashboard import api
class PasswordForm(forms.SelfHandlingForm):
current_password = forms.CharField(label=_("Current password"),
widget=forms.PasswordInput(render_value=False))
new_password = forms.RegexField(label=_("New password"),
widget=forms.PasswordInput(render_value=False),
regex=validators.password_validator(),
error_messages={'invalid':
validators.password_validator_msg()})
confirm_password = forms.CharField(label=_("Confirm new password"),
widget=forms.PasswordInput(render_value=False))
def clean(self):
'''Check to make sure password fields match.'''
data = super(forms.Form, self).clean()
if 'new_password' in data:
if data['new_password'] != data.get('confirm_password', None):
raise ValidationError(_('Passwords do not match.'))
return data
# We have to protect the entire "data" dict because it contains the
# oldpassword and newpassword strings.
@sensitive_variables('data')
def handle(self, request, data):
user_is_editable = api.keystone.keystone_can_edit_user()
if user_is_editable:
try:
passwd = api.keystone.user_update_own_password(request,
data['current_password'],
data['new_password'])
messages.success(request, _('Password changed.'))
except:
exceptions.handle(request,
_('Unable to change password.'))
return False
else:
messages.error(request, _('Changing password is not supported.'))
return False
return True

View File

@ -0,0 +1,29 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Centrin Data Systems Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.dashboards.settings import dashboard
class PasswordPanel(horizon.Panel):
name = _("Change Password")
slug = 'password'
dashboard.Settings.register(PasswordPanel)

View File

@ -0,0 +1,27 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}change_password_modal{% endblock %}
{% block form_action %}{% url 'horizon:settings:password:index' %}{% endblock %}
{% block modal_id %}change_password_modal{% endblock %}
{% block modal-header %}{% trans "Change Password" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>{% trans "From here you can change your password. We highly recommend you create a strong one. " %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
{% if hide %}<a href="{% url 'horizon:settings:password:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>{% endif %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Change Password" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Change Password") %}
{% endblock page_header %}
{% block main %}
{% include "settings/password/_change.html" %}
{% endblock %}

View File

@ -0,0 +1,57 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Centrin Data Systems Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django import http
from django.core.urlresolvers import reverse
from mox import IsA
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:settings:password:index')
class ChangePasswordTests(test.TestCase):
@test.create_stubs({api.keystone: ('user_update_own_password', )})
def test_change_password(self):
api.keystone.user_update_own_password(IsA(http.HttpRequest),
'oldpwd',
'normalpwd',).AndReturn(None)
self.mox.ReplayAll()
formData = {'method': 'PasswordForm',
'current_password': 'oldpwd',
'new_password': 'normalpwd',
'confirm_password': 'normalpwd'}
res = self.client.post(INDEX_URL, formData)
self.assertNoFormErrors(res)
def test_change_validation_passwords_not_matching(self):
formData = {'method': 'PasswordForm',
'current_password': 'currpasswd',
'new_password': 'testpassword',
'confirm_password': 'doesnotmatch'}
res = self.client.post(INDEX_URL, formData)
self.assertFormError(res, "form", None, ['Passwords do not match.'])

View File

@ -0,0 +1,23 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Centrin Data Systems Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls.defaults import patterns, url
from .views import PasswordView
urlpatterns = patterns('',
url(r'^$', PasswordView.as_view(), name='index'))

View File

@ -0,0 +1,26 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Centrin Data Systems Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from horizon import forms
from .forms import PasswordForm
from django.core.urlresolvers import reverse_lazy
class PasswordView(forms.ModalFormView):
form_class = PasswordForm
template_name = 'settings/password/change.html'
success_url = reverse_lazy('logout')