diff options
| author | Derek Higgins <derekh@redhat.com> | 2012-07-05 22:15:48 +0100 |
|---|---|---|
| committer | Derek Higgins <derekh@redhat.com> | 2012-07-10 11:06:11 +0100 |
| commit | 4ab47ad224c422dcd96aa256740945d1e6a8a208 (patch) | |
| tree | 0d4c27ae82696ab68e3d830b12af5f1e26015e4f | |
| parent | ec9c038ba28af4273aae81450249e3691a2c2cb6 (diff) | |
Adding user password setting api call
Fixes bug 996922
This commit adds a user_crud module that can be used in the public wsgi
pipeline, currently the only operation included allows a user to update
their own password.
In order to change their password a user should make a HTTP PATCH to
/v2.0/OS-KSCRUD/users/<userid>
with the json data fomated like this
{"user": {"password": "DCBA", "original_password": "ABCD"}}
in addition to changing the users password, all current tokens
will be cleared (for token backends that support listing) and
a new token id will be returned.
Change-Id: I0cbdafbb29a5b6531ad192f240efb9379f0efd2d
| -rw-r--r-- | doc/source/configuration.rst | 24 | ||||
| -rw-r--r-- | etc/keystone.conf.sample | 5 | ||||
| -rw-r--r-- | keystone/contrib/user_crud/__init__.py | 17 | ||||
| -rw-r--r-- | keystone/contrib/user_crud/core.py | 88 | ||||
| -rw-r--r-- | tests/test_keystoneclient.py | 89 |
5 files changed, 222 insertions, 1 deletions
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 415ee539..fbeab5e1 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -235,6 +235,30 @@ certificates:: * ``ca_certs``: Path to CA trust chain. * ``cert_required``: Requires client certificate. Defaults to False. +User CRUD +--------- + +Keystone provides a user CRUD filter that can be added to the public_api +pipeline. This user crud filter allows users to use a HTTP PATCH to change +their own password. To enable this extension you should define a +user_crud_extension filter, insert it after the ``*_body`` middleware +and before the ``public_service`` app in the public_api WSGI pipeline in +keystone.conf e.g.:: + + [filter:user_crud_extension] + paste.filter_factory = keystone.contrib.user_crud:CrudExtension.factory + + [pipeline:public_api] + pipeline = stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug ec2_extension user_crud_extension public_service + +Each user can then change their own password with a HTTP PATCH :: + + > curl -X PATCH http://localhost:5000/v2.0/OS-KSCRUD/users/<userid> -H "Content-type: application/json" \ + -H "X_Auth_Token: <authtokenid>" -d '{"user": {"password": "ABCD", "original_password": "DCBA"}}' + +In addition to changing their password all of the users current tokens will be +deleted (if the backend used is kvs or sql) + Sample Configuration Files -------------------------- diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index ac5b440d..1ff4249c 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -134,6 +134,9 @@ paste.filter_factory = keystone.middleware:XmlBodyMiddleware.factory [filter:json_body] paste.filter_factory = keystone.middleware:JsonBodyMiddleware.factory +[filter:user_crud_extension] +paste.filter_factory = keystone.contrib.user_crud:CrudExtension.factory + [filter:crud_extension] paste.filter_factory = keystone.contrib.admin_crud:CrudExtension.factory @@ -159,7 +162,7 @@ paste.app_factory = keystone.service:public_app_factory paste.app_factory = keystone.service:admin_app_factory [pipeline:public_api] -pipeline = stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug ec2_extension public_service +pipeline = stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug ec2_extension user_crud_extension public_service [pipeline:admin_api] pipeline = stats_monitoring url_normalize token_auth admin_token_auth xml_body json_body debug stats_reporting ec2_extension s3_extension crud_extension admin_service diff --git a/keystone/contrib/user_crud/__init__.py b/keystone/contrib/user_crud/__init__.py new file mode 100644 index 00000000..8f4a83f0 --- /dev/null +++ b/keystone/contrib/user_crud/__init__.py @@ -0,0 +1,17 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Red Hat, Inc +# +# 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 keystone.contrib.user_crud.core import * diff --git a/keystone/contrib/user_crud/core.py b/keystone/contrib/user_crud/core.py new file mode 100644 index 00000000..67aecdb9 --- /dev/null +++ b/keystone/contrib/user_crud/core.py @@ -0,0 +1,88 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Red Hat, Inc +# +# 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. + +import copy +import uuid + +from keystone import exception +from keystone.common import logging +from keystone.common import wsgi +from keystone.identity import Manager as IdentityManager +from keystone.identity import UserController as UserManager +from keystone.token import Manager as TokenManager + + +LOG = logging.getLogger(__name__) + + +class UserController(wsgi.Application): + def __init__(self): + self.identity_api = IdentityManager() + self.token_api = TokenManager() + self.user_controller = UserManager() + + def set_user_password(self, context, user_id, user): + token_id = context.get('token_id') + original_password = user.get('original_password') + + token_ref = self.token_api.get_token(context=context, + token_id=token_id) + user_id_from_token = token_ref['user']['id'] + + if user_id_from_token != user_id or original_password is None: + raise exception.Forbidden() + + try: + user_ref = self.identity_api.authenticate( + context=context, + user_id=user_id_from_token, + password=original_password)[0] + if not user_ref.get('enabled', True): + raise exception.Unauthorized() + except AssertionError: + raise exception.Unauthorized() + + update_dict = {'password': user['password'], 'id': user_id} + + admin_context = copy.copy(context) + admin_context['is_admin'] = True + self.user_controller.set_user_password(admin_context, + user_id, + update_dict) + + token_id = uuid.uuid4().hex + new_token_ref = copy.copy(token_ref) + new_token_ref['id'] = token_id + self.token_api.create_token(context=context, token_id=token_id, + data=new_token_ref) + logging.debug('TOKEN_REF %s', new_token_ref) + return {'access': {'token': new_token_ref}} + + +class CrudExtension(wsgi.ExtensionRouter): + """ + + Provides a subset of CRUD operations for internal data types. + + """ + + def add_routes(self, mapper): + user_controller = UserController() + + mapper.connect('/OS-KSCRUD/users/{user_id}', + controller=user_controller, + action='set_user_password', + conditions=dict(method=['PATCH'])) diff --git a/tests/test_keystoneclient.py b/tests/test_keystoneclient.py index 4847b2cf..28a8b716 100644 --- a/tests/test_keystoneclient.py +++ b/tests/test_keystoneclient.py @@ -16,10 +16,13 @@ import time import uuid +import webob import nose.exc from keystone import test +from keystone.openstack.common import jsonutils + import default_fixtures @@ -857,6 +860,92 @@ class KcMasterTestCase(CompatTestCase, KeystoneClientTests): tenant=self.tenant_bar['id']) self.assertTrue(len(roles) > 0) + def test_user_can_update_passwd(self): + client = self.get_client(self.user_two) + + token_id = client.auth_token + new_password = uuid.uuid4().hex + + # TODO(derekh) : Update to use keystoneclient when available + class FakeResponse(object): + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + responseobject = FakeResponse() + + req = webob.Request.blank( + '/v2.0/OS-KSCRUD/users/%s' % self.user_two['id'], + headers={'X-Auth-Token': token_id}) + req.method = 'PATCH' + req.body = '{"user":{"password":"%s","original_password":"%s"}}' % \ + (new_password, self.user_two['password']) + self.public_server.application(req.environ, + responseobject.start_fake_response) + + self.user_two['password'] = new_password + self.get_client(self.user_two) + + def test_user_cant_update_other_users_passwd(self): + from keystoneclient import exceptions as client_exceptions + + client = self.get_client(self.user_two) + + token_id = client.auth_token + new_password = uuid.uuid4().hex + + # TODO(derekh) : Update to use keystoneclient when available + class FakeResponse(object): + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + responseobject = FakeResponse() + + req = webob.Request.blank( + '/v2.0/OS-KSCRUD/users/%s' % self.user_foo['id'], + headers={'X-Auth-Token': token_id}) + req.method = 'PATCH' + req.body = '{"user":{"password":"%s","original_password":"%s"}}' % \ + (new_password, self.user_two['password']) + self.public_server.application(req.environ, + responseobject.start_fake_response) + self.assertEquals(403, responseobject.response_status) + + self.user_two['password'] = new_password + self.assertRaises(client_exceptions.Unauthorized, + self.get_client, self.user_two) + + def test_tokens_after_user_update_passwd(self): + from keystoneclient import exceptions as client_exceptions + + client = self.get_client(self.user_two) + + token_id = client.auth_token + new_password = uuid.uuid4().hex + + # TODO(derekh) : Update to use keystoneclient when available + class FakeResponse(object): + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + responseobject = FakeResponse() + + req = webob.Request.blank( + '/v2.0/OS-KSCRUD/users/%s' % self.user_two['id'], + headers={'X-Auth-Token': token_id}) + req.method = 'PATCH' + req.body = '{"user":{"password":"%s","original_password":"%s"}}' % \ + (new_password, self.user_two['password']) + + rv = self.public_server.application( + req.environ, + responseobject.start_fake_response) + responce_json = jsonutils.loads(rv.next()) + new_token_id = responce_json['access']['token']['id'] + + self.assertRaises(client_exceptions.Unauthorized, client.tenants.list) + client.auth_token = new_token_id + client.tenants.list() + class KcEssex3TestCase(CompatTestCase, KeystoneClientTests): def get_checkout(self): |
