Implement HEAD method for all v3 GET actions

Implement the HEAD method for all get-one and list-all operations in the
v3 API (non-extended). While this may never be used by
python-openstackclient, it is useful to operators and application
developers for quickly obtaining metainformation about API resources,
and for "testing hypertext links for validity, accessibility, and
recent modification"[1].

[1] https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4

Closes-bug: #1370335

Change-Id: Iae26ebea1aa40d3b5c6c676dabe4f60a86a4f99f
This commit is contained in:
Colleen Murphy 2016-03-21 14:15:52 -07:00
parent 8a56c161ee
commit 1d087af001
8 changed files with 131 additions and 79 deletions

View File

@ -63,7 +63,7 @@ class Routers(wsgi.RoutersBase):
self._add_resource(
mapper, project_controller,
path='/users/{user_id}/projects',
get_action='list_user_projects',
get_head_action='list_user_projects',
rel=json_home.build_v3_resource_relation('user_projects'),
path_vars={
'user_id': json_home.Parameters.USER_ID,
@ -137,7 +137,7 @@ class Routers(wsgi.RoutersBase):
self._add_resource(
mapper, grant_controller,
path='/projects/{project_id}/users/{user_id}/roles',
get_action='list_grants',
get_head_action='list_grants',
rel=json_home.build_v3_resource_relation('project_user_roles'),
path_vars={
'project_id': json_home.Parameters.PROJECT_ID,
@ -146,7 +146,7 @@ class Routers(wsgi.RoutersBase):
self._add_resource(
mapper, grant_controller,
path='/projects/{project_id}/groups/{group_id}/roles',
get_action='list_grants',
get_head_action='list_grants',
rel=json_home.build_v3_resource_relation('project_group_roles'),
path_vars={
'group_id': json_home.Parameters.GROUP_ID,
@ -179,7 +179,7 @@ class Routers(wsgi.RoutersBase):
self._add_resource(
mapper, grant_controller,
path='/domains/{domain_id}/users/{user_id}/roles',
get_action='list_grants',
get_head_action='list_grants',
rel=json_home.build_v3_resource_relation('domain_user_roles'),
path_vars={
'domain_id': json_home.Parameters.DOMAIN_ID,
@ -188,7 +188,7 @@ class Routers(wsgi.RoutersBase):
self._add_resource(
mapper, grant_controller,
path='/domains/{domain_id}/groups/{group_id}/roles',
get_action='list_grants',
get_head_action='list_grants',
rel=json_home.build_v3_resource_relation('domain_group_roles'),
path_vars={
'domain_id': json_home.Parameters.DOMAIN_ID,
@ -198,7 +198,7 @@ class Routers(wsgi.RoutersBase):
self._add_resource(
mapper, controllers.RoleAssignmentV3(),
path='/role_assignments',
get_action='list_role_assignments_wrapper',
get_head_action='list_role_assignments_wrapper',
rel=json_home.build_v3_resource_relation('role_assignments'))
if CONF.os_inherit.enabled:

View File

@ -44,12 +44,12 @@ class Router(wsgi.ComposableRouter):
collection_path,
controller=self.controller,
action=self.method_template % 'list_%s' % self.collection_key,
conditions=dict(method=['GET']))
conditions=dict(method=['GET', 'HEAD']))
mapper.connect(
entity_path,
controller=self.controller,
action=self.method_template % 'get_%s' % self.key,
conditions=dict(method=['GET']))
conditions=dict(method=['GET', 'HEAD']))
mapper.connect(
entity_path,
controller=self.controller,

View File

@ -50,7 +50,7 @@ class Routers(wsgi.RoutersBase):
self._add_resource(
mapper, user_controller,
path='/groups/{group_id}/users',
get_action='list_users_in_group',
get_head_action='list_users_in_group',
rel=json_home.build_v3_resource_relation('group_users'),
path_vars={
'group_id': json_home.Parameters.GROUP_ID,
@ -77,7 +77,7 @@ class Routers(wsgi.RoutersBase):
self._add_resource(
mapper, group_controller,
path='/users/{user_id}/groups',
get_action='list_groups_for_user',
get_head_action='list_groups_for_user',
rel=json_home.build_v3_resource_relation('user_groups'),
path_vars={
'user_id': json_home.Parameters.USER_ID,

View File

@ -51,18 +51,21 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
self.post('/roles', body={'role': {}},
expected_status=http_client.BAD_REQUEST)
def test_list_roles(self):
"""Call ``GET /roles``."""
def test_list_head_roles(self):
"""Call ``GET & HEAD /roles``."""
resource_url = '/roles'
r = self.get(resource_url)
self.assertValidRoleListResponse(r, ref=self.role,
resource_url=resource_url)
self.head(resource_url, expected_status=http_client.OK)
def test_get_role(self):
"""Call ``GET /roles/{role_id}``."""
r = self.get('/roles/%(role_id)s' % {
'role_id': self.role_id})
def test_get_head_role(self):
"""Call ``GET & HEAD /roles/{role_id}``."""
resource_url = '/roles/%(role_id)s' % {
'role_id': self.role_id}
r = self.get(resource_url)
self.assertValidRoleResponse(r, self.role)
self.head(resource_url, expected_status=http_client.OK)
def test_update_role(self):
"""Call ``PATCH /roles/{role_id}``."""
@ -115,11 +118,13 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
self.assertValidRoleListResponse(r, ref=role,
resource_url=collection_url,
expected_length=2)
self.head(collection_url, expected_status=http_client.OK)
self.delete(member_url)
r = self.get(collection_url)
self.assertValidRoleListResponse(r, ref=self.role, expected_length=1)
self.assertIn(collection_url, r.result['links']['self'])
self.head(collection_url, expected_status=http_client.OK)
def test_crud_user_project_role_grants_no_user(self):
"""Grant role on a project to a user that doesn't exist.
@ -153,11 +158,13 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
r = self.get(collection_url)
self.assertValidRoleListResponse(r, ref=self.role,
resource_url=collection_url)
self.head(collection_url, expected_status=http_client.OK)
self.delete(member_url)
r = self.get(collection_url)
self.assertValidRoleListResponse(r, expected_length=0,
resource_url=collection_url)
self.head(collection_url, expected_status=http_client.OK)
def test_crud_user_domain_role_grants_no_user(self):
"""Grant role on a domain to a user that doesn't exist.
@ -191,11 +198,13 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
r = self.get(collection_url)
self.assertValidRoleListResponse(r, ref=self.role,
resource_url=collection_url)
self.head(collection_url, expected_status=http_client.OK)
self.delete(member_url)
r = self.get(collection_url)
self.assertValidRoleListResponse(r, expected_length=0,
resource_url=collection_url)
self.head(collection_url, expected_status=http_client.OK)
def test_crud_group_project_role_grants_no_group(self):
"""Grant role on a project to a group that doesn't exist.
@ -230,11 +239,13 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
r = self.get(collection_url)
self.assertValidRoleListResponse(r, ref=self.role,
resource_url=collection_url)
self.head(collection_url, expected_status=http_client.OK)
self.delete(member_url)
r = self.get(collection_url)
self.assertValidRoleListResponse(r, expected_length=0,
resource_url=collection_url)
self.head(collection_url, expected_status=http_client.OK)
def test_crud_group_domain_role_grants_no_group(self):
"""Grant role on a domain to a group that doesn't exist.
@ -457,8 +468,8 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
# Role Assignments tests
def test_get_role_assignments(self):
"""Call ``GET /role_assignments``.
def test_get_head_role_assignments(self):
"""Call ``GET & HEAD /role_assignments``.
The sample data set up already has a user, group and project
that is part of self.domain. We use these plus a new user
@ -493,6 +504,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
r = self.get(collection_url)
self.assertValidRoleAssignmentListResponse(r,
resource_url=collection_url)
self.head(collection_url, expected_status=http_client.OK)
existing_assignments = len(r.result.get('role_assignments'))
# Now add one of each of the four types of assignment, making sure
@ -507,6 +519,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
expected_length=existing_assignments + 1,
resource_url=collection_url)
self.assertRoleAssignmentInListResponse(r, gd_entity)
self.head(collection_url, expected_status=http_client.OK)
ud_entity = self.build_role_assignment_entity(domain_id=self.domain_id,
user_id=user1['id'],
@ -518,6 +531,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
expected_length=existing_assignments + 2,
resource_url=collection_url)
self.assertRoleAssignmentInListResponse(r, ud_entity)
self.head(collection_url, expected_status=http_client.OK)
gp_entity = self.build_role_assignment_entity(
project_id=self.project_id, group_id=self.group_id,
@ -529,6 +543,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
expected_length=existing_assignments + 3,
resource_url=collection_url)
self.assertRoleAssignmentInListResponse(r, gp_entity)
self.head(collection_url, expected_status=http_client.OK)
up_entity = self.build_role_assignment_entity(
project_id=self.project_id, user_id=user1['id'],
@ -540,6 +555,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
expected_length=existing_assignments + 4,
resource_url=collection_url)
self.assertRoleAssignmentInListResponse(r, up_entity)
self.head(collection_url, expected_status=http_client.OK)
# Now delete the four we added and make sure they are removed
# from the collection.
@ -557,6 +573,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
self.assertRoleAssignmentNotInListResponse(r, ud_entity)
self.assertRoleAssignmentNotInListResponse(r, gp_entity)
self.assertRoleAssignmentNotInListResponse(r, up_entity)
self.head(collection_url, expected_status=http_client.OK)
def test_get_effective_role_assignments(self):
"""Call ``GET /role_assignments?effective``.
@ -2808,7 +2825,7 @@ class ListUserProjectsTestCase(test_v3.RestfulTestCase):
self.roles.append(role)
self.users.append(user)
def test_list_all(self):
def test_list_head_all(self):
for i in range(len(self.users)):
user = self.users[i]
auth = self.auths[i]
@ -2818,6 +2835,7 @@ class ListUserProjectsTestCase(test_v3.RestfulTestCase):
projects_result = result.json['projects']
self.assertEqual(1, len(projects_result))
self.assertEqual(self.projects[i]['id'], projects_result[0]['id'])
self.head(url, auth=auth, expected_status=http_client.OK)
def test_list_enabled(self):
for i in range(len(self.users)):

View File

@ -161,10 +161,12 @@ class CatalogTestCase(test_v3.RestfulTestCase):
body={'region': ref},
expected_status=http_client.BAD_REQUEST)
def test_list_regions(self):
"""Call ``GET /regions``."""
r = self.get('/regions')
def test_list_head_regions(self):
"""Call ``GET & HEAD /regions``."""
resource_url = '/regions'
r = self.get(resource_url)
self.assertValidRegionListResponse(r, ref=self.region)
self.head(resource_url, expected_status=http_client.OK)
def _create_region_with_parent_id(self, parent_id=None):
ref = unit.new_region_ref(parent_region_id=parent_id)
@ -185,11 +187,13 @@ class CatalogTestCase(test_v3.RestfulTestCase):
for region in r.result['regions']:
self.assertEqual(parent_id, region['parent_region_id'])
def test_get_region(self):
"""Call ``GET /regions/{region_id}``."""
r = self.get('/regions/%(region_id)s' % {
'region_id': self.region_id})
def test_get_head_region(self):
"""Call ``GET & HEAD /regions/{region_id}``."""
resource_url = '/regions/%(region_id)s' % {
'region_id': self.region_id}
r = self.get(resource_url)
self.assertValidRegionResponse(r, self.region)
self.head(resource_url, expected_status=http_client.OK)
def test_update_region(self):
"""Call ``PATCH /regions/{region_id}``."""
@ -308,10 +312,12 @@ class CatalogTestCase(test_v3.RestfulTestCase):
self.post('/services', body={'service': ref},
expected_status=http_client.BAD_REQUEST)
def test_list_services(self):
"""Call ``GET /services``."""
r = self.get('/services')
def test_list_head_services(self):
"""Call ``GET & HEAD /services``."""
resource_url = '/services'
r = self.get(resource_url)
self.assertValidServiceListResponse(r, ref=self.service)
self.head(resource_url, expected_status=http_client.OK)
def _create_random_service(self):
ref = unit.new_service_ref()
@ -354,11 +360,13 @@ class CatalogTestCase(test_v3.RestfulTestCase):
filtered_service = filtered_service_list[0]
self.assertEqual(target_ref['name'], filtered_service['name'])
def test_get_service(self):
"""Call ``GET /services/{service_id}``."""
r = self.get('/services/%(service_id)s' % {
'service_id': self.service_id})
def test_get_head_service(self):
"""Call ``GET & HEAD /services/{service_id}``."""
resource_url = '/services/%(service_id)s' % {
'service_id': self.service_id}
r = self.get(resource_url)
self.assertValidServiceResponse(r, self.service)
self.head(resource_url, expected_status=http_client.OK)
def test_update_service(self):
"""Call ``PATCH /services/{service_id}``."""
@ -376,10 +384,12 @@ class CatalogTestCase(test_v3.RestfulTestCase):
# endpoint crud tests
def test_list_endpoints(self):
"""Call ``GET /endpoints``."""
r = self.get('/endpoints')
def test_list_head_endpoints(self):
"""Call ``GET & HEAD /endpoints``."""
resource_url = '/endpoints'
r = self.get(resource_url)
self.assertValidEndpointListResponse(r, ref=self.endpoint)
self.head(resource_url, expected_status=http_client.OK)
def _create_random_endpoint(self, interface='public',
parent_region_id=None):
@ -591,12 +601,13 @@ class CatalogTestCase(test_v3.RestfulTestCase):
self.post('/endpoints', body={'endpoint': ref},
expected_status=http_client.BAD_REQUEST)
def test_get_endpoint(self):
"""Call ``GET /endpoints/{endpoint_id}``."""
r = self.get(
'/endpoints/%(endpoint_id)s' % {
'endpoint_id': self.endpoint_id})
def test_get_head_endpoint(self):
"""Call ``GET & HEAD /endpoints/{endpoint_id}``."""
resource_url = '/endpoints/%(endpoint_id)s' % {
'endpoint_id': self.endpoint_id}
r = self.get(resource_url)
self.assertValidEndpointResponse(r, self.endpoint)
self.head(resource_url, expected_status=http_client.OK)
def test_update_endpoint(self):
"""Call ``PATCH /endpoints/{endpoint_id}``."""

View File

@ -219,12 +219,13 @@ class IdentityTestCase(test_v3.RestfulTestCase):
self.post('/users', body={'user': {}},
expected_status=http_client.BAD_REQUEST)
def test_list_users(self):
"""Call ``GET /users``."""
def test_list_head_users(self):
"""Call ``GET & HEAD /users``."""
resource_url = '/users'
r = self.get(resource_url)
self.assertValidUserListResponse(r, ref=self.user,
resource_url=resource_url)
self.head(resource_url, expected_status=http_client.OK)
def test_list_users_with_multiple_backends(self):
"""Call ``GET /users`` when multiple backends is enabled.
@ -291,11 +292,13 @@ class IdentityTestCase(test_v3.RestfulTestCase):
self.assertValidUserListResponse(r, ref=user,
resource_url=resource_url)
def test_get_user(self):
"""Call ``GET /users/{user_id}``."""
r = self.get('/users/%(user_id)s' % {
'user_id': self.user['id']})
def test_get_head_user(self):
"""Call ``GET & HEAD /users/{user_id}``."""
resource_url = '/users/%(user_id)s' % {
'user_id': self.user['id']}
r = self.get(resource_url)
self.assertValidUserResponse(r, self.user)
self.head(resource_url, expected_status=http_client.OK)
def test_get_user_with_default_project(self):
"""Call ``GET /users/{user_id}`` making sure of default_project_id."""
@ -310,8 +313,8 @@ class IdentityTestCase(test_v3.RestfulTestCase):
self.put('/groups/%(group_id)s/users/%(user_id)s' % {
'group_id': self.group_id, 'user_id': self.user['id']})
def test_list_groups_for_user(self):
"""Call ``GET /users/{user_id}/groups``."""
def test_list_head_groups_for_user(self):
"""Call ``GET & HEAD /users/{user_id}/groups``."""
user1 = unit.create_user(self.identity_api,
domain_id=self.domain['id'])
user2 = unit.create_user(self.identity_api,
@ -331,6 +334,7 @@ class IdentityTestCase(test_v3.RestfulTestCase):
r = self.get(resource_url, auth=auth)
self.assertValidGroupListResponse(r, ref=self.group,
resource_url=resource_url)
self.head(resource_url, auth=auth, expected_status=http_client.OK)
# Administrator is allowed to list others' groups
resource_url = ('/users/%(user_id)s/groups' %
@ -338,14 +342,18 @@ class IdentityTestCase(test_v3.RestfulTestCase):
r = self.get(resource_url)
self.assertValidGroupListResponse(r, ref=self.group,
resource_url=resource_url)
self.head(resource_url, expected_status=http_client.OK)
# Ordinary users should not be allowed to list other's groups
auth = self.build_authentication_request(
user_id=user2['id'],
password=user2['password'])
r = self.get('/users/%(user_id)s/groups' % {
'user_id': user1['id']}, auth=auth,
expected_status=exception.ForbiddenAction.code)
resource_url = '/users/%(user_id)s/groups' % {
'user_id': user1['id']}
self.get(resource_url, auth=auth,
expected_status=exception.ForbiddenAction.code)
self.head(resource_url, auth=auth,
expected_status=exception.ForbiddenAction.code)
def test_check_user_in_group(self):
"""Call ``HEAD /groups/{group_id}/users/{user_id}``."""
@ -354,8 +362,8 @@ class IdentityTestCase(test_v3.RestfulTestCase):
self.head('/groups/%(group_id)s/users/%(user_id)s' % {
'group_id': self.group_id, 'user_id': self.user['id']})
def test_list_users_in_group(self):
"""Call ``GET /groups/{group_id}/users``."""
def test_list_head_users_in_group(self):
"""Call ``GET & HEAD /groups/{group_id}/users``."""
self.put('/groups/%(group_id)s/users/%(user_id)s' % {
'group_id': self.group_id, 'user_id': self.user['id']})
resource_url = ('/groups/%(group_id)s/users' %
@ -365,6 +373,7 @@ class IdentityTestCase(test_v3.RestfulTestCase):
resource_url=resource_url)
self.assertIn('/groups/%(group_id)s/users' % {
'group_id': self.group_id}, r.result['links']['self'])
self.head(resource_url, expected_status=http_client.OK)
def test_remove_user_from_group(self):
"""Call ``DELETE /groups/{group_id}/users/{user_id}``."""
@ -515,18 +524,21 @@ class IdentityTestCase(test_v3.RestfulTestCase):
self.post('/groups', body={'group': {}},
expected_status=http_client.BAD_REQUEST)
def test_list_groups(self):
"""Call ``GET /groups``."""
def test_list_head_groups(self):
"""Call ``GET & HEAD /groups``."""
resource_url = '/groups'
r = self.get(resource_url)
self.assertValidGroupListResponse(r, ref=self.group,
resource_url=resource_url)
self.head(resource_url, expected_status=http_client.OK)
def test_get_group(self):
"""Call ``GET /groups/{group_id}``."""
r = self.get('/groups/%(group_id)s' % {
'group_id': self.group_id})
def test_get_head_group(self):
"""Call ``GET & HEAD /groups/{group_id}``."""
resource_url = '/groups/%(group_id)s' % {
'group_id': self.group_id}
r = self.get(resource_url)
self.assertValidGroupResponse(r, self.group)
self.head(resource_url, expected_status=http_client.OK)
def test_update_group(self):
"""Call ``PATCH /groups/{group_id}``."""

View File

@ -15,6 +15,8 @@
import json
import uuid
from six.moves import http_client
from keystone.tests import unit
from keystone.tests.unit import test_v3
@ -38,16 +40,20 @@ class PolicyTestCase(test_v3.RestfulTestCase):
r = self.post('/policies', body={'policy': ref})
return self.assertValidPolicyResponse(r, ref)
def test_list_policies(self):
"""Call ``GET /policies``."""
r = self.get('/policies')
def test_list_head_policies(self):
"""Call ``GET & HEAD /policies``."""
resource_url = '/policies'
r = self.get(resource_url)
self.assertValidPolicyListResponse(r, ref=self.policy)
self.head(resource_url, expected_status=http_client.OK)
def test_get_policy(self):
"""Call ``GET /policies/{policy_id}``."""
r = self.get(
'/policies/%(policy_id)s' % {'policy_id': self.policy_id})
def test_get_head_policy(self):
"""Call ``GET & HEAD /policies/{policy_id}``."""
resource_url = ('/policies/%(policy_id)s' %
{'policy_id': self.policy_id})
r = self.get(resource_url)
self.assertValidPolicyResponse(r, self.policy)
self.head(resource_url, expected_status=http_client.OK)
def test_update_policy(self):
"""Call ``PATCH /policies/{policy_id}``."""

View File

@ -129,18 +129,21 @@ class ResourceTestCase(test_v3.RestfulTestCase,
self.assertValidDomainResponse(r)
self.assertIsNotNone(r.result['domain'])
def test_list_domains(self):
"""Call ``GET /domains``."""
def test_list_head_domains(self):
"""Call ``GET & HEAD /domains``."""
resource_url = '/domains'
r = self.get(resource_url)
self.assertValidDomainListResponse(r, ref=self.domain,
resource_url=resource_url)
self.head(resource_url, expected_status=http_client.OK)
def test_get_domain(self):
def test_get_head_domain(self):
"""Call ``GET /domains/{domain_id}``."""
r = self.get('/domains/%(domain_id)s' % {
'domain_id': self.domain_id})
resource_url = '/domains/%(domain_id)s' % {
'domain_id': self.domain_id}
r = self.get(resource_url)
self.assertValidDomainResponse(r, self.domain)
self.head(resource_url, expected_status=http_client.OK)
def test_update_domain(self):
"""Call ``PATCH /domains/{domain_id}``."""
@ -541,12 +544,13 @@ class ResourceTestCase(test_v3.RestfulTestCase,
# Project CRUD tests
def test_list_projects(self):
"""Call ``GET /projects``."""
def test_list_head_projects(self):
"""Call ``GET & HEAD /projects``."""
resource_url = '/projects'
r = self.get(resource_url)
self.assertValidProjectListResponse(r, ref=self.project,
resource_url=resource_url)
self.head(resource_url, expected_status=http_client.OK)
def test_create_project(self):
"""Call ``POST /projects``."""
@ -752,12 +756,13 @@ class ResourceTestCase(test_v3.RestfulTestCase,
"""Call ``POST /projects``."""
self._create_projects_hierarchy()
def test_get_project(self):
"""Call ``GET /projects/{project_id}``."""
r = self.get(
'/projects/%(project_id)s' % {
'project_id': self.project_id})
def test_get_head_project(self):
"""Call ``GET & HEAD /projects/{project_id}``."""
resource_url = '/projects/%(project_id)s' % {
'project_id': self.project_id}
r = self.get(resource_url)
self.assertValidProjectResponse(r, self.project)
self.head(resource_url, expected_status=http_client.OK)
def test_get_project_with_parents_as_list_with_invalid_id(self):
"""Call ``GET /projects/{project_id}?parents_as_list``."""