201 lines
7.6 KiB
Python
201 lines
7.6 KiB
Python
# 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.
|
|
|
|
"""Main entry point into the Federation service."""
|
|
|
|
import uuid
|
|
|
|
from oslo_log import log
|
|
|
|
from keystone.common import cache
|
|
from keystone.common import driver_hints
|
|
from keystone.common import manager
|
|
from keystone.common import provider_api
|
|
import keystone.conf
|
|
from keystone import exception
|
|
from keystone.federation import utils
|
|
from keystone.i18n import _
|
|
from keystone import notifications
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
# This is a general cache region for service providers.
|
|
MEMOIZE = cache.get_memoization_decorator(group='federation')
|
|
|
|
CONF = keystone.conf.CONF
|
|
PROVIDERS = provider_api.ProviderAPIs
|
|
|
|
|
|
class Manager(manager.Manager):
|
|
"""Default pivot point for the Federation backend.
|
|
|
|
See :mod:`keystone.common.manager.Manager` for more details on how this
|
|
dynamically calls the backend.
|
|
|
|
"""
|
|
|
|
driver_namespace = 'keystone.federation'
|
|
_provides_api = 'federation_api'
|
|
|
|
def __init__(self):
|
|
super(Manager, self).__init__(CONF.federation.driver)
|
|
notifications.register_event_callback(
|
|
notifications.ACTIONS.internal, notifications.DOMAIN_DELETED,
|
|
self._cleanup_identity_provider
|
|
)
|
|
|
|
def _cleanup_identity_provider(self, service, resource_type, operation,
|
|
payload):
|
|
domain_id = payload['resource_info']
|
|
hints = driver_hints.Hints()
|
|
hints.add_filter('domain_id', domain_id)
|
|
idps = self.driver.list_idps(hints=hints)
|
|
for idp in idps:
|
|
try:
|
|
self.delete_idp(idp['id'])
|
|
except exception.IdentityProviderNotFound:
|
|
LOG.debug(('Identity Provider %(idpid)s not found when '
|
|
'deleting domain contents for %(domainid)s, '
|
|
'continuing with cleanup.'),
|
|
{'idpid': idp['id'], 'domainid': domain_id})
|
|
|
|
def create_idp(self, idp_id, idp):
|
|
auto_created_domain = False
|
|
if not idp.get('domain_id'):
|
|
idp['domain_id'] = self._create_idp_domain(idp_id)
|
|
auto_created_domain = True
|
|
else:
|
|
self._assert_valid_domain_id(idp['domain_id'])
|
|
|
|
try:
|
|
return self.driver.create_idp(idp_id, idp)
|
|
except exception.Conflict:
|
|
# If there is a conflict storing the Identity Provider in the
|
|
# backend, then we need to make sure we clean up the domain we just
|
|
# created for it and raise the Conflict exception afterwards.
|
|
if auto_created_domain:
|
|
self._cleanup_idp_domain(idp['domain_id'])
|
|
raise
|
|
|
|
def delete_idp(self, idp_id):
|
|
self.driver.delete_idp(idp_id)
|
|
# NOTE(lbragstad): If an identity provider is removed from the system,
|
|
# then we need to invalidate the token cache. Otherwise it will be
|
|
# possible for federated tokens to be considered valid after a service
|
|
# provider removes a federated identity provider resource.
|
|
reason = (
|
|
'The token cache is being invalidated because identity provider '
|
|
'%(idp_id)s has been deleted. Authorization for federated users '
|
|
'will be recalculated and enforced accordingly the next time '
|
|
'they authenticate or validate a token.' % {'idp_id': idp_id}
|
|
)
|
|
notifications.invalidate_token_cache_notification(reason)
|
|
|
|
def _cleanup_idp_domain(self, domain_id):
|
|
domain = {'enabled': False}
|
|
PROVIDERS.resource_api.update_domain(domain_id, domain)
|
|
PROVIDERS.resource_api.delete_domain(domain_id)
|
|
|
|
def _create_idp_domain(self, idp_id):
|
|
domain_id = uuid.uuid4().hex
|
|
desc = 'Auto generated federated domain for Identity Provider: '
|
|
desc += idp_id
|
|
domain = {
|
|
'id': domain_id,
|
|
'name': domain_id,
|
|
'description': desc,
|
|
'enabled': True
|
|
}
|
|
PROVIDERS.resource_api.create_domain(domain['id'], domain)
|
|
return domain_id
|
|
|
|
def _assert_valid_domain_id(self, domain_id):
|
|
PROVIDERS.resource_api.get_domain(domain_id)
|
|
|
|
@MEMOIZE
|
|
def get_enabled_service_providers(self):
|
|
"""List enabled service providers for Service Catalog.
|
|
|
|
Service Provider in a catalog contains three attributes: ``id``,
|
|
``auth_url``, ``sp_url``, where:
|
|
|
|
- id is a unique, user defined identifier for service provider object
|
|
- auth_url is an authentication URL of remote Keystone
|
|
- sp_url a URL accessible at the remote service provider where SAML
|
|
assertion is transmitted.
|
|
|
|
:returns: list of dictionaries with enabled service providers
|
|
:rtype: list of dicts
|
|
|
|
"""
|
|
def normalize(sp):
|
|
ref = {
|
|
'auth_url': sp.auth_url,
|
|
'id': sp.id,
|
|
'sp_url': sp.sp_url
|
|
}
|
|
return ref
|
|
|
|
service_providers = self.driver.get_enabled_service_providers()
|
|
return [normalize(sp) for sp in service_providers]
|
|
|
|
def create_sp(self, sp_id, service_provider):
|
|
sp_ref = self.driver.create_sp(sp_id, service_provider)
|
|
self.get_enabled_service_providers.invalidate(self)
|
|
return sp_ref
|
|
|
|
def delete_sp(self, sp_id):
|
|
self.driver.delete_sp(sp_id)
|
|
self.get_enabled_service_providers.invalidate(self)
|
|
|
|
def update_sp(self, sp_id, service_provider):
|
|
sp_ref = self.driver.update_sp(sp_id, service_provider)
|
|
self.get_enabled_service_providers.invalidate(self)
|
|
return sp_ref
|
|
|
|
def evaluate(self, idp_id, protocol_id, assertion_data):
|
|
mapping = self.get_mapping_from_idp_and_protocol(idp_id, protocol_id)
|
|
rules = mapping['rules']
|
|
rule_processor = utils.RuleProcessor(mapping['id'], rules)
|
|
mapped_properties = rule_processor.process(assertion_data)
|
|
return mapped_properties, mapping['id']
|
|
|
|
def create_protocol(self, idp_id, protocol_id, protocol):
|
|
self._validate_mapping_exists(protocol['mapping_id'])
|
|
return self.driver.create_protocol(idp_id, protocol_id, protocol)
|
|
|
|
def delete_protocol(self, idp_id, protocol_id):
|
|
hints = driver_hints.Hints()
|
|
hints.add_filter('protocol_id', protocol_id)
|
|
shadow_users = PROVIDERS.shadow_users_api.list_federated_users_info(
|
|
hints)
|
|
|
|
self.driver.delete_protocol(idp_id, protocol_id)
|
|
|
|
for shadow_user in shadow_users:
|
|
PROVIDERS.identity_api.shadow_federated_user.invalidate(
|
|
PROVIDERS.identity_api, shadow_user['idp_id'],
|
|
shadow_user['protocol_id'], shadow_user['unique_id'],
|
|
shadow_user['display_name'],
|
|
shadow_user.get('extra', {}).get('email'))
|
|
|
|
def update_protocol(self, idp_id, protocol_id, protocol):
|
|
self._validate_mapping_exists(protocol['mapping_id'])
|
|
return self.driver.update_protocol(idp_id, protocol_id, protocol)
|
|
|
|
def _validate_mapping_exists(self, mapping_id):
|
|
try:
|
|
self.driver.get_mapping(mapping_id)
|
|
except exception.MappingNotFound:
|
|
msg = _('Invalid mapping id: %s')
|
|
raise exception.ValidationError(message=(msg % mapping_id))
|