diff options
Diffstat (limited to 'keystone-moon/keystone/common')
-rw-r--r-- | keystone-moon/keystone/common/authorization.py | 12 | ||||
-rw-r--r-- | keystone-moon/keystone/common/config.py | 31 | ||||
-rw-r--r-- | keystone-moon/keystone/common/controller.py | 11 | ||||
-rw-r--r-- | keystone-moon/keystone/common/manager.py | 32 | ||||
-rw-r--r-- | keystone-moon/keystone/common/sql/migrate_repo/versions/075_confirm_config_registration.py | 29 | ||||
-rw-r--r-- | keystone-moon/keystone/common/tokenless_auth.py | 193 | ||||
-rw-r--r-- | keystone-moon/keystone/common/wsgi.py | 7 |
7 files changed, 309 insertions, 6 deletions
diff --git a/keystone-moon/keystone/common/authorization.py b/keystone-moon/keystone/common/authorization.py index 8db618df..2c578dfd 100644 --- a/keystone-moon/keystone/common/authorization.py +++ b/keystone-moon/keystone/common/authorization.py @@ -29,13 +29,23 @@ AUTH_CONTEXT_ENV = 'KEYSTONE_AUTH_CONTEXT' Auth context is essentially the user credential used for policy enforcement. It is a dictionary with the following attributes: +* ``token``: Token from the request * ``user_id``: user ID of the principal * ``project_id`` (optional): project ID of the scoped project if auth is project-scoped * ``domain_id`` (optional): domain ID of the scoped domain if auth is domain-scoped +* ``domain_name`` (optional): domain name of the scoped domain if auth is + domain-scoped +* ``is_delegated_auth``: True if this is delegated (via trust or oauth) +* ``trust_id``: Trust ID if trust-scoped, or None +* ``trustor_id``: Trustor ID if trust-scoped, or None +* ``trustee_id``: Trustee ID if trust-scoped, or None +* ``consumer_id``: OAuth consumer ID, or None +* ``access_token_id``: OAuth access token ID, or None * ``roles`` (optional): list of role names for the given scope -* ``group_ids``: list of group IDs for which the API user has membership +* ``group_ids`` (optional): list of group IDs for which the API user has + membership if token was for a federated user """ diff --git a/keystone-moon/keystone/common/config.py b/keystone-moon/keystone/common/config.py index 4966dd9c..fcf05abe 100644 --- a/keystone-moon/keystone/common/config.py +++ b/keystone-moon/keystone/common/config.py @@ -529,8 +529,9 @@ FILE_OPTIONS = { 'token, the origin host must be a member of the ' 'trusted_dashboard list. This configuration ' 'option may be repeated for multiple values. ' - 'For example: trusted_dashboard=http://acme.com ' - 'trusted_dashboard=http://beta.com'), + 'For example: ' + 'trusted_dashboard=http://acme.com/auth/websso ' + 'trusted_dashboard=http://beta.com/auth/websso'), cfg.StrOpt('sso_callback_template', default=_SSO_CALLBACK, help='Location of Single Sign-On callback handler, will ' 'return a token to a trusted dashboard host.'), @@ -894,6 +895,32 @@ FILE_OPTIONS = { help='Entrypoint for the oAuth1.0 auth plugin module in ' 'the keystone.auth.oauth1 namespace.'), ], + 'tokenless_auth': [ + cfg.MultiStrOpt('trusted_issuer', default=[], + help='The list of trusted issuers to further filter ' + 'the certificates that are allowed to ' + 'participate in the X.509 tokenless ' + 'authorization. If the option is absent then ' + 'no certificates will be allowed. ' + 'The naming format for the attributes of a ' + 'Distinguished Name(DN) must be separated by a ' + 'comma and contain no spaces. This configuration ' + 'option may be repeated for multiple values. ' + 'For example: ' + 'trusted_issuer=CN=john,OU=keystone,O=openstack ' + 'trusted_issuer=CN=mary,OU=eng,O=abc'), + cfg.StrOpt('protocol', default='x509', + help='The protocol name for the X.509 tokenless ' + 'authorization along with the option issuer_attribute ' + 'below can look up its corresponding mapping.'), + cfg.StrOpt('issuer_attribute', default='SSL_CLIENT_I_DN', + help='The issuer attribute that is served as an IdP ID ' + 'for the X.509 tokenless authorization along with ' + 'the protocol to look up its corresponding mapping. ' + 'It is the environment variable in the WSGI ' + 'environment that references to the issuer of the ' + 'client certificate.'), + ], 'paste_deploy': [ cfg.StrOpt('config_file', default='keystone-paste.ini', help='Name of the paste configuration file that defines ' diff --git a/keystone-moon/keystone/common/controller.py b/keystone-moon/keystone/common/controller.py index bc7074ac..56bc211a 100644 --- a/keystone-moon/keystone/common/controller.py +++ b/keystone-moon/keystone/common/controller.py @@ -17,6 +17,7 @@ import uuid from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from oslo_utils import strutils import six @@ -736,6 +737,16 @@ class V3Controller(wsgi.Application): # the current tempest heat tests issue a v3 call without this. # This is raised as bug #1283539. Once this is fixed, we # should remove the line below and replace it with an error. + # + # Ahead of actually changing the code to raise an exception, we + # issue a deprecation warning. + versionutils.report_deprecated_feature( + LOG, + _LW('Not specifying a domain during a create user, group or ' + 'project call, and relying on falling back to the ' + 'default domain, is deprecated as of Liberty and will be ' + 'removed in the N release. Specify the domain explicitly ' + 'or use a domain-scoped token')) return CONF.identity.default_domain_id def _normalize_domain_id(self, context, ref): diff --git a/keystone-moon/keystone/common/manager.py b/keystone-moon/keystone/common/manager.py index 7150fbf3..f98a1763 100644 --- a/keystone-moon/keystone/common/manager.py +++ b/keystone-moon/keystone/common/manager.py @@ -104,3 +104,35 @@ class Manager(object): f = getattr(self.driver, name) setattr(self, name, f) return f + + +def create_legacy_driver(driver_class): + """Helper function to deprecate the original driver classes. + + The keystone.{subsystem}.Driver classes are deprecated in favor of the + new versioned classes. This function creates a new class based on a + versioned class and adds a deprecation message when it is used. + + This will allow existing custom drivers to work when the Driver class is + renamed to include a version. + + Example usage: + + Driver = create_legacy_driver(CatalogDriverV8) + + """ + + module_name = driver_class.__module__ + class_name = driver_class.__name__ + + class Driver(driver_class): + + @versionutils.deprecated( + as_of=versionutils.deprecated.LIBERTY, + what='%s.Driver' % module_name, + in_favor_of='%s.%s' % (module_name, class_name), + remove_in=+2) + def __init__(self, *args, **kwargs): + super(Driver, self).__init__(*args, **kwargs) + + return Driver diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/075_confirm_config_registration.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/075_confirm_config_registration.py new file mode 100644 index 00000000..576842c6 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/075_confirm_config_registration.py @@ -0,0 +1,29 @@ +# 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 + +REGISTRATION_TABLE = 'config_register' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + registration_table = sql.Table( + REGISTRATION_TABLE, + meta, + sql.Column('type', sql.String(64), primary_key=True), + sql.Column('domain_id', sql.String(64), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + registration_table.create(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/common/tokenless_auth.py b/keystone-moon/keystone/common/tokenless_auth.py new file mode 100644 index 00000000..7388b83c --- /dev/null +++ b/keystone-moon/keystone/common/tokenless_auth.py @@ -0,0 +1,193 @@ +# Copyright 2015 Hewlett-Packard +# All Rights Reserved. +# +# 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 hashlib + +from oslo_config import cfg +from oslo_log import log + +from keystone.auth import controllers +from keystone.common import dependency +from keystone.contrib.federation import constants as federation_constants +from keystone.contrib.federation import utils +from keystone import exception +from keystone.i18n import _ + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +@dependency.requires('assignment_api', 'federation_api', + 'identity_api', 'resource_api') +class TokenlessAuthHelper(object): + def __init__(self, env): + """A init class for TokenlessAuthHelper. + + :param env: The HTTP request environment that should contain + client certificate attributes. These attributes should match + with what the mapping defines. Or a user cannot be mapped and + results un-authenticated. The following examples are for the + attributes that reference to the client certificate's Subject's + Common Name and Organization: + SSL_CLIENT_S_DN_CN, SSL_CLIENT_S_DN_O + :type env: dict + """ + + self.env = env + + def _build_scope_info(self): + """Build the token request scope based on the headers. + + :returns: scope data + :rtype: dict + """ + project_id = self.env.get('HTTP_X_PROJECT_ID') + project_name = self.env.get('HTTP_X_PROJECT_NAME') + project_domain_id = self.env.get('HTTP_X_PROJECT_DOMAIN_ID') + project_domain_name = self.env.get('HTTP_X_PROJECT_DOMAIN_NAME') + domain_id = self.env.get('HTTP_X_DOMAIN_ID') + domain_name = self.env.get('HTTP_X_DOMAIN_NAME') + + scope = {} + if project_id: + scope['project'] = {'id': project_id} + elif project_name: + scope['project'] = {'name': project_name} + if project_domain_id: + scope['project']['domain'] = {'id': project_domain_id} + elif project_domain_name: + scope['project']['domain'] = {'name': project_domain_name} + else: + msg = _('Neither Project Domain ID nor Project Domain Name ' + 'was provided.') + raise exception.ValidationError(msg) + elif domain_id: + scope['domain'] = {'id': domain_id} + elif domain_name: + scope['domain'] = {'name': domain_name} + else: + raise exception.ValidationError( + attribute='project or domain', + target='scope') + return scope + + def get_scope(self): + auth = {} + # NOTE(chioleong): auth methods here are insignificant because + # we only care about using auth.controllers.AuthInfo + # to validate the scope information. Therefore, + # we don't provide any identity. + auth['scope'] = self._build_scope_info() + + # NOTE(chioleong): we'll let AuthInfo validate the scope for us + auth_info = controllers.AuthInfo.create({}, auth, scope_only=True) + return auth_info.get_scope() + + def get_mapped_user(self, project_id=None, domain_id=None): + """Map client certificate to an existing user. + + If user is ephemeral, there is no validation on the user himself; + however it will be mapped to a corresponding group(s) and the scope + of this ephemeral user is the same as what is assigned to the group. + + :param project_id: Project scope of the mapped user. + :param domain_id: Domain scope of the mapped user. + :returns: A dictionary that contains the keys, such as + user_id, user_name, domain_id, domain_name + :rtype: dict + """ + idp_id = self._build_idp_id() + LOG.debug('The IdP Id %s and protocol Id %s are used to look up ' + 'the mapping.', idp_id, CONF.tokenless_auth.protocol) + + mapped_properties, mapping_id = self.federation_api.evaluate( + idp_id, CONF.tokenless_auth.protocol, self.env) + + user = mapped_properties.get('user', {}) + user_id = user.get('id') + user_name = user.get('name') + user_type = user.get('type') + if user.get('domain') is not None: + user_domain_id = user.get('domain').get('id') + user_domain_name = user.get('domain').get('name') + else: + user_domain_id = None + user_domain_name = None + + # if user is ephemeral type, we don't care if the user exists + # or not, but just care if the mapped group(s) is valid. + if user_type == utils.UserType.EPHEMERAL: + user_ref = {'type': utils.UserType.EPHEMERAL} + group_ids = mapped_properties['group_ids'] + utils.validate_groups_in_backend(group_ids, + mapping_id, + self.identity_api) + group_ids.extend( + utils.transform_to_group_ids( + mapped_properties['group_names'], mapping_id, + self.identity_api, self.assignment_api)) + roles = self.assignment_api.get_roles_for_groups(group_ids, + project_id, + domain_id) + if roles is not None: + role_names = [role['name'] for role in roles] + user_ref['roles'] = role_names + user_ref['group_ids'] = list(group_ids) + user_ref[federation_constants.IDENTITY_PROVIDER] = idp_id + user_ref[federation_constants.PROTOCOL] = ( + CONF.tokenless_auth.protocol) + return user_ref + + if user_id: + user_ref = self.identity_api.get_user(user_id) + elif user_name and (user_domain_name or user_domain_id): + if user_domain_name: + user_domain = self.resource_api.get_domain_by_name( + user_domain_name) + self.resource_api.assert_domain_enabled(user_domain['id'], + user_domain) + user_domain_id = user_domain['id'] + user_ref = self.identity_api.get_user_by_name(user_name, + user_domain_id) + else: + msg = _('User auth cannot be built due to missing either ' + 'user id, or user name with domain id, or user name ' + 'with domain name.') + raise exception.ValidationError(msg) + self.identity_api.assert_user_enabled( + user_id=user_ref['id'], + user=user_ref) + user_ref['type'] = utils.UserType.LOCAL + return user_ref + + def _build_idp_id(self): + """Build the IdP name from the given config option issuer_attribute. + + The default issuer attribute SSL_CLIENT_I_DN in the environment is + built with the following formula - + + base64_idp = sha1(env['SSL_CLIENT_I_DN']) + + :returns: base64_idp like the above example + :rtype: str + """ + idp = self.env.get(CONF.tokenless_auth.issuer_attribute) + if idp is None: + raise exception.TokenlessAuthConfigError( + issuer_attribute=CONF.tokenless_auth.issuer_attribute) + + hashed_idp = hashlib.sha256(idp) + return hashed_idp.hexdigest() diff --git a/keystone-moon/keystone/common/wsgi.py b/keystone-moon/keystone/common/wsgi.py index 0dee954b..8b99c87d 100644 --- a/keystone-moon/keystone/common/wsgi.py +++ b/keystone-moon/keystone/common/wsgi.py @@ -660,7 +660,8 @@ class RoutersBase(object): get_action=None, head_action=None, get_head_action=None, put_action=None, post_action=None, patch_action=None, delete_action=None, get_post_action=None, - path_vars=None, status=json_home.Status.STABLE): + path_vars=None, status=json_home.Status.STABLE, + new_path=None): if get_head_action: getattr(controller, get_head_action) # ensure the attribute exists mapper.connect(path, controller=controller, action=get_head_action, @@ -697,10 +698,10 @@ class RoutersBase(object): resource_data = dict() if path_vars: - resource_data['href-template'] = path + resource_data['href-template'] = new_path or path resource_data['href-vars'] = path_vars else: - resource_data['href'] = path + resource_data['href'] = new_path or path json_home.Status.update_resource_data(resource_data, status) |