backend for policy endpoint extension

This patch implements the sql backend and manager level for the
policy endpoint extension.  The controller level will be added in
a follow on patch.

Partially implememts: bp endpoint-policy
Change-Id: I5024b3c85da84ea7f16a065819a3a22cf0d15306
This commit is contained in:
Henry Nash 2014-08-19 19:01:11 +01:00
parent 06676f6d9e
commit 9f9911328b
15 changed files with 1003 additions and 0 deletions

View File

@ -697,6 +697,16 @@
#return_all_endpoints_if_no_filter=true
[endpoint_policy]
#
# Options defined in keystone
#
# Endpoint Policy backend driver (string value)
#driver=keystone.contrib.endpoint_policy.backends.sql.EndpointPolicy
[federation]
#

View File

@ -15,6 +15,7 @@ from keystone import auth
from keystone import catalog
from keystone.common import cache
from keystone.contrib import endpoint_filter
from keystone.contrib import endpoint_policy
from keystone import credential
from keystone import identity
from keystone import policy
@ -37,6 +38,7 @@ def load_backends():
catalog_api=catalog.Manager(),
credential_api=credential.Manager(),
endpoint_filter_api=endpoint_filter.Manager(),
endpoint_policy_api=endpoint_policy.Manager(),
id_generator_api=identity.generator.Manager(),
id_mapping_api=identity.MappingManager(),
identity_api=_IDENTITY_API,

View File

@ -454,6 +454,12 @@ FILE_OPTIONS = {
help='Toggle to return all active endpoints if no filter '
'exists.'),
],
'endpoint_policy': [
cfg.StrOpt('driver',
default='keystone.contrib.endpoint_policy.backends'
'.sql.EndpointPolicy',
help='Endpoint policy backend driver'),
],
'stats': [
cfg.StrOpt('driver',
default=('keystone.contrib.stats.backends'

View File

@ -0,0 +1,15 @@
# Copyright 2014 IBM Corp.
#
# 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.endpoint_policy.core import * # noqa

View File

@ -0,0 +1,135 @@
# Copyright 2014 IBM Corp.
#
# 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 uuid
from keystone.common import sql
from keystone import exception
class PolicyAssociation(sql.ModelBase, sql.ModelDictMixin):
__tablename__ = 'policy_association'
attributes = ['policy_id', 'endpoint_id', 'region_id', 'service_id']
# The id column is never exposed outside this module. It only exists to
# provide a primary key, given that the real columns we would like to use
# (endpoint_id, service_id, region_id) can be null
id = sql.Column(sql.String(64), primary_key=True)
policy_id = sql.Column(sql.String(64), nullable=False)
endpoint_id = sql.Column(sql.String(64), nullable=True)
service_id = sql.Column(sql.String(64), nullable=True)
region_id = sql.Column(sql.String(64), nullable=True)
__table_args__ = (sql.UniqueConstraint('endpoint_id', 'service_id',
'region_id'), {})
def to_dict(self):
"""Returns the model's attributes as a dictionary.
We override the standard method in order to hide the id column,
since this only exists to provide the table with a primary key.
"""
d = {}
for attr in self.__class__.attributes:
d[attr] = getattr(self, attr)
return d
class EndpointPolicy(object):
def create_policy_association(self, policy_id, endpoint_id=None,
service_id=None, region_id=None):
with sql.transaction() as session:
try:
# See if there is already a row for this association, and if
# so, update it with the new policy_id
query = session.query(PolicyAssociation)
query = query.filter_by(endpoint_id=endpoint_id)
query = query.filter_by(service_id=service_id)
query = query.filter_by(region_id=region_id)
association = query.one()
association.policy_id = policy_id
except sql.NotFound:
association = PolicyAssociation(id=uuid.uuid4().hex,
policy_id=policy_id,
endpoint_id=endpoint_id,
service_id=service_id,
region_id=region_id)
session.add(association)
def check_policy_association(self, policy_id, endpoint_id=None,
service_id=None, region_id=None):
with sql.transaction() as session:
query = session.query(PolicyAssociation)
query = query.filter_by(policy_id=policy_id)
query = query.filter_by(endpoint_id=endpoint_id)
query = query.filter_by(service_id=service_id)
query = query.filter_by(region_id=region_id)
try:
query.one()
except sql.NotFound:
raise exception.PolicyAssociationNotFound()
def delete_policy_association(self, policy_id, endpoint_id=None,
service_id=None, region_id=None):
with sql.transaction() as session:
query = session.query(PolicyAssociation)
query = query.filter_by(policy_id=policy_id)
query = query.filter_by(endpoint_id=endpoint_id)
query = query.filter_by(service_id=service_id)
query = query.filter_by(region_id=region_id)
query.delete()
def get_policy_association(self, endpoint_id=None,
service_id=None, region_id=None):
with sql.transaction() as session:
query = session.query(PolicyAssociation)
query = query.filter_by(endpoint_id=endpoint_id)
query = query.filter_by(service_id=service_id)
query = query.filter_by(region_id=region_id)
try:
ref = query.one()
except sql.NotFound:
raise exception.PolicyAssociationNotFound()
return {'policy_id': ref.policy_id}
def list_associations_for_policy(self, policy_id):
with sql.transaction() as session:
query = session.query(PolicyAssociation)
query = query.filter_by(policy_id=policy_id)
return [ref.to_dict() for ref in query.all()]
def delete_association_by_endpoint(self, endpoint_id):
with sql.transaction() as session:
query = session.query(PolicyAssociation)
query = query.filter_by(endpoint_id=endpoint_id)
query.delete()
def delete_association_by_service(self, service_id):
with sql.transaction() as session:
query = session.query(PolicyAssociation)
query = query.filter_by(service_id=service_id)
query.delete()
def delete_association_by_region(self, region_id):
with sql.transaction() as session:
query = session.query(PolicyAssociation)
query = query.filter_by(region_id=region_id)
query.delete()
def delete_association_by_policy(self, policy_id):
with sql.transaction() as session:
query = session.query(PolicyAssociation)
query = query.filter_by(policy_id=policy_id)
query.delete()

View File

@ -0,0 +1,447 @@
# Copyright 2014 IBM Corp.
#
# 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 abc
import six
from keystone.common import dependency
from keystone.common import manager
from keystone import config
from keystone import exception
from keystone.i18n import _, _LE, _LW
from keystone.openstack.common import log
CONF = config.CONF
LOG = log.getLogger(__name__)
extension_data = {
'name': 'OpenStack Keystone Endpoint Policy API',
'namespace': 'http://docs.openstack.org/identity/api/ext/'
'OS-ENDPOINT-POLICY/v1.0',
'alias': 'OS-ENDPOINT-POLICY',
'updated': '2014-08-18T12:00:0-00:00',
'description': 'OpenStack Keystone Endpoint Policy API.',
'links': [
{
'rel': 'describedby',
'type': 'text/html',
'href': 'https://github.com/openstack/identity-api/blob/master'
'/openstack-identity-api/v3/src/markdown/'
'identity-api-v3-os-endpoint-policy-ext.md',
}
]}
@dependency.provider('endpoint_policy_api')
@dependency.requires('catalog_api', 'policy_api')
class Manager(manager.Manager):
"""Default pivot point for the Endpoint Policy backend.
See :mod:`keystone.common.manager.Manager` for more details on how this
dynamically calls the backend.
"""
def __init__(self):
super(Manager, self).__init__(CONF.endpoint_policy.driver)
def _assert_valid_association(self, endpoint_id, service_id, region_id):
"""Assert that the association is supported.
There are three types of association supported:
- Endpoint (in which case service and region must be None)
- Service and region (in which endpoint must be None)
- Service (in which case endpoint and region must be None)
"""
if (endpoint_id is not None and
service_id is None and region_id is None):
return
if (service_id is not None and region_id is not None and
endpoint_id is None):
return
if (service_id is not None and
endpoint_id is None and region_id is None):
return
raise exception.InvalidPolicyAssociation(endpoint_id=endpoint_id,
service_id=service_id,
region_id=region_id)
def create_policy_association(self, policy_id, endpoint_id=None,
service_id=None, region_id=None):
self._assert_valid_association(endpoint_id, service_id, region_id)
self.driver.create_policy_association(policy_id, endpoint_id,
service_id, region_id)
def check_policy_association(self, policy_id, endpoint_id=None,
service_id=None, region_id=None):
self._assert_valid_association(endpoint_id, service_id, region_id)
self.driver.check_policy_association(policy_id, endpoint_id,
service_id, region_id)
def delete_policy_association(self, policy_id, endpoint_id=None,
service_id=None, region_id=None):
self._assert_valid_association(endpoint_id, service_id, region_id)
self.driver.delete_policy_association(policy_id, endpoint_id,
service_id, region_id)
def list_endpoints_for_policy(self, policy_id):
def _get_endpoint(endpoint_id, policy_id):
try:
return self.catalog_api.get_endpoint(endpoint_id)
except exception.EndpointNotFound:
msg = _LW('Endpoint %(endpoint_id)s referenced in '
'association for policy %(policy_id)s not found.')
LOG.warning(msg, {'policy_id': policy_id,
'endpoint_id': endpoint_id})
raise
def _get_endpoints_for_service(service_id, endpoints):
# TODO(henry-nash): Consider optimizing this in the future by
# adding an explicit list_endpoints_for_service to the catalog API.
return [ep for ep in endpoints if ep['service_id'] == service_id]
def _get_endpoints_for_service_and_region(
service_id, region_id, endpoints, regions):
# TODO(henry-nash): Consider optimizing this in the future.
# The lack of a two-way pointer in the region tree structure
# makes this somewhat inefficient.
def _recursively_get_endpoints_for_region(
region_id, service_id, endpoint_list, region_list,
endpoints_found, regions_examined):
"""Recursively search down a region tree for endpoints.
:param region_id: the point in the tree to examine
:param service_id: the service we are interested in
:param endpoint_list: list of all endpoints
:param region_list: list of all regions
:param endpoints_found: list of matching endpoints found so
far - which will be updated if more are
found in this iteration
:param regions_examined: list of regions we have already looked
at - used to spot illegal circular
references in the tree to avoid never
completing search
:returns: list of endpoints that match
"""
if region_id in regions_examined:
msg = _LE('Circular reference or a repeated entry found '
'in region tree - %(region_id)s.')
LOG.error(msg, {'region_id': ref.region_id})
return
regions_examined.append(region_id)
endpoints_found += (
[ep for ep in endpoint_list if
ep['service_id'] == service_id and
ep['region_id'] == region_id])
for region in region_list:
if region['parent_region_id'] == region_id:
_recursively_get_endpoints_for_region(
region['id'], service_id, endpoints, regions,
endpoints_found, regions_examined)
endpoints_found = []
regions_examined = []
# Now walk down the region tree
_recursively_get_endpoints_for_region(
region_id, service_id, endpoints, regions,
endpoints_found, regions_examined)
return endpoints_found
matching_endpoints = []
endpoints = self.catalog_api.list_endpoints()
regions = self.catalog_api.list_regions()
for ref in self.driver.list_associations_for_policy(policy_id):
if ref.get('endpoint_id') is not None:
matching_endpoints.append(
_get_endpoint(ref['endpoint_id'], policy_id))
continue
if (ref.get('service_id') is not None and
ref.get('region_id') is None):
matching_endpoints += _get_endpoints_for_service(
ref['service_id'], endpoints)
continue
if (ref.get('service_id') is not None and
ref.get('region_id') is not None):
matching_endpoints += (
_get_endpoints_for_service_and_region(
ref['service_id'], ref['region_id'],
endpoints, regions))
continue
msg = _LW('Unsupported policy association found - '
'Policy %(policy_id)s, Endpoint %(endpoint_id)s, '
'Service %(service_id)s, Region %(region_id)s, ')
LOG.warning(msg, {'policy_id': policy_id,
'endpoint_id': ref['endpoint_id'],
'service_id': ref['service_id'],
'region_id': ref['region_id']})
return matching_endpoints
def get_policy_for_endpoint(self, endpoint_id):
def _get_policy(policy_id, endpoint_id):
try:
return self.policy_api.get_policy(policy_id)
except exception.PolicyNotFound:
msg = _LW('Policy %(policy_id)s referenced in association '
'for endpoint %(endpoint_id)s not found.')
LOG.warning(msg, {'policy_id': policy_id,
'endpoint_id': endpoint_id})
raise
def _look_for_policy_for_region_and_service(endpoint):
"""Look in the region and its parents for a policy.
Examine the region of the endpoint for a policy appropriate for
the service of the endpoint. If there isn't a match, then chase up
the region tree to find one.
"""
next_region_id = endpoint['region_id']
regions_examined = []
while next_region_id is not None:
try:
ref = self.driver.get_policy_association(
service_id=endpoint['service_id'],
region_id=next_region_id)
return ref['policy_id']
except exception.PolicyAssociationNotFound:
pass
# There wasn't one for that region & service, let's
# chase up the region tree
regions_examined.append(next_region_id)
region = self.catalog_api.get_region(next_region_id)
next_region_id = None
if region.get('parent_region_id') is not None:
next_region_id = region['parent_region_id']
if next_region_id in regions_examined:
msg = _LE('Circular reference or a repeated entry '
'found in region tree - %(region_id)s.')
LOG.error(msg, {'region_id': next_region_id})
break
# First let's see if there is a policy explicitly defined for
# this endpoint.
try:
ref = self.driver.get_policy_association(endpoint_id=endpoint_id)
return _get_policy(ref['policy_id'], endpoint_id)
except exception.PolicyAssociationNotFound:
pass
# So there wasn't a policy explicitly defined for this endpoint, so
# now let's see if there is one for the Region & Service.
endpoint = self.catalog_api.get_endpoint(endpoint_id)
policy_id = _look_for_policy_for_region_and_service(endpoint)
if policy_id is not None:
return _get_policy(policy_id, endpoint_id)
# Finally, just check if there is one for the service.
try:
ref = self.driver.get_policy_association(
service_id=endpoint['service_id'])
return _get_policy(ref['policy_id'], endpoint_id)
except exception.PolicyAssociationNotFound:
pass
msg = _('No policy is associated with endpoint '
'%(endpoint_id)s.') % {'endpoint_id': endpoint_id}
raise exception.NotFound(msg)
@six.add_metaclass(abc.ABCMeta)
class Driver(object):
"""Interface description for an Endpoint Policy driver."""
@abc.abstractmethod
def create_policy_association(self, policy_id, endpoint_id=None,
service_id=None, region_id=None):
"""Creates a policy association.
:param policy_id: identity of policy that is being associated
:type policy_id: string
:param endpoint_id: identity of endpoint to associate
:type endpoint_id: string
:param service_id: identity of the service to associate
:type service_id: string
:param region_id: identity of the region to associate
:type region_id: string
:returns: None
There are three types of association permitted:
- Endpoint (in which case service and region must be None)
- Service and region (in which endpoint must be None)
- Service (in which case endpoint and region must be None)
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def check_policy_association(self, policy_id, endpoint_id=None,
service_id=None, region_id=None):
"""Checks existence a policy association.
:param policy_id: identity of policy that is being associated
:type policy_id: string
:param endpoint_id: identity of endpoint to associate
:type endpoint_id: string
:param service_id: identity of the service to associate
:type service_id: string
:param region_id: identity of the region to associate
:type region_id: string
:raises: keystone.exception.PolicyAssociationNotFound if the is no
match for the specified association
:returns: None
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def delete_policy_association(self, policy_id, endpoint_id=None,
service_id=None, region_id=None):
"""Deletes a policy association.
:param policy_id: identity of policy that is being associated
:type policy_id: string
:param endpoint_id: identity of endpoint to associate
:type endpoint_id: string
:param service_id: identity of the service to associate
:type service_id: string
:param region_id: identity of the region to associate
:type region_id: string
:returns: None
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def get_policy_association(self, endpoint_id=None,
service_id=None, region_id=None):
"""Gets the policy for an explicit association.
This method is not exposed as a public API, but is used by
get_policy_for_endpoint().
:param endpoint_id: identity of endpoint
:type endpoint_id: string
:param service_id: identity of the service
:type service_id: string
:param region_id: identity of the region
:type region_id: string
:raises: keystone.exception.PolicyAssociationNotFound if the is no
match for the specified association
:returns: dict containing policy_id
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def list_associations_for_policy(self, policy_id):
"""List the associations for a policy.
This method is not exposed as a public API, but is used by
list_endpoints_for_policy().
:param policy_id: identity of policy
:type policy_id: string
:returns: List of association dicts
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def list_endpoints_for_policy(self, policy_id):
"""List all the endpoints using a given policy.
:param policy_id: identity of policy that is being associated
:type policy_id: string
:returns: list of endpoints that have an effective association with
that policy
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def get_policy_for_endpoint(self, endpoint_id):
"""Get the appropriate policy for a given endpoint.
:param endpoint_id: identity of endpoint
:type endpoint_id: string
:returns: Policy entity for the endpoint
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def delete_association_by_endpoint(self, endpoint_id):
"""Removes all the policy associations with the specific endpoint.
:param endpoint_id: identity of endpoint to check
:type endpoint_id: string
:returns: None
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def delete_association_by_service(self, service_id):
"""Removes all the policy associations with the specific service.
:param service_id: identity of endpoint to check
:type service_id: string
:returns: None
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def delete_association_by_region(self, region_id):
"""Removes all the policy associations with the specific region.
:param region_id: identity of endpoint to check
:type region_id: string
:returns: None
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def delete_association_by_policy(self, policy_id):
"""Removes all the policy associations with the specific policy.
:param policy_id: identity of endpoint to check
:type policy_id: string
:returns: None
"""
raise exception.NotImplemented() # pragma: no cover

View File

@ -0,0 +1,25 @@
[db_settings]
# Used to identify which repository this database is versioned under.
# You can use the name of your project.
repository_id=endpoint_policy
# The name of the database table used to track the schema version.
# This name shouldn't already be used by your project.
# If this is changed once a database is under version control, you'll need to
# change the table name in each database too.
version_table=migrate_version
# When committing a change script, Migrate will attempt to generate the
# sql for all supported databases; normally, if one of them fails - probably
# because you don't have that database installed - it is ignored and the
# commit continues, perhaps ending successfully.
# Databases in this list MUST compile successfully during a commit, or the
# entire commit will fail. List the databases your application will actually
# be using to ensure your updates to that database work properly.
# This must be a list; example: ['postgres','sqlite']
required_dbs=[]
# When creating new change scripts, Migrate will stamp the new script with
# a version number. By default this is latest_version + 1. You can set this
# to 'true' to tell Migrate to use the UTC timestamp instead.
use_timestamp_numbering=False

View File

@ -0,0 +1,48 @@
# Copyright 2014 IBM Corp.
#
# 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 sqlalchemy as sql
def upgrade(migrate_engine):
# Upgrade operations go here. Don't create your own engine; bind
# migrate_engine to your metadata
meta = sql.MetaData()
meta.bind = migrate_engine
endpoint_policy_table = sql.Table(
'policy_association',
meta,
sql.Column('id', sql.String(64), primary_key=True),
sql.Column('policy_id', sql.String(64),
nullable=False),
sql.Column('endpoint_id', sql.String(64),
nullable=True),
sql.Column('service_id', sql.String(64),
nullable=True),
sql.Column('region_id', sql.String(64),
nullable=True),
sql.UniqueConstraint('endpoint_id', 'service_id', 'region_id'),
mysql_engine='InnoDB',
mysql_charset='utf8')
endpoint_policy_table.create(migrate_engine, checkfirst=True)
def downgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine
# Operations to reverse the above upgrade go here.
table = sql.Table('policy_association', meta, autoload=True)
table.drop()

View File

@ -210,6 +210,12 @@ class CrossBackendNotAllowed(Forbidden):
"user is %(user_id)s")
class InvalidPolicyAssociation(Forbidden):
message_format = _("Invalid mix of entities for policy association - "
"Endpoint: %(endpoint_id)s, Service: %(service_id)s, "
"Region: %(region_id)s")
class NotFound(Error):
message_format = _("Could not find: %(target)s")
code = 404
@ -232,6 +238,10 @@ class PolicyNotFound(NotFound):
message_format = _("Could not find policy: %(policy_id)s")
class PolicyAssociationNotFound(NotFound):
message_format = _("Could not find policy association")
class RoleNotFound(NotFound):
message_format = _("Could not find role: %(role_id)s")

View File

@ -0,0 +1,247 @@
# Copyright 2014 IBM Corp.
#
# 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 uuid
from testtools import matchers
from keystone import exception
class PolicyAssociationTests(object):
def _assert_correct_policy(self, endpoint, policy):
ref = (
self.endpoint_policy_api.get_policy_for_endpoint(endpoint['id']))
self.assertEqual(policy['id'], ref['id'])
def _assert_correct_endpoints(self, policy, endpoint_list):
endpoint_id_list = [ep['id'] for ep in endpoint_list]
endpoints = (
self.endpoint_policy_api.list_endpoints_for_policy(policy['id']))
self.assertThat(endpoints, matchers.HasLength(len(endpoint_list)))
for endpoint in endpoints:
self.assertIn(endpoint['id'], endpoint_id_list)
def load_sample_data(self):
"""Create sample data to test policy associations.
The following data is created:
- 3 regions, in a hierarchy, 0 -> 1 -> 2 (where 0 is top)
- 3 services
- 6 endpoints, 2 in each region, with a mixture of services:
0 - region 0, Service 0
1 - region 0, Service 1
2 - region 1, Service 1
3 - region 1, Service 2
4 - region 2, Service 2
5 - region 2, Service 0
"""
def new_endpoint(region_id, service_id):
endpoint = {'id': uuid.uuid4().hex, 'interface': 'test',
'region_id': region_id, 'service_id': service_id,
'url': '/url'}
self.endpoint.append(self.catalog_api.create_endpoint(
endpoint['id'], endpoint))
self.policy = []
self.endpoint = []
self.service = []
self.region = []
for i in range(3):
policy = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex,
'blob': {'data': uuid.uuid4().hex}}
self.policy.append(self.policy_api.create_policy(policy['id'],
policy))
service = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex}
self.service.append(self.catalog_api.create_service(service['id'],
service))
region = {'id': uuid.uuid4().hex, 'description': uuid.uuid4().hex}
# Link the 3 regions together as a hierarchy, [0] at the top
if i != 0:
region['parent_region_id'] = self.region[i - 1]['id']
self.region.append(self.catalog_api.create_region(region))
new_endpoint(self.region[0]['id'], self.service[0]['id'])
new_endpoint(self.region[0]['id'], self.service[1]['id'])
new_endpoint(self.region[1]['id'], self.service[1]['id'])
new_endpoint(self.region[1]['id'], self.service[2]['id'])
new_endpoint(self.region[2]['id'], self.service[2]['id'])
new_endpoint(self.region[2]['id'], self.service[0]['id'])
def test_policy_to_endpoint_association_crud(self):
self.endpoint_policy_api.create_policy_association(
self.policy[0]['id'], endpoint_id=self.endpoint[0]['id'])
self.endpoint_policy_api.check_policy_association(
self.policy[0]['id'], endpoint_id=self.endpoint[0]['id'])
self.endpoint_policy_api.delete_policy_association(
self.policy[0]['id'], endpoint_id=self.endpoint[0]['id'])
self.assertRaises(exception.NotFound,
self.endpoint_policy_api.check_policy_association,
self.policy[0]['id'],
endpoint_id=self.endpoint[0]['id'])
def test_overwriting_policy_to_endpoint_association(self):
self.endpoint_policy_api.create_policy_association(
self.policy[0]['id'], endpoint_id=self.endpoint[0]['id'])
self.endpoint_policy_api.create_policy_association(
self.policy[1]['id'], endpoint_id=self.endpoint[0]['id'])
self.assertRaises(exception.NotFound,
self.endpoint_policy_api.check_policy_association,
self.policy[0]['id'],
endpoint_id=self.endpoint[0]['id'])
self.endpoint_policy_api.check_policy_association(
self.policy[1]['id'], endpoint_id=self.endpoint[0]['id'])
def test_invalid_policy_to_endpoint_association(self):
self.assertRaises(exception.InvalidPolicyAssociation,
self.endpoint_policy_api.create_policy_association,
self.policy[0]['id'])
self.assertRaises(exception.InvalidPolicyAssociation,
self.endpoint_policy_api.create_policy_association,
self.policy[0]['id'],
endpoint_id=self.endpoint[0]['id'],
region_id=self.region[0]['id'])
self.assertRaises(exception.InvalidPolicyAssociation,
self.endpoint_policy_api.create_policy_association,
self.policy[0]['id'],
endpoint_id=self.endpoint[0]['id'],
service_id=self.service[0]['id'])
self.assertRaises(exception.InvalidPolicyAssociation,
self.endpoint_policy_api.create_policy_association,
self.policy[0]['id'],
region_id=self.region[0]['id'])
def test_policy_to_explicit_endpoint_association(self):
# Associate policy 0 with endpoint 0
self.endpoint_policy_api.create_policy_association(
self.policy[0]['id'], endpoint_id=self.endpoint[0]['id'])
self._assert_correct_policy(self.endpoint[0], self.policy[0])
self._assert_correct_endpoints(self.policy[0], [self.endpoint[0]])
self.assertRaises(exception.NotFound,
self.endpoint_policy_api.get_policy_for_endpoint,
uuid.uuid4().hex)
def test_policy_to_service_association(self):
self.endpoint_policy_api.create_policy_association(
self.policy[0]['id'], service_id=self.service[0]['id'])
self.endpoint_policy_api.create_policy_association(
self.policy[1]['id'], service_id=self.service[1]['id'])
# Endpoints 0 and 5 are part of service 0
self._assert_correct_policy(self.endpoint[0], self.policy[0])
self._assert_correct_policy(self.endpoint[5], self.policy[0])
self._assert_correct_endpoints(
self.policy[0], [self.endpoint[0], self.endpoint[5]])
# Endpoints 1 and 2 are part of service 1
self._assert_correct_policy(self.endpoint[1], self.policy[1])
self._assert_correct_policy(self.endpoint[2], self.policy[1])
self._assert_correct_endpoints(
self.policy[1], [self.endpoint[1], self.endpoint[2]])
def test_policy_to_region_and_service_association(self):
self.endpoint_policy_api.create_policy_association(
self.policy[0]['id'], service_id=self.service[0]['id'],
region_id=self.region[0]['id'])
self.endpoint_policy_api.create_policy_association(
self.policy[1]['id'], service_id=self.service[1]['id'],
region_id=self.region[1]['id'])
self.endpoint_policy_api.create_policy_association(
self.policy[2]['id'], service_id=self.service[2]['id'],
region_id=self.region[2]['id'])
# Endpoint 0 is in region 0 with service 0, so should get policy 0
self._assert_correct_policy(self.endpoint[0], self.policy[0])
# Endpoint 5 is in Region 2 with service 0, so should also get
# policy 0 by searching up the tree to Region 0
self._assert_correct_policy(self.endpoint[5], self.policy[0])
# Looking the other way round, policy 2 should only be in use by
# endpoint 4, since that's the only endpoint in region 2 with the
# correct service
self._assert_correct_endpoints(
self.policy[2], [self.endpoint[4]])
# Policy 1 should only be in use by endpoint 2, since that's the only
# endpoint in region 1 (and region 2 below it) with the correct service
self._assert_correct_endpoints(
self.policy[1], [self.endpoint[2]])
# Policy 0 should be in use by endpoint 0, as well as 5 (since 5 is
# of the correct service and in region 2 below it)
self._assert_correct_endpoints(
self.policy[0], [self.endpoint[0], self.endpoint[5]])
def test_delete_association_by_entity(self):
self.endpoint_policy_api.create_policy_association(
self.policy[0]['id'], endpoint_id=self.endpoint[0]['id'])
self.endpoint_policy_api.delete_association_by_endpoint(
self.endpoint[0]['id'])
self.assertRaises(exception.NotFound,
self.endpoint_policy_api.check_policy_association,
self.policy[0]['id'],
endpoint_id=self.endpoint[0]['id'])
# Make sure deleting it again is silent - since this method is used
# in response to notifications by the controller.
self.endpoint_policy_api.delete_association_by_endpoint(
self.endpoint[0]['id'])
# Now try with service - ensure both combined region & service
# associations and explicit service ones are removed
self.endpoint_policy_api.create_policy_association(
self.policy[0]['id'], service_id=self.service[0]['id'],
region_id=self.region[0]['id'])
self.endpoint_policy_api.create_policy_association(
self.policy[1]['id'], service_id=self.service[0]['id'],
region_id=self.region[1]['id'])
self.endpoint_policy_api.create_policy_association(
self.policy[0]['id'], service_id=self.service[0]['id'])
self.endpoint_policy_api.delete_association_by_service(
self.service[0]['id'])
self.assertRaises(exception.NotFound,
self.endpoint_policy_api.check_policy_association,
self.policy[0]['id'],
service_id=self.service[0]['id'],
region_id=self.region[0]['id'])
self.assertRaises(exception.NotFound,
self.endpoint_policy_api.check_policy_association,
self.policy[1]['id'],
service_id=self.service[0]['id'],
region_id=self.region[1]['id'])
self.assertRaises(exception.NotFound,
self.endpoint_policy_api.check_policy_association,
self.policy[0]['id'],
service_id=self.service[0]['id'])
# Finally, check delete by region
self.endpoint_policy_api.create_policy_association(
self.policy[0]['id'], service_id=self.service[0]['id'],
region_id=self.region[0]['id'])
self.endpoint_policy_api.delete_association_by_region(
self.region[0]['id'])
self.assertRaises(exception.NotFound,
self.endpoint_policy_api.check_policy_association,
self.policy[0]['id'],
service_id=self.service[0]['id'],
region_id=self.region[0]['id'])
self.assertRaises(exception.NotFound,
self.endpoint_policy_api.check_policy_association,
self.policy[0]['id'],
service_id=self.service[0]['id'])

View File

@ -0,0 +1,37 @@
# Copyright 2014 IBM Corp.
#
# 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 sql
from keystone.tests import test_backend_endpoint_policy
from keystone.tests import test_backend_sql
class SqlPolicyAssociationTable(test_backend_sql.SqlModels):
"""Set of tests for checking SQL Policy Association Mapping."""
def test_policy_association_mapping(self):
cols = (('policy_id', sql.String, 64),
('endpoint_id', sql.String, 64),
('service_id', sql.String, 64),
('region_id', sql.String, 64))
self.assertExpectedSchema('policy_association', cols)
class SqlPolicyAssociationTests(
test_backend_sql.SqlTests,
test_backend_endpoint_policy.PolicyAssociationTests):
def load_fixtures(self, fixtures):
super(SqlPolicyAssociationTests, self).load_fixtures(fixtures)
self.load_sample_data()

View File

@ -34,6 +34,7 @@ To run these tests against a live database:
from oslo.db.sqlalchemy import utils
from keystone.contrib import endpoint_filter
from keystone.contrib import endpoint_policy
from keystone.contrib import example
from keystone.contrib import federation
from keystone.contrib import oauth1
@ -137,6 +138,26 @@ class EndpointFilterExtension(test_sql_upgrade.SqlMigrateBase):
self.assertTableDoesNotExist('project_endpoint')
class EndpointPolicyExtension(test_sql_upgrade.SqlMigrateBase):
def repo_package(self):
return endpoint_policy
def test_upgrade(self):
self.assertTableDoesNotExist('policy_association')
self.upgrade(1, repository=self.repo_path)
self.assertTableColumns('policy_association',
['id', 'policy_id', 'endpoint_id',
'service_id', 'region_id'])
def test_downgrade(self):
self.upgrade(1, repository=self.repo_path)
self.assertTableColumns('policy_association',
['id', 'policy_id', 'endpoint_id',
'service_id', 'region_id'])
self.downgrade(0, repository=self.repo_path)
self.assertTableDoesNotExist('policy_association')
class FederationExtension(test_sql_upgrade.SqlMigrateBase):
"""Test class for ensuring the Federation SQL."""