Merge "backend for policy endpoint extension"
This commit is contained in:
commit
7b819748c4
@ -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]
|
||||
|
||||
#
|
||||
|
@ -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,
|
||||
|
@ -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'
|
||||
|
15
keystone/contrib/endpoint_policy/__init__.py
Normal file
15
keystone/contrib/endpoint_policy/__init__.py
Normal 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
|
135
keystone/contrib/endpoint_policy/backends/sql.py
Normal file
135
keystone/contrib/endpoint_policy/backends/sql.py
Normal 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()
|
447
keystone/contrib/endpoint_policy/core.py
Normal file
447
keystone/contrib/endpoint_policy/core.py
Normal 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
|
25
keystone/contrib/endpoint_policy/migrate_repo/migrate.cfg
Normal file
25
keystone/contrib/endpoint_policy/migrate_repo/migrate.cfg
Normal 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
|
@ -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()
|
@ -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")
|
||||
|
||||
|
247
keystone/tests/test_backend_endpoint_policy.py
Normal file
247
keystone/tests/test_backend_endpoint_policy.py
Normal 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'])
|
37
keystone/tests/test_backend_endpoint_policy_sql.py
Normal file
37
keystone/tests/test_backend_endpoint_policy_sql.py
Normal 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()
|
@ -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."""
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user