Service Providers API for OS-FEDERATION
For new Keystone2Keystone federation usecase Keystone needs to manage trusted Service Providers. This patch implements CRUD API calls for that. Implements blueprint: k2k-service-providers Change-Id: I69673045009926365803a89a2e4b0d892d841ed6
This commit is contained in:
parent
5cf219382b
commit
96747bc484
@ -148,6 +148,12 @@
|
||||
"identity:delete_mapping": "rule:admin_required",
|
||||
"identity:update_mapping": "rule:admin_required",
|
||||
|
||||
"identity:create_service_provider": "rule:admin_required",
|
||||
"identity:list_service_providers": "rule:admin_required",
|
||||
"identity:get_service_provider": "rule:admin_required",
|
||||
"identity:update_service_provider": "rule:admin_required",
|
||||
"identity:delete_service_provider": "rule:admin_required",
|
||||
|
||||
"identity:get_auth_catalog": "",
|
||||
"identity:get_auth_projects": "",
|
||||
"identity:get_auth_domains": "",
|
||||
|
@ -161,6 +161,12 @@
|
||||
"identity:delete_mapping": "rule:cloud_admin",
|
||||
"identity:update_mapping": "rule:cloud_admin",
|
||||
|
||||
"identity:create_service_provider": "rule:cloud_admin",
|
||||
"identity:list_service_providers": "rule:cloud_admin",
|
||||
"identity:get_service_provider": "rule:cloud_admin",
|
||||
"identity:update_service_provider": "rule:cloud_admin",
|
||||
"identity:delete_service_provider": "rule:cloud_admin",
|
||||
|
||||
"identity:get_auth_catalog": "",
|
||||
"identity:get_auth_projects": "",
|
||||
"identity:get_auth_domains": "",
|
||||
|
@ -57,8 +57,8 @@ class IdentityProviderModel(sql.ModelBase, sql.DictBase):
|
||||
new_dictionary = dictionary.copy()
|
||||
return cls(**new_dictionary)
|
||||
|
||||
def to_dict(self, include_extra_dict=False):
|
||||
"""Return the model's attributes as a dictionary."""
|
||||
def to_dict(self):
|
||||
"""Return a dictionary with model's attributes."""
|
||||
d = dict()
|
||||
for attr in self.__class__.attributes:
|
||||
d[attr] = getattr(self, attr)
|
||||
@ -85,6 +85,31 @@ class MappingModel(sql.ModelBase, sql.DictBase):
|
||||
return d
|
||||
|
||||
|
||||
class ServiceProviderModel(sql.ModelBase, sql.DictBase):
|
||||
__tablename__ = 'service_provider'
|
||||
attributes = ['auth_url', 'id', 'enabled', 'description', 'sp_url']
|
||||
mutable_attributes = frozenset(['auth_url', 'description', 'enabled',
|
||||
'sp_url'])
|
||||
|
||||
id = sql.Column(sql.String(64), primary_key=True)
|
||||
enabled = sql.Column(sql.Boolean, nullable=False)
|
||||
description = sql.Column(sql.Text(), nullable=True)
|
||||
auth_url = sql.Column(sql.String(256), nullable=True)
|
||||
sp_url = sql.Column(sql.String(256), nullable=True)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dictionary):
|
||||
new_dictionary = dictionary.copy()
|
||||
return cls(**new_dictionary)
|
||||
|
||||
def to_dict(self):
|
||||
"""Return a dictionary with model's attributes."""
|
||||
d = dict()
|
||||
for attr in self.__class__.attributes:
|
||||
d[attr] = getattr(self, attr)
|
||||
return d
|
||||
|
||||
|
||||
class Federation(core.Driver):
|
||||
|
||||
# Identity Provider CRUD
|
||||
@ -241,3 +266,45 @@ class Federation(core.Driver):
|
||||
mapping_id = protocol_ref.mapping_id
|
||||
mapping_ref = self._get_mapping(session, mapping_id)
|
||||
return mapping_ref.to_dict()
|
||||
|
||||
# Service Provider CRUD
|
||||
@sql.handle_conflicts(conflict_type='service_provider')
|
||||
def create_sp(self, sp_id, sp):
|
||||
with sql.transaction() as session:
|
||||
sp['id'] = sp_id
|
||||
sp_ref = ServiceProviderModel.from_dict(sp)
|
||||
session.add(sp_ref)
|
||||
return sp_ref.to_dict()
|
||||
|
||||
def delete_sp(self, sp_id):
|
||||
with sql.transaction() as session:
|
||||
sp_ref = self._get_sp(session, sp_id)
|
||||
session.delete(sp_ref)
|
||||
|
||||
def _get_sp(self, session, sp_id):
|
||||
sp_ref = session.query(ServiceProviderModel).get(sp_id)
|
||||
if not sp_ref:
|
||||
raise exception.ServiceProviderNotFound(sp_id=sp_id)
|
||||
return sp_ref
|
||||
|
||||
def list_sps(self):
|
||||
session = sql.get_session()
|
||||
with sql.transaction() as session:
|
||||
sps = session.query(ServiceProviderModel)
|
||||
sps_list = [sp.to_dict() for sp in sps]
|
||||
return sps_list
|
||||
|
||||
def get_sp(self, sp_id):
|
||||
with sql.transaction() as session:
|
||||
sp_ref = self._get_sp(session, sp_id)
|
||||
return sp_ref.to_dict()
|
||||
|
||||
def update_sp(self, sp_id, sp):
|
||||
with sql.transaction() as session:
|
||||
sp_ref = self._get_sp(session, sp_id)
|
||||
old_sp = sp_ref.to_dict()
|
||||
old_sp.update(sp)
|
||||
new_sp = ServiceProviderModel.from_dict(old_sp)
|
||||
for attr in ServiceProviderModel.mutable_attributes:
|
||||
setattr(sp_ref, attr, getattr(new_sp, attr))
|
||||
return sp_ref.to_dict()
|
||||
|
@ -337,6 +337,50 @@ class ProjectV3(controller.V3Controller):
|
||||
return ProjectV3.wrap_collection(context, projects)
|
||||
|
||||
|
||||
@dependency.requires('federation_api')
|
||||
class ServiceProvider(_ControllerBase):
|
||||
"""Service Provider representation."""
|
||||
|
||||
collection_name = 'service_providers'
|
||||
member_name = 'service_provider'
|
||||
|
||||
_mutable_parameters = frozenset(['auth_url', 'description', 'enabled',
|
||||
'sp_url'])
|
||||
_public_parameters = frozenset(['auth_url', 'id', 'enabled', 'description',
|
||||
'links', 'sp_url'])
|
||||
|
||||
@controller.protected()
|
||||
def create_service_provider(self, context, sp_id, service_provider):
|
||||
service_provider = self._normalize_dict(service_provider)
|
||||
service_provider.setdefault('enabled', False)
|
||||
ServiceProvider.check_immutable_params(service_provider)
|
||||
sp_ref = self.federation_api.create_sp(sp_id, service_provider)
|
||||
response = ServiceProvider.wrap_member(context, sp_ref)
|
||||
return wsgi.render_response(body=response, status=('201', 'Created'))
|
||||
|
||||
@controller.protected()
|
||||
def list_service_providers(self, context):
|
||||
ref = self.federation_api.list_sps()
|
||||
ref = [self.filter_params(x) for x in ref]
|
||||
return ServiceProvider.wrap_collection(context, ref)
|
||||
|
||||
@controller.protected()
|
||||
def get_service_provider(self, context, sp_id):
|
||||
ref = self.federation_api.get_sp(sp_id)
|
||||
return ServiceProvider.wrap_member(context, ref)
|
||||
|
||||
@controller.protected()
|
||||
def delete_service_provider(self, context, sp_id):
|
||||
self.federation_api.delete_sp(sp_id)
|
||||
|
||||
@controller.protected()
|
||||
def update_service_provider(self, context, sp_id, service_provider):
|
||||
service_provider = self._normalize_dict(service_provider)
|
||||
ServiceProvider.check_immutable_params(service_provider)
|
||||
sp_ref = self.federation_api.update_sp(sp_id, service_provider)
|
||||
return ServiceProvider.wrap_member(context, sp_ref)
|
||||
|
||||
|
||||
class SAMLMetadataV3(_ControllerBase):
|
||||
member_name = 'metadata'
|
||||
|
||||
|
@ -224,3 +224,70 @@ class Driver(object):
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented() # pragma: no cover
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_sp(self, sp_id, sp):
|
||||
"""Create a service provider.
|
||||
|
||||
:param sp_id: id of the service provider
|
||||
:type sp_id: string
|
||||
:param sp: service prvider object
|
||||
:type sp: dict
|
||||
|
||||
:returns: sp_ref
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented() # pragma: no cover
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_sp(self, sp_id):
|
||||
"""Delete a service provider.
|
||||
|
||||
:param sp_id: id of the service provider
|
||||
:type sp_id: string
|
||||
|
||||
:raises: keystone.exception.ServiceProviderNotFound
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented() # pragma: no cover
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_sps(self):
|
||||
"""List all service providers.
|
||||
|
||||
:returns List of sp_ref objects
|
||||
:rtype: list of dicts
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented() # pragma: no cover
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_sp(self, sp_id):
|
||||
"""Get a service provider.
|
||||
|
||||
:param sp_id: id of the service provider
|
||||
:type sp_id: string
|
||||
|
||||
:returns: sp_ref
|
||||
:raises: keystone.exception.ServiceProviderNotFound
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented() # pragma: no cover
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_sp(self, sp_id, sp):
|
||||
"""Update a service provider.
|
||||
|
||||
:param sp_id: id of the service provider
|
||||
:type sp_id: string
|
||||
:param sp: service prvider object
|
||||
:type sp: dict
|
||||
|
||||
:returns: sp_ref
|
||||
:rtype: dict
|
||||
|
||||
:raises: keystone.exception.ServiceProviderNotFound
|
||||
|
||||
"""
|
||||
raise exception.NotImplemented() # pragma: no cover
|
||||
|
@ -0,0 +1,38 @@
|
||||
# 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):
|
||||
meta = sql.MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
sp_table = sql.Table(
|
||||
'service_provider',
|
||||
meta,
|
||||
sql.Column('auth_url', sql.String(256), nullable=True),
|
||||
sql.Column('id', sql.String(64), primary_key=True),
|
||||
sql.Column('enabled', sql.Boolean, nullable=False),
|
||||
sql.Column('description', sql.Text(), nullable=True),
|
||||
sql.Column('sp_url', sql.String(256), nullable=True),
|
||||
mysql_engine='InnoDB',
|
||||
mysql_charset='utf8')
|
||||
|
||||
sp_table.create(migrate_engine, checkfirst=True)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = sql.MetaData()
|
||||
meta.bind = migrate_engine
|
||||
table = sql.Table('service_provider', meta, autoload=True)
|
||||
table.drop()
|
@ -29,6 +29,7 @@ build_parameter_relation = functools.partial(
|
||||
IDP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='idp_id')
|
||||
PROTOCOL_ID_PARAMETER_RELATION = build_parameter_relation(
|
||||
parameter_name='protocol_id')
|
||||
SP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='sp_id')
|
||||
|
||||
|
||||
class FederationExtension(wsgi.V3ExtensionRouter):
|
||||
@ -62,17 +63,20 @@ class FederationExtension(wsgi.V3ExtensionRouter):
|
||||
GET /OS-FEDERATION/projects
|
||||
GET /OS-FEDERATION/domains
|
||||
|
||||
PUT /OS-FEDERATION/service_providers/$service_provider
|
||||
GET /OS-FEDERATION/service_providers
|
||||
GET /OS-FEDERATION/service_providers/$service_provider
|
||||
DELETE /OS-FEDERATION/service_providers/$service_provider
|
||||
PATCH /OS-FEDERATION/service_providers/$service_provider
|
||||
|
||||
GET /OS-FEDERATION/identity_providers/$identity_provider/
|
||||
protocols/$protocol/auth
|
||||
POST /OS-FEDERATION/identity_providers/$identity_provider/
|
||||
protocols/$protocol/auth
|
||||
|
||||
|
||||
POST /auth/OS-FEDERATION/saml2
|
||||
|
||||
GET /OS-FEDERATION/saml2/metadata
|
||||
|
||||
|
||||
"""
|
||||
def _construct_url(self, suffix):
|
||||
return "/OS-FEDERATION/%s" % suffix
|
||||
@ -88,6 +92,7 @@ class FederationExtension(wsgi.V3ExtensionRouter):
|
||||
project_controller = controllers.ProjectV3()
|
||||
domain_controller = controllers.DomainV3()
|
||||
saml_metadata_controller = controllers.SAMLMetadataV3()
|
||||
sp_controller = controllers.ServiceProvider()
|
||||
|
||||
# Identity Provider CRUD operations
|
||||
|
||||
@ -153,6 +158,27 @@ class FederationExtension(wsgi.V3ExtensionRouter):
|
||||
path=self._construct_url('mappings'),
|
||||
get_action='list_mappings',
|
||||
rel=build_resource_relation(resource_name='mappings'))
|
||||
|
||||
# Service Providers CRUD operations
|
||||
|
||||
self._add_resource(
|
||||
mapper, sp_controller,
|
||||
path=self._construct_url('service_providers/{sp_id}'),
|
||||
get_action='get_service_provider',
|
||||
put_action='create_service_provider',
|
||||
patch_action='update_service_provider',
|
||||
delete_action='delete_service_provider',
|
||||
rel=build_resource_relation(resource_name='service_provider'),
|
||||
path_vars={
|
||||
'sp_id': SP_ID_PARAMETER_RELATION,
|
||||
})
|
||||
|
||||
self._add_resource(
|
||||
mapper, sp_controller,
|
||||
path=self._construct_url('service_providers'),
|
||||
get_action='list_service_providers',
|
||||
rel=build_resource_relation(resource_name='service_providers'))
|
||||
|
||||
self._add_resource(
|
||||
mapper, domain_controller,
|
||||
path=self._construct_url('domains'),
|
||||
|
@ -307,6 +307,10 @@ class IdentityProviderNotFound(NotFound):
|
||||
message_format = _("Could not find Identity Provider: %(idp_id)s")
|
||||
|
||||
|
||||
class ServiceProviderNotFound(NotFound):
|
||||
message_format = _("Could not find Service Provider: %(sp_id)s")
|
||||
|
||||
|
||||
class FederatedProtocolNotFound(NotFound):
|
||||
message_format = _("Could not find federated protocol %(protocol_id)s for"
|
||||
" Identity Provider: %(idp_id)s")
|
||||
|
@ -36,3 +36,11 @@ class SqlFederation(test_backend_sql.SqlModels):
|
||||
cols = (('id', sql.String, 64),
|
||||
('rules', sql.JsonBlob, None))
|
||||
self.assertExpectedSchema('mapping', cols)
|
||||
|
||||
def test_service_provider(self):
|
||||
cols = (('auth_url', sql.String, 256),
|
||||
('id', sql.String, 64),
|
||||
('enabled', sql.Boolean, None),
|
||||
('description', sql.Text, None),
|
||||
('sp_url', sql.String, 256))
|
||||
self.assertExpectedSchema('service_provider', cols)
|
||||
|
@ -2356,3 +2356,150 @@ class IdPMetadataGenerationTests(FederationTests):
|
||||
|
||||
reference_file = _load_xml('idp_saml2_metadata.xml')
|
||||
self.assertEqual(reference_file, r.result)
|
||||
|
||||
|
||||
class ServiceProviderTests(FederationTests):
|
||||
"""A test class for Service Providers."""
|
||||
|
||||
MEMBER_NAME = 'service_provider'
|
||||
COLLECTION_NAME = 'service_providers'
|
||||
SERVICE_PROVIDER_ID = 'ACME'
|
||||
SP_KEYS = ['auth_url', 'id', 'enabled', 'description', 'sp_url']
|
||||
|
||||
def setUp(self):
|
||||
super(FederationTests, self).setUp()
|
||||
# Add a Service Provider
|
||||
url = self.base_url(suffix=self.SERVICE_PROVIDER_ID)
|
||||
self.SP_REF = self.sp_ref()
|
||||
self.SERVICE_PROVIDER = self.put(
|
||||
url, body={'service_provider': self.SP_REF},
|
||||
expected_status=201).result
|
||||
|
||||
def sp_ref(self):
|
||||
ref = {
|
||||
'auth_url': 8 * uuid.uuid4().hex,
|
||||
'enabled': True,
|
||||
'description': uuid.uuid4().hex,
|
||||
'sp_url': 8 * uuid.uuid4().hex
|
||||
}
|
||||
return ref
|
||||
|
||||
def base_url(self, suffix=None):
|
||||
if suffix is not None:
|
||||
return '/OS-FEDERATION/service_providers/' + str(suffix)
|
||||
return '/OS-FEDERATION/service_providers'
|
||||
|
||||
def test_get_service_provider(self):
|
||||
url = self.base_url(suffix=self.SERVICE_PROVIDER_ID)
|
||||
resp = self.get(url, expected_status=200)
|
||||
self.assertValidEntity(resp.result['service_provider'],
|
||||
keys_to_check=self.SP_KEYS)
|
||||
|
||||
def test_get_service_provider_fail(self):
|
||||
url = self.base_url(suffix=uuid.uuid4().hex)
|
||||
self.get(url, expected_status=404)
|
||||
|
||||
def test_create_service_provider(self):
|
||||
url = self.base_url(suffix=uuid.uuid4().hex)
|
||||
sp = self.sp_ref()
|
||||
resp = self.put(url, body={'service_provider': sp},
|
||||
expected_status=201)
|
||||
self.assertValidEntity(resp.result['service_provider'],
|
||||
keys_to_check=self.SP_KEYS)
|
||||
|
||||
def test_create_service_provider_fail(self):
|
||||
"""Try adding SP object with unallowed attribute."""
|
||||
url = self.base_url(suffix=uuid.uuid4().hex)
|
||||
sp = self.sp_ref()
|
||||
sp[uuid.uuid4().hex] = uuid.uuid4().hex
|
||||
self.put(url, body={'service_provider': sp},
|
||||
expected_status=403)
|
||||
|
||||
def test_list_service_providers(self):
|
||||
"""Test listing of service provider objects.
|
||||
|
||||
Add two new service providers. List all available service providers.
|
||||
Expect to get list of three service providers (one created by setUp())
|
||||
Test if attributes match.
|
||||
|
||||
"""
|
||||
ref_service_providers = {
|
||||
uuid.uuid4().hex: self.sp_ref(),
|
||||
uuid.uuid4().hex: self.sp_ref(),
|
||||
}
|
||||
for id, sp in ref_service_providers.items():
|
||||
url = self.base_url(suffix=id)
|
||||
self.put(url, body={'service_provider': sp}, expected_status=201)
|
||||
|
||||
# Insert ids into service provider object, we will compare it with
|
||||
# responses from server and those include 'id' attribute.
|
||||
|
||||
ref_service_providers[self.SERVICE_PROVIDER_ID] = self.SP_REF
|
||||
for id, sp in ref_service_providers.items():
|
||||
sp['id'] = id
|
||||
|
||||
url = self.base_url()
|
||||
resp = self.get(url)
|
||||
service_providers = resp.result
|
||||
for service_provider in service_providers['service_providers']:
|
||||
id = service_provider['id']
|
||||
self.assertValidEntity(
|
||||
service_provider, ref=ref_service_providers[id],
|
||||
keys_to_check=self.SP_KEYS)
|
||||
|
||||
def test_update_service_provider(self):
|
||||
"""Update existing service provider.
|
||||
|
||||
Update default existing service provider and make sure it has been
|
||||
properly change.
|
||||
|
||||
"""
|
||||
new_sp_ref = self.sp_ref()
|
||||
url = self.base_url(suffix=self.SERVICE_PROVIDER_ID)
|
||||
resp = self.patch(url, body={'service_provider': new_sp_ref},
|
||||
expected_status=200)
|
||||
patch_result = resp.result
|
||||
new_sp_ref['id'] = self.SERVICE_PROVIDER_ID
|
||||
self.assertValidEntity(patch_result['service_provider'],
|
||||
ref=new_sp_ref,
|
||||
keys_to_check=self.SP_KEYS)
|
||||
|
||||
resp = self.get(url, expected_status=200)
|
||||
get_result = resp.result
|
||||
|
||||
self.assertDictEqual(patch_result['service_provider'],
|
||||
get_result['service_provider'])
|
||||
|
||||
def test_update_service_provider_immutable_parameters(self):
|
||||
"""Update immutable attributes in service provider.
|
||||
|
||||
In this particular case the test will try to change ``id`` attribute.
|
||||
Expectet server to return error code
|
||||
|
||||
"""
|
||||
new_sp_ref = {'id': uuid.uuid4().hex}
|
||||
url = self.base_url(suffix=self.SERVICE_PROVIDER_ID)
|
||||
self.patch(url, body={'service_provider': new_sp_ref},
|
||||
expected_status=403)
|
||||
|
||||
def test_update_service_provider_unknown_parameter(self):
|
||||
new_sp_ref = self.sp_ref()
|
||||
new_sp_ref[uuid.uuid4().hex] = uuid.uuid4().hex
|
||||
url = self.base_url(suffix=self.SERVICE_PROVIDER_ID)
|
||||
self.patch(url, body={'service_provider': new_sp_ref},
|
||||
expected_status=403)
|
||||
|
||||
def test_update_service_provider_404(self):
|
||||
new_sp_ref = self.sp_ref()
|
||||
new_sp_ref['description'] = uuid.uuid4().hex
|
||||
url = self.base_url(suffix=uuid.uuid4().hex)
|
||||
self.patch(url, body={'service_provider': new_sp_ref},
|
||||
expected_status=404)
|
||||
|
||||
def test_delete_service_provider(self):
|
||||
url = self.base_url(suffix=self.SERVICE_PROVIDER_ID)
|
||||
self.delete(url, expected_status=204)
|
||||
|
||||
def test_delete_service_provider_404(self):
|
||||
url = self.base_url(suffix=uuid.uuid4().hex)
|
||||
self.delete(url, expected_status=404)
|
||||
|
Loading…
x
Reference in New Issue
Block a user