Split the assignments controller

This is the final part of the more comprehensive split of
assignments, which rationalizes both the backend and controllers.
In order to make this change easier for reviewers, it is divided
into a number of smaller patches. This patch divides up the
assignment controller, giving resource its own controller.

Previous patches have:
- Moved role management into its own manager and drivers
- Fixed incorrect doc strings for grant driver methods
- Updated controllers to call the new role manager
- Updated unit tests to call the new role manager
- Refactored the assignment manager and drivers enabling
  projects/domains to be split out
- Fixed incorrect comment about circular dependency between
  assignment and identity
- Moved the logically separated project and domain
  functionality into their own manager/backend (called resource).
- Removes unused pointer to assignment from identity driver
- Uddated controllers and managers to call the new resource
  manager
- Updated tests to call the new resource manager

Partially implements: bp pluggable-assignments
Change-Id: Ic7a4dbe9e39c1910ecc23b37d0b798955544fde4
This commit is contained in:
Henry Nash 2014-11-02 10:38:50 +00:00
parent 8ba0c166e5
commit f01cd89bd0
17 changed files with 537 additions and 384 deletions

View File

@ -36,14 +36,31 @@ Identity
--------
The Identity service provides auth credential validation and data about Users,
Groups, Projects, Domains and Roles, as well as any associated metadata.
Groups.
In the basic case all this data is managed by the service, allowing the service
to manage all the CRUD associated with the data.
In other cases, this data is pulled, by varying degrees, from an authoritative
backend service. An example of this would be when backending on LDAP. See
`LDAP Backend` below for more details.
In other cases from an authoritative backend service. An example of this would
be when backending on LDAP. See `LDAP Backend` below for more details.
Resource
--------
The Resource service provides data about Projects and Domains.
Like the Identity service, this data may either be managed directly by the
service or be pulled from another authoritative backend service, such as LDAP.
Assignment
----------
The Assignment service provides data about Roles and Role assignments to the
entities managed by the Identity and Resource services. Again, like these two
services, this data may either be managed directly by the Assignment service
or be pulled from another authoritative backend service, such as LDAP.
Token
@ -90,8 +107,12 @@ on the Keystone configuration.
* Assignment
* :mod:`keystone.assignment.controllers.DomainV3`
* :mod:`keystone.assignment.controllers.ProjectV3`
* :mod:`keystone.assignment.controllers.GrantAssignmentV3`
* :mod:`keystone.assignment.controllers.ProjectAssignmentV3`
* :mod:`keystone.assignment.controllers.TenantAssignment`
* :mod:`keystone.assignment.controllers.Role`
* :mod:`keystone.assignment.controllers.RoleAssignmentV2`
* :mod:`keystone.assignment.controllers.RoleAssignmentV3`
* :mod:`keystone.assignment.controllers.RoleV3`
* Authentication
@ -113,6 +134,11 @@ on the Keystone configuration.
* :mod:`keystone.policy.controllers.PolicyV3`
* Resource
* :mod:`keystone.resource.controllers.DomainV3`
* :mod:`keystone.resource.controllers.ProjectV3`
* Token
* :mod:`keystone.token.controllers.Auth`

View File

@ -36,27 +36,9 @@ CONF = config.CONF
LOG = log.getLogger(__name__)
@dependency.requires('assignment_api', 'identity_api', 'resource_api',
'token_provider_api')
class Tenant(controller.V2Controller):
@controller.v2_deprecated
def get_all_projects(self, context, **kw):
"""Gets a list of all tenants for an admin user."""
if 'name' in context['query_string']:
return self.get_project_by_name(
context, context['query_string'].get('name'))
self.assert_admin(context)
tenant_refs = self.resource_api.list_projects_in_domain(
CONF.identity.default_domain_id)
for tenant_ref in tenant_refs:
tenant_ref = self.filter_domain_id(tenant_ref)
params = {
'limit': context['query_string'].get('limit'),
'marker': context['query_string'].get('marker'),
}
return self._format_project_list(tenant_refs, **params)
@dependency.requires('assignment_api', 'identity_api', 'token_provider_api')
class TenantAssignment(controller.V2Controller):
"""The V2 Project APIs that are processing assignments."""
@controller.v2_deprecated
def get_projects_for_token(self, context, **kw):
@ -85,54 +67,7 @@ class Tenant(controller.V2Controller):
'limit': context['query_string'].get('limit'),
'marker': context['query_string'].get('marker'),
}
return self._format_project_list(tenant_refs, **params)
@controller.v2_deprecated
def get_project(self, context, tenant_id):
# TODO(termie): this stuff should probably be moved to middleware
self.assert_admin(context)
ref = self.resource_api.get_project(tenant_id)
return {'tenant': self.filter_domain_id(ref)}
@controller.v2_deprecated
def get_project_by_name(self, context, tenant_name):
self.assert_admin(context)
ref = self.resource_api.get_project_by_name(
tenant_name, CONF.identity.default_domain_id)
return {'tenant': self.filter_domain_id(ref)}
# CRUD Extension
@controller.v2_deprecated
def create_project(self, context, tenant):
tenant_ref = self._normalize_dict(tenant)
if 'name' not in tenant_ref or not tenant_ref['name']:
msg = _('Name field is required and cannot be empty')
raise exception.ValidationError(message=msg)
self.assert_admin(context)
tenant_ref['id'] = tenant_ref.get('id', uuid.uuid4().hex)
tenant = self.resource_api.create_project(
tenant_ref['id'],
self._normalize_domain_id(context, tenant_ref))
return {'tenant': self.filter_domain_id(tenant)}
@controller.v2_deprecated
def update_project(self, context, tenant_id, tenant):
self.assert_admin(context)
# Remove domain_id if specified - a v2 api caller should not
# be specifying that
clean_tenant = tenant.copy()
clean_tenant.pop('domain_id', None)
tenant_ref = self.resource_api.update_project(
tenant_id, clean_tenant)
return {'tenant': tenant_ref}
@controller.v2_deprecated
def delete_project(self, context, tenant_id):
self.assert_admin(context)
self.resource_api.delete_project(tenant_id)
return self.format_project_list(tenant_refs, **params)
@controller.v2_deprecated
def get_project_users(self, context, tenant_id, **kw):
@ -152,60 +87,11 @@ class Tenant(controller.V2Controller):
user_refs.append(self.v3_to_v2_user(user_ref))
return {'users': user_refs}
def _format_project_list(self, tenant_refs, **kwargs):
marker = kwargs.get('marker')
first_index = 0
if marker is not None:
for (marker_index, tenant) in enumerate(tenant_refs):
if tenant['id'] == marker:
# we start pagination after the marker
first_index = marker_index + 1
break
else:
msg = _('Marker could not be found')
raise exception.ValidationError(message=msg)
limit = kwargs.get('limit')
last_index = None
if limit is not None:
try:
limit = int(limit)
if limit < 0:
raise AssertionError()
except (ValueError, AssertionError):
msg = _('Invalid limit value')
raise exception.ValidationError(message=msg)
last_index = first_index + limit
tenant_refs = tenant_refs[first_index:last_index]
for x in tenant_refs:
if 'enabled' not in x:
x['enabled'] = True
o = {'tenants': tenant_refs,
'tenants_links': []}
return o
@dependency.requires('assignment_api', 'role_api')
class Role(controller.V2Controller):
"""The Role management APIs."""
# COMPAT(essex-3)
@controller.v2_deprecated
def get_user_roles(self, context, user_id, tenant_id):
"""Get the roles for a user and tenant pair.
Since we're trying to ignore the idea of user-only roles we're
not implementing them in hopes that the idea will die off.
"""
self.assert_admin(context)
roles = self.assignment_api.get_roles_for_user_and_project(
user_id, tenant_id)
return {'roles': [self.role_api.get_role(x)
for x in roles]}
# CRUD extension
@controller.v2_deprecated
def get_role(self, context, role_id):
self.assert_admin(context)
@ -235,6 +121,26 @@ class Role(controller.V2Controller):
self.assert_admin(context)
return {'roles': self.role_api.list_roles()}
@dependency.requires('assignment_api', 'resource_api', 'role_api')
class RoleAssignmentV2(controller.V2Controller):
"""The V2 Role APIs that are processing assignments."""
# COMPAT(essex-3)
@controller.v2_deprecated
def get_user_roles(self, context, user_id, tenant_id=None):
"""Get the roles for a user and tenant pair.
Since we're trying to ignore the idea of user-only roles we're
not implementing them in hopes that the idea will die off.
"""
self.assert_admin(context)
roles = self.assignment_api.get_roles_for_user_and_project(
user_id, tenant_id)
return {'roles': [self.role_api.get_role(x)
for x in roles]}
@controller.v2_deprecated
def add_role_to_user(self, context, user_id, role_id, tenant_id=None):
"""Add a role to a user and tenant pair.
@ -342,142 +248,29 @@ class Role(controller.V2Controller):
user_id, tenant_id, role_id)
@dependency.requires('resource_api')
class DomainV3(controller.V3Controller):
collection_name = 'domains'
member_name = 'domain'
def __init__(self):
super(DomainV3, self).__init__()
self.get_member_from_driver = self.resource_api.get_domain
@controller.protected()
@validation.validated(schema.domain_create, 'domain')
def create_domain(self, context, domain):
ref = self._assign_unique_id(self._normalize_dict(domain))
ref = self.resource_api.create_domain(ref['id'], ref)
return DomainV3.wrap_member(context, ref)
@controller.filterprotected('enabled', 'name')
def list_domains(self, context, filters):
hints = DomainV3.build_driver_hints(context, filters)
refs = self.resource_api.list_domains(hints=hints)
return DomainV3.wrap_collection(context, refs, hints=hints)
@controller.protected()
def get_domain(self, context, domain_id):
ref = self.resource_api.get_domain(domain_id)
return DomainV3.wrap_member(context, ref)
@controller.protected()
@validation.validated(schema.domain_update, 'domain')
def update_domain(self, context, domain_id, domain):
self._require_matching_id(domain_id, domain)
ref = self.resource_api.update_domain(domain_id, domain)
return DomainV3.wrap_member(context, ref)
@controller.protected()
def delete_domain(self, context, domain_id):
return self.resource_api.delete_domain(domain_id)
@dependency.requires('assignment_api', 'resource_api')
class ProjectV3(controller.V3Controller):
class ProjectAssignmentV3(controller.V3Controller):
"""The V3 Project APIs that are processing assignments."""
collection_name = 'projects'
member_name = 'project'
def __init__(self):
super(ProjectV3, self).__init__()
super(ProjectAssignmentV3, self).__init__()
self.get_member_from_driver = self.resource_api.get_project
@controller.protected()
@validation.validated(schema.project_create, 'project')
def create_project(self, context, project):
ref = self._assign_unique_id(self._normalize_dict(project))
ref = self._normalize_domain_id(context, ref)
ref = self.resource_api.create_project(ref['id'], ref)
return ProjectV3.wrap_member(context, ref)
@controller.filterprotected('domain_id', 'enabled', 'name',
'parent_id')
def list_projects(self, context, filters):
hints = ProjectV3.build_driver_hints(context, filters)
refs = self.resource_api.list_projects(hints=hints)
return ProjectV3.wrap_collection(context, refs, hints=hints)
@controller.filterprotected('enabled', 'name')
def list_user_projects(self, context, filters, user_id):
hints = ProjectV3.build_driver_hints(context, filters)
refs = self.assignment_api.list_projects_for_user(user_id, hints=hints)
return ProjectV3.wrap_collection(context, refs, hints=hints)
def _expand_project_ref(self, context, ref):
params = context['query_string']
parents_as_list = 'parents_as_list' in params and (
self.query_filter_is_true(params['parents_as_list']))
parents_as_ids = 'parents_as_ids' in params and (
self.query_filter_is_true(params['parents_as_ids']))
subtree_as_list = 'subtree_as_list' in params and (
self.query_filter_is_true(params['subtree_as_list']))
subtree_as_ids = 'subtree_as_ids' in params and (
self.query_filter_is_true(params['subtree_as_ids']))
# parents_as_list and parents_as_ids are mutually exclusive
if parents_as_list and parents_as_ids:
msg = _('Cannot use parents_as_list and parents_as_ids query '
'params at the same time.')
raise exception.ValidationError(msg)
# subtree_as_list and subtree_as_ids are mutually exclusive
if subtree_as_list and subtree_as_ids:
msg = _('Cannot use subtree_as_list and subtree_as_ids query '
'params at the same time.')
raise exception.ValidationError(msg)
user_id = self.get_auth_context(context).get('user_id')
if parents_as_list:
parents = self.resource_api.list_project_parents(
ref['id'], user_id)
ref['parents'] = [ProjectV3.wrap_member(context, p)
for p in parents]
elif parents_as_ids:
ref['parents'] = self.resource_api.get_project_parents_as_ids(ref)
if subtree_as_list:
subtree = self.resource_api.list_projects_in_subtree(
ref['id'], user_id)
ref['subtree'] = [ProjectV3.wrap_member(context, p)
for p in subtree]
elif subtree_as_ids:
ref['subtree'] = self.resource_api.get_projects_in_subtree_as_ids(
ref['id'])
@controller.protected()
def get_project(self, context, project_id):
ref = self.resource_api.get_project(project_id)
self._expand_project_ref(context, ref)
return ProjectV3.wrap_member(context, ref)
@controller.protected()
@validation.validated(schema.project_update, 'project')
def update_project(self, context, project_id, project):
self._require_matching_id(project_id, project)
self._require_matching_domain_id(
project_id, project, self.resource_api.get_project)
ref = self.resource_api.update_project(project_id, project)
return ProjectV3.wrap_member(context, ref)
@controller.protected()
def delete_project(self, context, project_id):
return self.resource_api.delete_project(project_id)
hints = ProjectAssignmentV3.build_driver_hints(context, filters)
refs = self.assignment_api.list_projects_for_user(user_id,
hints=hints)
return ProjectAssignmentV3.wrap_collection(context, refs, hints=hints)
@dependency.requires('assignment_api', 'identity_api', 'resource_api',
'role_api')
@dependency.requires('role_api')
class RoleV3(controller.V3Controller):
"""The V3 Role CRUD APIs."""
collection_name = 'roles'
member_name = 'role'
@ -516,6 +309,19 @@ class RoleV3(controller.V3Controller):
def delete_role(self, context, role_id):
self.role_api.delete_role(role_id)
@dependency.requires('assignment_api', 'identity_api', 'resource_api',
'role_api')
class GrantAssignmentV3(controller.V3Controller):
"""The V3 Grant Assignment APIs."""
collection_name = 'roles'
member_name = 'role'
def __init__(self):
super(GrantAssignmentV3, self).__init__()
self.get_member_from_driver = self.role_api.get_role
def _require_domain_xor_project(self, domain_id, project_id):
if (domain_id and project_id) or (not domain_id and not project_id):
msg = _('Specify a domain or project, not both')
@ -582,7 +388,7 @@ class RoleV3(controller.V3Controller):
refs = self.assignment_api.list_grants(
user_id, group_id, domain_id, project_id,
self._check_if_inherited(context))
return RoleV3.wrap_collection(context, refs)
return GrantAssignmentV3.wrap_collection(context, refs)
@controller.protected(callback=_check_grant_protection)
def check_grant(self, context, role_id, user_id=None,
@ -613,6 +419,7 @@ class RoleV3(controller.V3Controller):
@dependency.requires('assignment_api', 'identity_api', 'resource_api')
class RoleAssignmentV3(controller.V3Controller):
"""The V3 Role Assignment APIs, really just list_role_assignment()."""
# TODO(henry-nash): The current implementation does not provide a full
# first class entity for role-assignment. There is no role_assignment_id

View File

@ -31,7 +31,7 @@ build_os_inherit_relation = functools.partial(
class Public(wsgi.ComposableRouter):
def add_routes(self, mapper):
tenant_controller = controllers.Tenant()
tenant_controller = controllers.TenantAssignment()
mapper.connect('/tenants',
controller=tenant_controller,
action='get_projects_for_token',
@ -40,19 +40,8 @@ class Public(wsgi.ComposableRouter):
class Admin(wsgi.ComposableRouter):
def add_routes(self, mapper):
# Tenant Operations
tenant_controller = controllers.Tenant()
mapper.connect('/tenants',
controller=tenant_controller,
action='get_all_projects',
conditions=dict(method=['GET']))
mapper.connect('/tenants/{tenant_id}',
controller=tenant_controller,
action='get_project',
conditions=dict(method=['GET']))
# Role Operations
roles_controller = controllers.Role()
roles_controller = controllers.RoleAssignmentV2()
mapper.connect('/tenants/{tenant_id}/users/{user_id}/roles',
controller=roles_controller,
action='get_user_roles',
@ -66,17 +55,8 @@ class Admin(wsgi.ComposableRouter):
class Routers(wsgi.RoutersBase):
def append_v3_routers(self, mapper, routers):
routers.append(
router.Router(controllers.DomainV3(),
'domains', 'domain',
resource_descriptions=self.v3_resources))
project_controller = controllers.ProjectV3()
routers.append(
router.Router(project_controller,
'projects', 'project',
resource_descriptions=self.v3_resources))
project_controller = controllers.ProjectAssignmentV3()
self._add_resource(
mapper, project_controller,
path='/users/{user_id}/projects',
@ -86,13 +66,13 @@ class Routers(wsgi.RoutersBase):
'user_id': json_home.Parameters.USER_ID,
})
role_controller = controllers.RoleV3()
routers.append(
router.Router(role_controller, 'roles', 'role',
router.Router(controllers.RoleV3(), 'roles', 'role',
resource_descriptions=self.v3_resources))
grant_controller = controllers.GrantAssignmentV3()
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/projects/{project_id}/users/{user_id}/roles/{role_id}',
get_head_action='check_grant',
put_action='create_grant',
@ -104,7 +84,7 @@ class Routers(wsgi.RoutersBase):
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/projects/{project_id}/groups/{group_id}/roles/{role_id}',
get_head_action='check_grant',
put_action='create_grant',
@ -116,7 +96,7 @@ class Routers(wsgi.RoutersBase):
'role_id': json_home.Parameters.ROLE_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/projects/{project_id}/users/{user_id}/roles',
get_action='list_grants',
rel=json_home.build_v3_resource_relation('project_user_roles'),
@ -125,7 +105,7 @@ class Routers(wsgi.RoutersBase):
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/projects/{project_id}/groups/{group_id}/roles',
get_action='list_grants',
rel=json_home.build_v3_resource_relation('project_group_roles'),
@ -134,7 +114,7 @@ class Routers(wsgi.RoutersBase):
'project_id': json_home.Parameters.PROJECT_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/domains/{domain_id}/users/{user_id}/roles/{role_id}',
get_head_action='check_grant',
put_action='create_grant',
@ -146,7 +126,7 @@ class Routers(wsgi.RoutersBase):
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/domains/{domain_id}/groups/{group_id}/roles/{role_id}',
get_head_action='check_grant',
put_action='create_grant',
@ -158,7 +138,7 @@ class Routers(wsgi.RoutersBase):
'role_id': json_home.Parameters.ROLE_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/domains/{domain_id}/users/{user_id}/roles',
get_action='list_grants',
rel=json_home.build_v3_resource_relation('domain_user_roles'),
@ -167,7 +147,7 @@ class Routers(wsgi.RoutersBase):
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/domains/{domain_id}/groups/{group_id}/roles',
get_action='list_grants',
rel=json_home.build_v3_resource_relation('domain_group_roles'),
@ -184,7 +164,7 @@ class Routers(wsgi.RoutersBase):
if config.CONF.os_inherit.enabled:
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/OS-INHERIT/domains/{domain_id}/users/{user_id}/roles/'
'{role_id}/inherited_to_projects',
get_head_action='check_grant',
@ -198,7 +178,7 @@ class Routers(wsgi.RoutersBase):
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/OS-INHERIT/domains/{domain_id}/groups/{group_id}/roles/'
'{role_id}/inherited_to_projects',
get_head_action='check_grant',
@ -212,7 +192,7 @@ class Routers(wsgi.RoutersBase):
'role_id': json_home.Parameters.ROLE_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/OS-INHERIT/domains/{domain_id}/groups/{group_id}/roles/'
'inherited_to_projects',
get_action='list_grants',
@ -223,7 +203,7 @@ class Routers(wsgi.RoutersBase):
'group_id': json_home.Parameters.GROUP_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/OS-INHERIT/domains/{domain_id}/users/{user_id}/roles/'
'inherited_to_projects',
get_action='list_grants',
@ -234,7 +214,7 @@ class Routers(wsgi.RoutersBase):
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/OS-INHERIT/projects/{project_id}/users/{user_id}/roles/'
'{role_id}/inherited_to_projects',
get_head_action='check_grant',
@ -248,7 +228,7 @@ class Routers(wsgi.RoutersBase):
'role_id': json_home.Parameters.ROLE_ID,
})
self._add_resource(
mapper, role_controller,
mapper, grant_controller,
path='/OS-INHERIT/projects/{project_id}/groups/{group_id}/'
'roles/{role_id}/inherited_to_projects',
get_head_action='check_grant',

View File

@ -10,70 +10,9 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystone.common import validation
from keystone.common.validation import parameter_types
_project_properties = {
'description': validation.nullable(parameter_types.description),
# NOTE(lbragstad): domain_id isn't nullable according to some backends.
# The identity-api should be updated to be consistent with the
# implementation.
'domain_id': parameter_types.id_string,
'enabled': parameter_types.boolean,
'parent_id': validation.nullable(parameter_types.id_string),
'name': {
'type': 'string',
'minLength': 1,
'maxLength': 64
}
}
project_create = {
'type': 'object',
'properties': _project_properties,
# NOTE(lbragstad): A project name is the only parameter required for
# project creation according to the Identity V3 API. We should think
# about using the maxProperties validator here, and in update.
'required': ['name'],
'additionalProperties': True
}
project_update = {
'type': 'object',
'properties': _project_properties,
# NOTE(lbragstad) Make sure at least one property is being updated
'minProperties': 1,
'additionalProperties': True
}
_domain_properties = {
'description': validation.nullable(parameter_types.description),
'enabled': parameter_types.boolean,
'name': {
'type': 'string',
'minLength': 1,
'maxLength': 64
}
}
domain_create = {
'type': 'object',
'properties': _domain_properties,
# TODO(lbragstad): According to the V3 API spec, name isn't required but
# the current implementation in assignment.controller:DomainV3 requires a
# name for the domain.
'required': ['name'],
'additionalProperties': True
}
domain_update = {
'type': 'object',
'properties': _domain_properties,
'minProperties': 1,
'additionalProperties': True
}
_role_properties = {
'name': parameter_types.name
}

View File

@ -20,7 +20,6 @@ from oslo_utils import importutils
from oslo_utils import timeutils
import six
from keystone.assignment import controllers as assignment_controllers
from keystone.common import controller
from keystone.common import dependency
from keystone.common import wsgi
@ -29,6 +28,7 @@ from keystone.contrib import federation
from keystone import exception
from keystone.i18n import _, _LI, _LW
from keystone.openstack.common import log
from keystone.resource import controllers as resource_controllers
LOG = log.getLogger(__name__)
@ -582,7 +582,7 @@ class Auth(controller.V3Controller):
grp_refs = self.assignment_api.list_projects_for_groups(group_ids)
refs = self._combine_lists_uniquely(user_refs, grp_refs)
return assignment_controllers.ProjectV3.wrap_collection(context, refs)
return resource_controllers.ProjectV3.wrap_collection(context, refs)
@controller.protected()
def get_auth_domains(self, context):
@ -603,7 +603,7 @@ class Auth(controller.V3Controller):
grp_refs = self.assignment_api.list_domains_for_groups(group_ids)
refs = self._combine_lists_uniquely(user_refs, grp_refs)
return assignment_controllers.DomainV3.wrap_collection(context, refs)
return resource_controllers.DomainV3.wrap_collection(context, refs)
@controller.protected()
def get_auth_catalog(self, context):

View File

@ -287,6 +287,41 @@ class V2Controller(wsgi.Application):
else:
raise ValueError(_('Expected dict or list: %s') % type(ref))
def format_project_list(self, tenant_refs, **kwargs):
"""Format a v2 style project list, including marker/limits."""
marker = kwargs.get('marker')
first_index = 0
if marker is not None:
for (marker_index, tenant) in enumerate(tenant_refs):
if tenant['id'] == marker:
# we start pagination after the marker
first_index = marker_index + 1
break
else:
msg = _('Marker could not be found')
raise exception.ValidationError(message=msg)
limit = kwargs.get('limit')
last_index = None
if limit is not None:
try:
limit = int(limit)
if limit < 0:
raise AssertionError()
except (ValueError, AssertionError):
msg = _('Invalid limit value')
raise exception.ValidationError(message=msg)
last_index = first_index + limit
tenant_refs = tenant_refs[first_index:last_index]
for x in tenant_refs:
if 'enabled' not in x:
x['enabled'] = True
o = {'tenants': tenant_refs,
'tenants_links': []}
return o
@dependency.requires('policy_api', 'token_provider_api')
class V3Controller(wsgi.Application):

View File

@ -17,6 +17,7 @@ from keystone import catalog
from keystone.common import extension
from keystone.common import wsgi
from keystone import identity
from keystone import resource
extension.register_admin_extension(
@ -47,9 +48,12 @@ class CrudExtension(wsgi.ExtensionRouter):
"""
def add_routes(self, mapper):
tenant_controller = assignment.controllers.Tenant()
tenant_controller = resource.controllers.Tenant()
assignment_tenant_controller = (
assignment.controllers.TenantAssignment())
user_controller = identity.controllers.User()
role_controller = assignment.controllers.Role()
assignment_role_controller = assignment.controllers.RoleAssignmentV2()
service_controller = catalog.controllers.Service()
endpoint_controller = catalog.controllers.Endpoint()
@ -71,7 +75,7 @@ class CrudExtension(wsgi.ExtensionRouter):
conditions=dict(method=['DELETE']))
mapper.connect(
'/tenants/{tenant_id}/users',
controller=tenant_controller,
controller=assignment_tenant_controller,
action='get_project_users',
conditions=dict(method=['GET']))
@ -137,41 +141,41 @@ class CrudExtension(wsgi.ExtensionRouter):
# User Roles
mapper.connect(
'/users/{user_id}/roles/OS-KSADM/{role_id}',
controller=role_controller,
controller=assignment_role_controller,
action='add_role_to_user',
conditions=dict(method=['PUT']))
mapper.connect(
'/users/{user_id}/roles/OS-KSADM/{role_id}',
controller=role_controller,
controller=assignment_role_controller,
action='remove_role_from_user',
conditions=dict(method=['DELETE']))
# COMPAT(diablo): User Roles
mapper.connect(
'/users/{user_id}/roleRefs',
controller=role_controller,
controller=assignment_role_controller,
action='get_role_refs',
conditions=dict(method=['GET']))
mapper.connect(
'/users/{user_id}/roleRefs',
controller=role_controller,
controller=assignment_role_controller,
action='create_role_ref',
conditions=dict(method=['POST']))
mapper.connect(
'/users/{user_id}/roleRefs/{role_ref_id}',
controller=role_controller,
controller=assignment_role_controller,
action='delete_role_ref',
conditions=dict(method=['DELETE']))
# User-Tenant Roles
mapper.connect(
'/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}',
controller=role_controller,
controller=assignment_role_controller,
action='add_role_to_user',
conditions=dict(method=['PUT']))
mapper.connect(
'/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}',
controller=role_controller,
controller=assignment_role_controller,
action='remove_role_from_user',
conditions=dict(method=['DELETE']))

View File

@ -14,12 +14,12 @@
import six
from keystone import assignment
from keystone.catalog import controllers as catalog_controllers
from keystone.common import controller
from keystone.common import dependency
from keystone import exception
from keystone import notifications
from keystone import resource
@dependency.requires('catalog_api', 'endpoint_filter_api', 'resource_api')
@ -135,8 +135,8 @@ class EndpointFilterV3Controller(_ControllerBase):
projects = [self.resource_api.get_project(
ref['project_id']) for ref in refs]
return assignment.controllers.ProjectV3.wrap_collection(context,
projects)
return resource.controllers.ProjectV3.wrap_collection(context,
projects)
class EndpointGroupV3Controller(_ControllerBase):
@ -226,8 +226,8 @@ class EndpointGroupV3Controller(_ControllerBase):
endpoint_group_ref['project_id'])
if project:
projects.append(project)
return assignment.controllers.ProjectV3.wrap_collection(context,
projects)
return resource.controllers.ProjectV3.wrap_collection(context,
projects)
@controller.protected()
def list_endpoints_associated_with_endpoint_group(self,

View File

@ -321,12 +321,12 @@ class DomainV3(controller.V3Controller):
@dependency.requires('assignment_api', 'resource_api')
class ProjectV3(controller.V3Controller):
class ProjectAssignmentV3(controller.V3Controller):
collection_name = 'projects'
member_name = 'project'
def __init__(self):
super(ProjectV3, self).__init__()
super(ProjectAssignmentV3, self).__init__()
self.get_member_from_driver = self.resource_api.get_project
@controller.protected()
@ -340,7 +340,7 @@ class ProjectV3(controller.V3Controller):
auth_context = context['environment'][authorization.AUTH_CONTEXT_ENV]
projects = self.assignment_api.list_projects_for_groups(
auth_context['group_ids'])
return ProjectV3.wrap_collection(context, projects)
return ProjectAssignmentV3.wrap_collection(context, projects)
@dependency.requires('federation_api')

View File

@ -89,7 +89,7 @@ class FederationExtension(wsgi.V3ExtensionRouter):
idp_controller = controllers.IdentityProvider()
protocol_controller = controllers.FederationProtocol()
mapping_controller = controllers.MappingController()
project_controller = controllers.ProjectV3()
project_controller = controllers.ProjectAssignmentV3()
domain_controller = controllers.DomainV3()
saml_metadata_controller = controllers.SAMLMetadataV3()
sp_controller = controllers.ServiceProvider()

View File

@ -10,4 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystone.resource import controllers # noqa
from keystone.resource.core import * # noqa
from keystone.resource import routers # noqa

View File

@ -0,0 +1,227 @@
# Copyright 2013 Metacloud, Inc.
# Copyright 2012 OpenStack Foundation
#
# 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.
"""Workflow Logic the Resource service."""
import uuid
from keystone.common import controller
from keystone.common import dependency
from keystone.common import validation
from keystone import config
from keystone import exception
from keystone.i18n import _
from keystone.openstack.common import log
from keystone.resource import schema
CONF = config.CONF
LOG = log.getLogger(__name__)
@dependency.requires('resource_api')
class Tenant(controller.V2Controller):
@controller.v2_deprecated
def get_all_projects(self, context, **kw):
"""Gets a list of all tenants for an admin user."""
if 'name' in context['query_string']:
return self.get_project_by_name(
context, context['query_string'].get('name'))
self.assert_admin(context)
tenant_refs = self.resource_api.list_projects_in_domain(
CONF.identity.default_domain_id)
for tenant_ref in tenant_refs:
tenant_ref = self.filter_domain_id(tenant_ref)
params = {
'limit': context['query_string'].get('limit'),
'marker': context['query_string'].get('marker'),
}
return self.format_project_list(tenant_refs, **params)
@controller.v2_deprecated
def get_project(self, context, tenant_id):
# TODO(termie): this stuff should probably be moved to middleware
self.assert_admin(context)
ref = self.resource_api.get_project(tenant_id)
return {'tenant': self.filter_domain_id(ref)}
@controller.v2_deprecated
def get_project_by_name(self, context, tenant_name):
self.assert_admin(context)
ref = self.resource_api.get_project_by_name(
tenant_name, CONF.identity.default_domain_id)
return {'tenant': self.filter_domain_id(ref)}
# CRUD Extension
@controller.v2_deprecated
def create_project(self, context, tenant):
tenant_ref = self._normalize_dict(tenant)
if 'name' not in tenant_ref or not tenant_ref['name']:
msg = _('Name field is required and cannot be empty')
raise exception.ValidationError(message=msg)
self.assert_admin(context)
tenant_ref['id'] = tenant_ref.get('id', uuid.uuid4().hex)
tenant = self.resource_api.create_project(
tenant_ref['id'],
self._normalize_domain_id(context, tenant_ref))
return {'tenant': self.filter_domain_id(tenant)}
@controller.v2_deprecated
def update_project(self, context, tenant_id, tenant):
self.assert_admin(context)
# Remove domain_id if specified - a v2 api caller should not
# be specifying that
clean_tenant = tenant.copy()
clean_tenant.pop('domain_id', None)
tenant_ref = self.resource_api.update_project(
tenant_id, clean_tenant)
return {'tenant': tenant_ref}
@controller.v2_deprecated
def delete_project(self, context, tenant_id):
self.assert_admin(context)
self.resource_api.delete_project(tenant_id)
@dependency.requires('resource_api')
class DomainV3(controller.V3Controller):
collection_name = 'domains'
member_name = 'domain'
def __init__(self):
super(DomainV3, self).__init__()
self.get_member_from_driver = self.resource_api.get_domain
@controller.protected()
@validation.validated(schema.domain_create, 'domain')
def create_domain(self, context, domain):
ref = self._assign_unique_id(self._normalize_dict(domain))
ref = self.resource_api.create_domain(ref['id'], ref)
return DomainV3.wrap_member(context, ref)
@controller.filterprotected('enabled', 'name')
def list_domains(self, context, filters):
hints = DomainV3.build_driver_hints(context, filters)
refs = self.resource_api.list_domains(hints=hints)
return DomainV3.wrap_collection(context, refs, hints=hints)
@controller.protected()
def get_domain(self, context, domain_id):
ref = self.resource_api.get_domain(domain_id)
return DomainV3.wrap_member(context, ref)
@controller.protected()
@validation.validated(schema.domain_update, 'domain')
def update_domain(self, context, domain_id, domain):
self._require_matching_id(domain_id, domain)
ref = self.resource_api.update_domain(domain_id, domain)
return DomainV3.wrap_member(context, ref)
@controller.protected()
def delete_domain(self, context, domain_id):
return self.resource_api.delete_domain(domain_id)
@dependency.requires('resource_api')
class ProjectV3(controller.V3Controller):
collection_name = 'projects'
member_name = 'project'
def __init__(self):
super(ProjectV3, self).__init__()
self.get_member_from_driver = self.resource_api.get_project
@controller.protected()
@validation.validated(schema.project_create, 'project')
def create_project(self, context, project):
ref = self._assign_unique_id(self._normalize_dict(project))
ref = self._normalize_domain_id(context, ref)
ref = self.resource_api.create_project(ref['id'], ref)
return ProjectV3.wrap_member(context, ref)
@controller.filterprotected('domain_id', 'enabled', 'name',
'parent_id')
def list_projects(self, context, filters):
hints = ProjectV3.build_driver_hints(context, filters)
refs = self.resource_api.list_projects(hints=hints)
return ProjectV3.wrap_collection(context, refs, hints=hints)
def _expand_project_ref(self, context, ref):
params = context['query_string']
parents_as_list = 'parents_as_list' in params and (
self.query_filter_is_true(params['parents_as_list']))
parents_as_ids = 'parents_as_ids' in params and (
self.query_filter_is_true(params['parents_as_ids']))
subtree_as_list = 'subtree_as_list' in params and (
self.query_filter_is_true(params['subtree_as_list']))
subtree_as_ids = 'subtree_as_ids' in params and (
self.query_filter_is_true(params['subtree_as_ids']))
# parents_as_list and parents_as_ids are mutually exclusive
if parents_as_list and parents_as_ids:
msg = _('Cannot use parents_as_list and parents_as_ids query '
'params at the same time.')
raise exception.ValidationError(msg)
# subtree_as_list and subtree_as_ids are mutually exclusive
if subtree_as_list and subtree_as_ids:
msg = _('Cannot use subtree_as_list and subtree_as_ids query '
'params at the same time.')
raise exception.ValidationError(msg)
user_id = self.get_auth_context(context).get('user_id')
if parents_as_list:
parents = self.resource_api.list_project_parents(
ref['id'], user_id)
ref['parents'] = [ProjectV3.wrap_member(context, p)
for p in parents]
elif parents_as_ids:
ref['parents'] = self.resource_api.get_project_parents_as_ids(ref)
if subtree_as_list:
subtree = self.resource_api.list_projects_in_subtree(
ref['id'], user_id)
ref['subtree'] = [ProjectV3.wrap_member(context, p)
for p in subtree]
elif subtree_as_ids:
ref['subtree'] = self.resource_api.get_projects_in_subtree_as_ids(
ref['id'])
@controller.protected()
def get_project(self, context, project_id):
ref = self.resource_api.get_project(project_id)
self._expand_project_ref(context, ref)
return ProjectV3.wrap_member(context, ref)
@controller.protected()
@validation.validated(schema.project_update, 'project')
def update_project(self, context, project_id, project):
self._require_matching_id(project_id, project)
self._require_matching_domain_id(
project_id, project, self.resource_api.get_project)
ref = self.resource_api.update_project(project_id, project)
return ProjectV3.wrap_member(context, ref)
@controller.protected()
def delete_project(self, context, project_id):
return self.resource_api.delete_project(project_id)

View File

@ -0,0 +1,48 @@
# Copyright 2013 Metacloud, Inc.
# Copyright 2012 OpenStack Foundation
#
# 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.
"""WSGI Routers for the Resource service."""
from keystone.common import router
from keystone.common import wsgi
from keystone.resource import controllers
class Admin(wsgi.ComposableRouter):
def add_routes(self, mapper):
# Tenant Operations
tenant_controller = controllers.Tenant()
mapper.connect('/tenants',
controller=tenant_controller,
action='get_all_projects',
conditions=dict(method=['GET']))
mapper.connect('/tenants/{tenant_id}',
controller=tenant_controller,
action='get_project',
conditions=dict(method=['GET']))
class Routers(wsgi.RoutersBase):
def append_v3_routers(self, mapper, routers):
routers.append(
router.Router(controllers.DomainV3(),
'domains', 'domain',
resource_descriptions=self.v3_resources))
routers.append(
router.Router(controllers.ProjectV3(),
'projects', 'project',
resource_descriptions=self.v3_resources))

View File

@ -0,0 +1,75 @@
# 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.common import validation
from keystone.common.validation import parameter_types
_project_properties = {
'description': validation.nullable(parameter_types.description),
# NOTE(lbragstad): domain_id isn't nullable according to some backends.
# The identity-api should be updated to be consistent with the
# implementation.
'domain_id': parameter_types.id_string,
'enabled': parameter_types.boolean,
'parent_id': validation.nullable(parameter_types.id_string),
'name': {
'type': 'string',
'minLength': 1,
'maxLength': 64
}
}
project_create = {
'type': 'object',
'properties': _project_properties,
# NOTE(lbragstad): A project name is the only parameter required for
# project creation according to the Identity V3 API. We should think
# about using the maxProperties validator here, and in update.
'required': ['name'],
'additionalProperties': True
}
project_update = {
'type': 'object',
'properties': _project_properties,
# NOTE(lbragstad) Make sure at least one property is being updated
'minProperties': 1,
'additionalProperties': True
}
_domain_properties = {
'description': validation.nullable(parameter_types.description),
'enabled': parameter_types.boolean,
'name': {
'type': 'string',
'minLength': 1,
'maxLength': 64
}
}
domain_create = {
'type': 'object',
'properties': _domain_properties,
# TODO(lbragstad): According to the V3 API spec, name isn't required but
# the current implementation in assignment.controller:DomainV3 requires a
# name for the domain.
'required': ['name'],
'additionalProperties': True
}
domain_update = {
'type': 'object',
'properties': _domain_properties,
'minProperties': 1,
'additionalProperties': True
}

View File

@ -28,6 +28,7 @@ from keystone import credential
from keystone import identity
from keystone.openstack.common import log
from keystone import policy
from keystone import resource
from keystone import routers
from keystone import token
from keystone import trust
@ -78,6 +79,7 @@ def admin_app_factory(global_conf, **local_conf):
[identity.routers.Admin(),
assignment.routers.Admin(),
token.routers.Router(),
resource.routers.Admin(),
routers.VersionV2('admin'),
routers.Extension()])
@ -101,7 +103,8 @@ def v3_app_factory(global_conf, **local_conf):
sub_routers = []
_routers = []
router_modules = [assignment, auth, catalog, credential, identity, policy]
router_modules = [assignment, auth, catalog, credential, identity, policy,
resource]
if CONF.trust.enabled:
router_modules.append(trust)

View File

@ -15,7 +15,8 @@
import uuid
from keystone.assignment import controllers
from keystone.assignment import controllers as assignment_controllers
from keystone.resource import controllers as resource_controllers
from keystone import tests
from keystone.tests import default_fixtures
from keystone.tests.ksfixtures import database
@ -35,8 +36,11 @@ class TenantTestCase(tests.TestCase):
self.useFixture(database.Database())
self.load_backends()
self.load_fixtures(default_fixtures)
self.tenant_controller = controllers.Tenant()
self.role_controller = controllers.Role()
self.tenant_controller = resource_controllers.Tenant()
self.assignment_tenant_controller = (
assignment_controllers.TenantAssignment())
self.assignment_role_controller = (
assignment_controllers.RoleAssignmentV2())
def test_get_project_users_no_user(self):
"""get_project_users when user doesn't exist.
@ -47,17 +51,19 @@ class TenantTestCase(tests.TestCase):
"""
project_id = self.tenant_bar['id']
orig_project_users = self.tenant_controller.get_project_users(
_ADMIN_CONTEXT, project_id)
orig_project_users = (
self.assignment_tenant_controller.get_project_users(_ADMIN_CONTEXT,
project_id))
# Assign a role to a user that doesn't exist to the `bar` project.
user_id = uuid.uuid4().hex
self.role_controller.add_role_to_user(
self.assignment_role_controller.add_role_to_user(
_ADMIN_CONTEXT, user_id, self.role_other['id'], project_id)
new_project_users = self.tenant_controller.get_project_users(
_ADMIN_CONTEXT, project_id)
new_project_users = (
self.assignment_tenant_controller.get_project_users(_ADMIN_CONTEXT,
project_id))
# The new user isn't included in the result, so no change.
# asserting that the expected values appear in the list,

View File

@ -23,6 +23,7 @@ from keystone.common.validation import validators
from keystone.credential import schema as credential_schema
from keystone import exception
from keystone.policy import schema as policy_schema
from keystone.resource import schema as resource_schema
from keystone.trust import schema as trust_schema
"""Example model to validate create requests against. Assume that this is
@ -298,8 +299,8 @@ class ProjectValidationTestCase(testtools.TestCase):
self.project_name = 'My Project'
create = assignment_schema.project_create
update = assignment_schema.project_update
create = resource_schema.project_create
update = resource_schema.project_update
self.create_project_validator = validators.SchemaValidator(create)
self.update_project_validator = validators.SchemaValidator(update)
@ -425,8 +426,8 @@ class DomainValidationTestCase(testtools.TestCase):
self.domain_name = 'My Domain'
create = assignment_schema.domain_create
update = assignment_schema.domain_update
create = resource_schema.domain_create
update = resource_schema.domain_update
self.create_domain_validator = validators.SchemaValidator(create)
self.update_domain_validator = validators.SchemaValidator(update)