Skip to content

Commit

Permalink
Adding user password setting api call
Browse files Browse the repository at this point in the history
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
  • Loading branch information
derekhiggins committed Jul 10, 2012
1 parent ec9c038 commit 4ab47ad
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 1 deletion.
24 changes: 24 additions & 0 deletions doc/source/configuration.rst
Expand Up @@ -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
--------------------------
Expand Down
5 changes: 4 additions & 1 deletion etc/keystone.conf.sample
Expand Up @@ -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

Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions 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 *
88 changes: 88 additions & 0 deletions 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']))
89 changes: 89 additions & 0 deletions tests/test_keystoneclient.py
Expand Up @@ -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

Expand Down Expand Up @@ -857,6 +860,92 @@ def test_roles_get_by_user(self):
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):
Expand Down

0 comments on commit 4ab47ad

Please sign in to comment.