From efbc57e5938370e9b2a43f83a3519aa9f6525f8f Mon Sep 17 00:00:00 2001 From: chioleong Date: Tue, 3 Feb 2015 16:59:51 -0800 Subject: [PATCH] Tokenless authz with X.509 SSL client certificate Implemented middleware to map an incoming trusted SSL client certificate into Keystone auth credential so we can perform authorization without having to issue a token. TODO: to submit a separate patch to devstack to enable this feature. Co-authored-by: guang-yee SecurityImapct DocImpact implements bp keystone-tokenless-authz-with-x509-ssl-client-cert Change-Id: Icc7305ca9d96f8e9cdc95ccde57de650801c6544 --- doc/source/configure_tokenless_x509.rst | 328 ++++++++++++ doc/source/index.rst | 1 + keystone/auth/controllers.py | 21 +- keystone/common/config.py | 26 + keystone/common/tokenless_auth.py | 193 ++++++++ keystone/exception.py | 6 + keystone/middleware/core.py | 112 ++++- keystone/tests/unit/mapping_fixtures.py | 245 +++++++++ keystone/tests/unit/test_middleware.py | 630 ++++++++++++++++++++++++ 9 files changed, 1549 insertions(+), 13 deletions(-) create mode 100644 doc/source/configure_tokenless_x509.rst create mode 100644 keystone/common/tokenless_auth.py diff --git a/doc/source/configure_tokenless_x509.rst b/doc/source/configure_tokenless_x509.rst new file mode 100644 index 0000000000..40b9fd20c9 --- /dev/null +++ b/doc/source/configure_tokenless_x509.rst @@ -0,0 +1,328 @@ +.. + 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. + +================================================ +Configuring Keystone for Tokenless Authorization +================================================ + +.. NOTE:: + + This feature is experimental and unsupported in Liberty. + +----------- +Definitions +----------- + +* `X.509 Tokenless Authorization`: Provides a means to authorize client + operations within Keystone by using an X.509 SSL client certificate + without having to issue a token. For details, please refer to the specs + `Tokenless Authorization with X.509 Client SSL Certificate`_ + +.. _`Tokenless Authorization with X.509 Client SSL Certificate`: http://specs.openstack.org/openstack/keystone-specs/specs/liberty/keystone-tokenless-authz-with-x509-ssl-client-cert.html + +Prerequisites +------------- + +Keystone must be running in a web container with https enabled; tests have +been done with Apache/2.4.7 running on Ubuntu 14.04 . Please refer to +`running-keystone-in-httpd`_ and `apache-certificate-and-key-installation`_ +as references for this setup. + +.. _`running-keystone-in-httpd`: http://docs.openstack.org/developer/keystone/apache-httpd.html +.. _`apache-certificate-and-key-installation`: https://www.digitalocean.com/community/tutorials/how-to-create-a-ssl-certificate-on-apache-for-ubuntu-14-04 + +-------------------- +Apache Configuration +-------------------- + +To enable X.509 tokenless authorization, SSL has to be enabled and configured +in the Apache virtual host file. The Client authentication attribute +``SSLVerifyClient`` should be set as ``optional`` to allow other token +authentication methods and attribute ``SSLOptions`` needs to set as +``+StdEnvVars`` to allow certificate attributes to be passed. The following +is the sample virtual host file used for the testing. + +.. code-block:: ini + + + WSGIScriptAlias / /var/www/cgi-bin/keystone/main + ErrorLog /var/log/apache2/keystone.log + LogLevel debug + CustomLog /var/log/apache2/access.log combined + SSLEngine on + SSLCertificateFile /etc/apache2/ssl/apache.cer + SSLCertificateKeyFile /etc/apache2/ssl/apache.key + SSLCACertificatePath /etc/apache2/capath + SSLOptions +StdEnvVars + SSLVerifyClient optional + + +---------------------- +Keystone Configuration +---------------------- + +The following options can be defined in `keystone.conf`: + +* ``trusted_issuer`` - The multi-str 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; however + spaces are allowed for the value of an attribute, like 'L=San Jose' in + the example below. This configuration option may be repeated for multiple + values. Please look at the sample below. +* ``protocol`` - The protocol name for the X.509 tokenless authorization + along with the option `issuer_attribute` below can look up its + corresponding mapping. It defaults to ``x509``. +* ``issuer_attribute`` - 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 + enviornment that references to the Issuer of the client certificate. It + defaults to ``SSL_CLIENT_I_DN``. + +This is a sample configuration for two `trusted_issuer` and a `protocol` set +to ``x509``. + +.. code-block:: ini + + [tokenless_auth] + trusted_issuer = emailAddress=mary@abc.com,CN=mary,OU=eng,O=abc,L=San Jose,ST=California,C=US + trusted_issuer = emailAddress=john@openstack.com,CN=john,OU=keystone,O=openstack,L=Sunnyvale,ST=California,C=US + protocol = x509 + +------------- +Setup Mapping +------------- + +Like federation, X.509 tokenless authorization also utilizes the mapping +mechanism to formulate an identity. The identity provider must correspond +to the issuer of the X.509 SSL client certificate. The protocol for the +given identity is ``x509`` by default, but can be configurable. + +Create an Identity Provider(IdP) +-------------------------------- + +In order to create an IdP, the issuer DN in the client certificate needs +to be provided. The following sample is what a generic issuer DN looks +like in a certificate. + +.. code-block:: ini + + E=john@openstack.com + CN=john + OU=keystone + O=openstack + L=Sunnyvale + S=California + C=US + +The issuer DN should be constructed as a string that contains no spaces +and have the right order seperated by commas like the example below. +Please be aware that ``emailAddress`` and ``ST`` should be used instead +of ``E`` and ``S`` that are shown in the above example. The following is +the sample Python code used to create the IdP ID. + +.. code-block:: python + + import hashlib + issuer_dn = 'emailAddress=john@openstack.com,CN=john,OU=keystone, + O=openstack,L=Sunnyvale,ST=California,C=US' + hashed_idp = hashlib.sha256(issuer_dn) + idp_id = hashed_idp.hexdigest() + print(idp_id) + +The output of the above Python code will be the IdP ID and the following +sample curl command should be sent to keystone to create an IdP with the +newly generated IdP ID. + +.. code-block:: bash + + curl -k -s -X PUT -H "X-Auth-Token: " \ + -H "Content-Type: application/json" \ + -d '{"identity_provider": {"description": "Stores keystone IDP identities.","enabled": true}}' \ + https://:/v3/OS-FEDERATION/identity_providers/ + +Create a Map +------------ + +A mapping needs to be created to map the ``Subject DN`` in the client +certificate as a user to yield a valid local user if the user's ``type`` +defined as ``local`` in the mapping. For example, the client certificate +has ``Subject DN`` as ``CN=alex,OU=eng,O=nice-network,L=Sunnyvale, +ST=California,C=US``, in the following examples, ``user_name`` will be +mapped to``alex`` and ``domain_name`` will be mapped to ``nice-network``. +And it has user's ``type`` set to ``local``. If user's ``type`` is not +defined, it defaults to ``ephemeral``. + +Please refer to `mod_ssl`_ for the detailed mapping attributes. + +.. _`mod_ssl`: http://httpd.apache.org/docs/current/mod/mod_ssl.html + +.. code-block:: javascript + + { + "mapping": { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + "domain": { + "name": "{1}" + }, + "type": "local" + } + } + ], + "remote": [ + { + "type": "SSL_CLIENT_S_DN_CN" + }, + { + "type": "SSL_CLIENT_S_DN_O" + } + ] + } + ] + } + } + +When user's ``type`` is not defined or set to ``ephemeral``, the mapped user +does not have to be a valid local user but the mapping must yield at least +one valid local group. For example: + +.. code-block:: javascript + + { + "mapping": { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + "type": "ephemeral" + } + }, + { + "group": { + "id": "12345678" + } + } + ], + "remote": [ + { + "type": "SSL_CLIENT_S_DN_CN" + } + ] + } + ] + } + } + +The following sample curl command should be sent to keystone to create a +mapping with the provided mapping ID. The mapping ID is user designed and +it can be any string as opposed to IdP ID. + +.. code-block:: bash + + curl -k -s -H "X-Auth-Token: " \ + -H "Content-Type: application/json" \ + -d '{"mapping": {"rules": [{"local": [{"user": {"name": "{0}","type": "ephemeral"}},{"group": {"id": ""}}],"remote": [{"type": "SSL_CLIENT_S_DN_CN"}]}]}}' \ + -X PUT https://:/v3/OS-FEDERATION/mappings/ + + +Create a Protocol +----------------- + +The name of the protocol will be the one defined in `keystone.conf` as +``protocol`` which defaults to ``x509``. The protocol name is user designed +and it can be any name as opposed to IdP ID. + +A protocol name and an IdP ID will uniquely identify a mapping. + +The following sample curl command should be sent to keystone to create a +protocol with the provided protocol name that is defined in `keystone.conf`. + +.. code-block:: bash + + curl -k -s -H "X-Auth-Token: " \ + -H "Content-Type: application/json" \ + -d '{"protocol": {"mapping_id": ""}}' \ + -X PUT https://:/v3/OS-FEDERATION/identity_providers//protocols/ + +------------------------------- +Setup ``auth_token`` middleware +------------------------------- + +In order to use ``auth_token`` middleware as the service client for X.509 +tokenless authorization, both configurable options and scope information +will need to be setup. + +Configurable Options +-------------------- + +The following configurable options in ``auth_token`` middleware +should set to the correct values: + +* ``auth_protocol`` - Set to ``https``. +* ``certfile`` - Set to the full path of the certificate file. +* ``keyfile`` - Set to the full path of the private key file. +* ``cafile`` - Set to the full path of the trusted CA certificate file. + +Scope Information +----------------- + +The scope information will be passed from the headers with the following +header attributes to: + +* ``X-Project-Id`` - If specified, its the project scope. +* ``X-Project-Name`` - If specified, its the project scope. +* ``X-Project-Domain-Id`` - If specified, its the domain of project scope. +* ``X-Project-Domain-Name`` - If specified, its the domain of project scope. +* ``X-Domain-Id`` - If specified, its the domain scope. +* ``X-Domain-Name`` - If specified, its the domain scope. + +--------------------- +Test It Out with cURL +--------------------- + +Once the above configurations have been setup, the following curl command can +be used for token validation. + +.. code-block:: bash + + curl -v -k -s -X GET --cert //x509client.crt \ + --key //x509client.key \ + --cacert //ca.crt \ + -H "X-Project-Name: " \ + -H "X-Project-Domain-Id: " \ + -H "X-Subject-Token: " \ + https://:/v3/auth/tokens | python -mjson.tool + +Details of the Options +---------------------- + +* ``--cert`` - The client certificate that will be presented to Keystone. + The ``Issuer`` in the certificate along with the defined ``protocol`` + in `keystone.conf` will uniquely identify the mapping. The ``Subject`` + in the certificate will be mapped to the valid local user from the + identified mapping. +* ``--key`` - The corresponding client private key. +* ``--cacert`` - It can be the Apache server certificate or its issuer + (signer) certificate. +* ``X-Project-Name`` - The project scope needs to be passed in the header. +* ``X-Project-Domain-Id`` - Its the domain of project scope. +* ``X-Subject-Token`` - The token to be validated. + diff --git a/doc/source/index.rst b/doc/source/index.rst index c77d773830..d54dd9825b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -54,6 +54,7 @@ Getting Started configure_federation mapping_combinations mapping_schema + configure_tokenless_x509 configuringservices extensions key_terms diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py index 0412469605..133230d693 100644 --- a/keystone/auth/controllers.py +++ b/keystone/auth/controllers.py @@ -129,9 +129,9 @@ class AuthInfo(object): """Encapsulation of "auth" request.""" @staticmethod - def create(context, auth=None): + def create(context, auth=None, scope_only=False): auth_info = AuthInfo(context, auth=auth) - auth_info._validate_and_normalize_auth_data() + auth_info._validate_and_normalize_auth_data(scope_only) return auth_info def __init__(self, context, auth=None): @@ -272,14 +272,25 @@ class AuthInfo(object): if method_name not in AUTH_METHODS: raise exception.AuthMethodNotSupported() - def _validate_and_normalize_auth_data(self): - """Make sure "auth" is valid.""" + def _validate_and_normalize_auth_data(self, scope_only=False): + """Make sure "auth" is valid. + + :param scope_only: If it is True, auth methods will not be + validated but only the scope data. + :type scope_only: boolean + """ # make sure "auth" exist if not self.auth: raise exception.ValidationError(attribute='auth', target='request body') - self._validate_auth_methods() + # NOTE(chioleong): Tokenless auth does not provide auth methods, + # we only care about using this method to validate the scope + # information. Therefore, validating the auth methods here is + # insignificant and we can skip it when scope_only is set to + # true. + if scope_only is False: + self._validate_auth_methods() self._validate_and_normalize_scope_data() def get_method_names(self): diff --git a/keystone/common/config.py b/keystone/common/config.py index 6cc848b478..a09aca513e 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -894,6 +894,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/common/tokenless_auth.py b/keystone/common/tokenless_auth.py new file mode 100644 index 0000000000..7388b83c3e --- /dev/null +++ b/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/exception.py b/keystone/exception.py index 8e573c4c67..2940d50838 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -480,3 +480,9 @@ class OAuthHeadersMissingError(UnexpectedError): 'HTTPd or Apache, ensure WSGIPassAuthorization ' 'is set to On.') title = 'Error retrieving OAuth headers' + + +class TokenlessAuthConfigError(ValidationError): + message_format = _('Could not determine Identity Provider ID. The ' + 'configuration option %(issuer_attribute)s ' + 'was not found in the request environment.') diff --git a/keystone/middleware/core.py b/keystone/middleware/core.py index 62ff291acf..04c74f6b07 100644 --- a/keystone/middleware/core.py +++ b/keystone/middleware/core.py @@ -19,10 +19,14 @@ from oslo_middleware import sizelimit from oslo_serialization import jsonutils from keystone.common import authorization +from keystone.common import tokenless_auth from keystone.common import wsgi +from keystone.contrib.federation import constants as federation_constants +from keystone.contrib.federation import utils from keystone import exception -from keystone.i18n import _LW +from keystone.i18n import _, _LI, _LW from keystone.models import token_model +from keystone.token.providers import common CONF = cfg.CONF @@ -194,17 +198,109 @@ class AuthContextMiddleware(wsgi.Middleware): LOG.warning(_LW('RBAC: Invalid token')) raise exception.Unauthorized() - def process_request(self, request): - if AUTH_TOKEN_HEADER not in request.headers: - LOG.debug(('Auth token not in the request header. ' - 'Will not build auth context.')) - return + def _build_tokenless_auth_context(self, env): + """Build the authentication context. + The context is built from the attributes provided in the env, + such as certificate and scope attributes. + """ + tokenless_helper = tokenless_auth.TokenlessAuthHelper(env) + + (domain_id, project_id, trust_ref, unscoped) = ( + tokenless_helper.get_scope()) + user_ref = tokenless_helper.get_mapped_user( + project_id, + domain_id) + + # NOTE(gyee): if it is an ephemeral user, the + # given X.509 SSL client cert does not need to map to + # an existing user. + if user_ref['type'] == utils.UserType.EPHEMERAL: + auth_context = {} + auth_context['group_ids'] = user_ref['group_ids'] + auth_context[federation_constants.IDENTITY_PROVIDER] = ( + user_ref[federation_constants.IDENTITY_PROVIDER]) + auth_context[federation_constants.PROTOCOL] = ( + user_ref[federation_constants.PROTOCOL]) + if domain_id and project_id: + msg = _('Scoping to both domain and project is not allowed') + raise ValueError(msg) + if domain_id: + auth_context['domain_id'] = domain_id + if project_id: + auth_context['project_id'] = project_id + auth_context['roles'] = user_ref['roles'] + else: + # it's the local user, so token data is needed. + token_helper = common.V3TokenDataHelper() + token_data = token_helper.get_token_data( + user_id=user_ref['id'], + method_names=[CONF.tokenless_auth.protocol], + domain_id=domain_id, + project_id=project_id) + + auth_context = {'user_id': user_ref['id']} + auth_context['is_delegated_auth'] = False + if domain_id: + auth_context['domain_id'] = domain_id + if project_id: + auth_context['project_id'] = project_id + auth_context['roles'] = [role['name'] for role + in token_data['token']['roles']] + return auth_context + + def _validate_trusted_issuer(self, env): + """To further filter the certificates that are trusted. + + If the config option 'trusted_issuer' is absent or does + not contain the trusted issuer DN, no certificates + will be allowed in tokenless authorization. + + :param env: The env contains the client issuer's attributes + :type env: dict + :returns: True if client_issuer is trusted; otherwise False + """ + + client_issuer = env.get(CONF.tokenless_auth.issuer_attribute) + if not client_issuer: + msg = _LI('Cannot find client issuer in env by the ' + 'issuer attribute - %s.') + LOG.info(msg, CONF.tokenless_auth.issuer_attribute) + return False + + if client_issuer in CONF.tokenless_auth.trusted_issuer: + return True + + msg = _LI('The client issuer %(client_issuer)s does not match with ' + 'the trusted issuer %(trusted_issuer)s') + LOG.info( + msg, {'client_issuer': client_issuer, + 'trusted_issuer': CONF.tokenless_auth.trusted_issuer}) + + return False + + def process_request(self, request): if authorization.AUTH_CONTEXT_ENV in request.environ: - msg = _LW('Auth context already exists in the request environment') + msg = _LW('Auth context already exists in the request ' + 'environment; it will be used for authorization ' + 'instead of creating a new one.') LOG.warning(msg) return - auth_context = self._build_auth_context(request) + # NOTE(gyee): token takes precedence over SSL client certificates. + # This will preserve backward compatibility with the existing + # behavior. Tokenless authorization with X.509 SSL client + # certificate is effectively disabled if no trusted issuers are + # provided. + if AUTH_TOKEN_HEADER in request.headers: + auth_context = self._build_auth_context(request) + elif self._validate_trusted_issuer(request.environ): + auth_context = self._build_tokenless_auth_context( + request.environ) + else: + LOG.debug('There is either no auth token in the request or ' + 'the certificate issuer is not trusted. No auth ' + 'context will be set.') + return LOG.debug('RBAC: auth_context: %s', auth_context) request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context diff --git a/keystone/tests/unit/mapping_fixtures.py b/keystone/tests/unit/mapping_fixtures.py index f86d9245d5..55b7ab68b7 100644 --- a/keystone/tests/unit/mapping_fixtures.py +++ b/keystone/tests/unit/mapping_fixtures.py @@ -901,6 +901,251 @@ MAPPING_GROUPS_WHITELIST_AND_BLACKLIST = { ] } +# Mapping used by tokenless test cases, it maps the user_name +# and domain_name. +MAPPING_WITH_USERNAME_AND_DOMAINNAME = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'name': '{0}', + 'domain': { + 'name': '{1}' + }, + 'type': 'local' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_USER_NAME' + }, + { + 'type': 'SSL_CLIENT_DOMAIN_NAME' + } + ] + } + ] +} + +# Mapping used by tokenless test cases, it maps the user_id +# and domain_name. +MAPPING_WITH_USERID_AND_DOMAINNAME = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'id': '{0}', + 'domain': { + 'name': '{1}' + }, + 'type': 'local' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_USER_ID' + }, + { + 'type': 'SSL_CLIENT_DOMAIN_NAME' + } + ] + } + ] +} + +# Mapping used by tokenless test cases, it maps the user_name +# and domain_id. +MAPPING_WITH_USERNAME_AND_DOMAINID = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'name': '{0}', + 'domain': { + 'id': '{1}' + }, + 'type': 'local' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_USER_NAME' + }, + { + 'type': 'SSL_CLIENT_DOMAIN_ID' + } + ] + } + ] +} + +# Mapping used by tokenless test cases, it maps the user_id +# and domain_id. +MAPPING_WITH_USERID_AND_DOMAINID = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'id': '{0}', + 'domain': { + 'id': '{1}' + }, + 'type': 'local' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_USER_ID' + }, + { + 'type': 'SSL_CLIENT_DOMAIN_ID' + } + ] + } + ] +} + +# Mapping used by tokenless test cases, it maps the domain_id only. +MAPPING_WITH_DOMAINID_ONLY = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'domain': { + 'id': '{0}' + }, + 'type': 'local' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_DOMAIN_ID' + } + ] + } + ] +} + +# Mapping used by tokenless test cases, it maps the domain_name only. +MAPPING_WITH_DOMAINNAME_ONLY = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'domain': { + 'name': '{0}' + }, + 'type': 'local' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_DOMAIN_NAME' + } + ] + } + ] +} + +# Mapping used by tokenless test cases, it maps the user_name only. +MAPPING_WITH_USERNAME_ONLY = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'name': '{0}', + 'type': 'local' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_USER_NAME' + } + ] + } + ] +} + +# Mapping used by tokenless test cases, it maps the user_id only. +MAPPING_WITH_USERID_ONLY = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'id': '{0}', + 'type': 'local' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_USER_ID' + } + ] + } + ] +} + +MAPPING_FOR_EPHEMERAL_USER = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'name': '{0}', + 'type': 'ephemeral' + }, + 'group': { + 'id': 'dummy' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_USER_NAME' + } + ] + } + ] +} + +MAPPING_FOR_DEFAULT_EPHEMERAL_USER = { + 'rules': [ + { + 'local': [ + { + 'user': { + 'name': '{0}' + }, + 'group': { + 'id': 'dummy' + } + } + ], + 'remote': [ + { + 'type': 'SSL_CLIENT_USER_NAME' + } + ] + } + ] +} + EMPLOYEE_ASSERTION = { 'Email': 'tim@example.com', 'UserName': 'tbo', diff --git a/keystone/tests/unit/test_middleware.py b/keystone/tests/unit/test_middleware.py index 3a26dd24d2..d420a568a0 100644 --- a/keystone/tests/unit/test_middleware.py +++ b/keystone/tests/unit/test_middleware.py @@ -12,11 +12,20 @@ # License for the specific language governing permissions and limitations # under the License. +import hashlib +import uuid + from oslo_config import cfg import webob +from keystone.common import authorization +from keystone.common import tokenless_auth +from keystone.contrib.federation import constants as federation_constants +from keystone import exception from keystone import middleware from keystone.tests import unit as tests +from keystone.tests.unit import mapping_fixtures +from keystone.tests.unit import test_backend_sql CONF = cfg.CONF @@ -117,3 +126,624 @@ class JsonBodyMiddlewareTest(tests.TestCase): middleware.JsonBodyMiddleware(None).process_request(req) params = req.environ.get(middleware.PARAMS_ENV, {}) self.assertEqual({}, params) + + +class AuthContextMiddlewareTest(test_backend_sql.SqlTests): + + def setUp(self): + super(AuthContextMiddlewareTest, self).setUp() + self.client_issuer = uuid.uuid4().hex + self.untrusted_client_issuer = uuid.uuid4().hex + self.trusted_issuer = self.client_issuer + self.config_fixture.config(group='tokenless_auth', + trusted_issuer=[self.trusted_issuer]) + + # This idp_id is calculated based on + # sha256(self.client_issuer) + hashed_idp = hashlib.sha256(self.client_issuer) + self.idp_id = hashed_idp.hexdigest() + self._load_sample_data() + + def _load_sample_data(self): + self.domain_id = uuid.uuid4().hex + self.domain_name = uuid.uuid4().hex + self.project_id = uuid.uuid4().hex + self.project_name = uuid.uuid4().hex + self.user_name = uuid.uuid4().hex + self.user_password = uuid.uuid4().hex + self.user_email = uuid.uuid4().hex + self.protocol_id = 'x509' + self.role_id = uuid.uuid4().hex + self.role_name = uuid.uuid4().hex + # for ephemeral user + self.group_name = uuid.uuid4().hex + + # 1) Create a domain for the user. + self.domain = { + 'description': uuid.uuid4().hex, + 'enabled': True, + 'id': self.domain_id, + 'name': self.domain_name, + } + + self.resource_api.create_domain(self.domain_id, self.domain) + + # 2) Create a project for the user. + self.project = { + 'description': uuid.uuid4().hex, + 'domain_id': self.domain_id, + 'enabled': True, + 'id': self.project_id, + 'name': self.project_name, + } + + self.resource_api.create_project(self.project_id, self.project) + + # 3) Create a user in new domain. + self.user = { + 'name': self.user_name, + 'domain_id': self.domain_id, + 'project_id': self.project_id, + 'password': self.user_password, + 'email': self.user_email, + } + + self.user = self.identity_api.create_user(self.user) + + # Add IDP + self.idp = self._idp_ref(id=self.idp_id) + self.federation_api.create_idp(self.idp['id'], + self.idp) + + # Add a role + self.role = { + 'id': self.role_id, + 'name': self.role_name, + } + self.role_api.create_role(self.role_id, self.role) + + # Add a group + self.group = { + 'name': self.group_name, + 'domain_id': self.domain_id, + } + self.group = self.identity_api.create_group(self.group) + + # Assign a role to the user on a project + self.assignment_api.add_role_to_user_and_project( + user_id=self.user['id'], + tenant_id=self.project_id, + role_id=self.role_id) + + # Assign a role to the group on a project + self.assignment_api.create_grant( + role_id=self.role_id, + group_id=self.group['id'], + project_id=self.project_id) + + def _load_mapping_rules(self, rules): + # Add a mapping + self.mapping = self._mapping_ref(rules=rules) + self.federation_api.create_mapping(self.mapping['id'], + self.mapping) + # Add protocols + self.proto_x509 = self._proto_ref(mapping_id=self.mapping['id']) + self.proto_x509['id'] = self.protocol_id + self.federation_api.create_protocol(self.idp['id'], + self.proto_x509['id'], + self.proto_x509) + + def _idp_ref(self, id=None): + idp = { + 'id': id or uuid.uuid4().hex, + 'enabled': True, + 'description': uuid.uuid4().hex + } + return idp + + def _proto_ref(self, mapping_id=None): + proto = { + 'id': uuid.uuid4().hex, + 'mapping_id': mapping_id or uuid.uuid4().hex + } + return proto + + def _mapping_ref(self, rules=None): + if rules is None: + mapped_rules = {} + else: + mapped_rules = rules.get('rules', {}) + return { + 'id': uuid.uuid4().hex, + 'rules': mapped_rules + } + + def _assert_tokenless_auth_context(self, context, ephemeral_user=False): + self.assertIsNotNone(context) + self.assertEqual(self.project_id, context['project_id']) + self.assertIn(self.role_name, context['roles']) + if ephemeral_user: + self.assertEqual(self.group['id'], context['group_ids'][0]) + self.assertEqual('ephemeral', + context[federation_constants.PROTOCOL]) + self.assertEqual(self.idp_id, + context[federation_constants.IDENTITY_PROVIDER]) + else: + self.assertEqual(self.user['id'], context['user_id']) + + def _create_context(self, request, mapping_ref=None, + exception_expected=False): + """Builds the auth context from the given arguments. + + auth context will be returned from the AuthContextMiddleware based on + what is being passed in the given request and what mapping is being + setup in the backend DB. + + :param request: HTTP request + :param mapping_ref: A mapping in JSON structure will be setup in the + backend DB for mapping an user or a group. + :param exception_expected: Sets to True when an exception is expected + to raised based on the given arguments. + :returns: context an auth context contains user and role information + :rtype: dict + """ + if mapping_ref: + self._load_mapping_rules(mapping_ref) + + if not exception_expected: + (middleware.AuthContextMiddleware('Tokenless_auth_test'). + process_request(request)) + context = request.environ.get(authorization.AUTH_CONTEXT_ENV) + else: + context = middleware.AuthContextMiddleware('Tokenless_auth_test') + return context + + def test_context_already_exists(self): + req = make_request() + token_id = uuid.uuid4().hex + req.environ[authorization.AUTH_CONTEXT_ENV] = {'token_id': token_id} + context = self._create_context(request=req) + self.assertEqual(token_id, context['token_id']) + + def test_not_applicable_to_token_request(self): + env = {} + env['PATH_INFO'] = '/auth/tokens' + env['REQUEST_METHOD'] = 'POST' + req = make_request(environ=env) + context = self._create_context(request=req) + self.assertIsNone(context) + + def test_no_tokenless_attributes_request(self): + req = make_request() + context = self._create_context(request=req) + self.assertIsNone(context) + + def test_no_issuer_attribute_request(self): + env = {} + env['HTTP_X_PROJECT_ID'] = uuid.uuid4().hex + req = make_request(environ=env) + context = self._create_context(request=req) + self.assertIsNone(context) + + def test_has_only_issuer_and_project_name_request(self): + env = {} + # SSL_CLIENT_I_DN is the attribute name that wsgi env + # references to issuer of the client certificate. + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = uuid.uuid4().hex + req = make_request(environ=env) + context = self._create_context(request=req, + exception_expected=True) + self.assertRaises(exception.ValidationError, + context.process_request, + req) + + def test_has_only_issuer_and_project_domain_name_request(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_DOMAIN_NAME'] = uuid.uuid4().hex + req = make_request(environ=env) + context = self._create_context(request=req, + exception_expected=True) + self.assertRaises(exception.ValidationError, + context.process_request, + req) + + def test_has_only_issuer_and_project_domain_id_request(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_DOMAIN_ID'] = uuid.uuid4().hex + req = make_request(environ=env) + context = self._create_context(request=req, + exception_expected=True) + self.assertRaises(exception.ValidationError, + context.process_request, + req) + + def test_missing_both_domain_and_project_request(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + req = make_request(environ=env) + context = self._create_context(request=req, + exception_expected=True) + self.assertRaises(exception.ValidationError, + context.process_request, + req) + + def test_empty_trusted_issuer_list(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_ID'] = uuid.uuid4().hex + req = make_request(environ=env) + self.config_fixture.config(group='tokenless_auth', + trusted_issuer=[]) + context = self._create_context(request=req) + self.assertIsNone(context) + + def test_client_issuer_not_trusted(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.untrusted_client_issuer + env['HTTP_X_PROJECT_ID'] = uuid.uuid4().hex + req = make_request(environ=env) + context = self._create_context(request=req) + self.assertIsNone(context) + + def test_proj_scope_with_proj_id_and_proj_dom_id_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_ID'] = self.project_id + env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id + # SSL_CLIENT_USER_NAME and SSL_CLIENT_DOMAIN_NAME are the types + # defined in the mapping that will map to the user name and + # domain name + env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + self._assert_tokenless_auth_context(context) + + def test_proj_scope_with_proj_id_only_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_ID'] = self.project_id + env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + self._assert_tokenless_auth_context(context) + + def test_proj_scope_with_proj_name_and_proj_dom_id_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id + env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + self._assert_tokenless_auth_context(context) + + def test_proj_scope_with_proj_name_and_proj_dom_name_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + self._assert_tokenless_auth_context(context) + + def test_proj_scope_with_proj_name_only_fail(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_id + env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME, + exception_expected=True) + self.assertRaises(exception.ValidationError, + context.process_request, + req) + + def test_mapping_with_userid_and_domainid_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_ID'] = self.user['id'] + env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERID_AND_DOMAINID) + self._assert_tokenless_auth_context(context) + + def test_mapping_with_userid_and_domainname_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_ID'] = self.user['id'] + env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERID_AND_DOMAINNAME) + self._assert_tokenless_auth_context(context) + + def test_mapping_with_username_and_domainid_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINID) + self._assert_tokenless_auth_context(context) + + def test_only_domain_name_fail(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_ID'] = self.project_id + env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id + env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_DOMAINNAME_ONLY, + exception_expected=True) + self.assertRaises(exception.ValidationError, + context.process_request, + req) + + def test_only_domain_id_fail(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_ID'] = self.project_id + env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id + env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_DOMAINID_ONLY, + exception_expected=True) + self.assertRaises(exception.ValidationError, + context.process_request, + req) + + def test_missing_domain_data_fail(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_ID'] = self.project_id + env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id + env['SSL_CLIENT_USER_NAME'] = self.user_name + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_ONLY, + exception_expected=True) + self.assertRaises(exception.ValidationError, + context.process_request, + req) + + def test_userid_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_ID'] = self.project_id + env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id + env['SSL_CLIENT_USER_ID'] = self.user['id'] + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERID_ONLY) + self._assert_tokenless_auth_context(context) + + def test_domain_disable_fail(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id + req = make_request(environ=env) + self.domain['enabled'] = False + self.domain = self.resource_api.update_domain( + self.domain['id'], self.domain) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINID, + exception_expected=True) + self.assertRaises(exception.Unauthorized, + context.process_request, + req) + + def test_user_disable_fail(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id + req = make_request(environ=env) + self.user['enabled'] = False + self.user = self.identity_api.update_user(self.user['id'], self.user) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINID, + exception_expected=True) + self.assertRaises(AssertionError, + context.process_request, + req) + + def test_invalid_user_fail(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_ID'] = self.project_id + env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id + env['SSL_CLIENT_USER_NAME'] = uuid.uuid4().hex + env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name + req = make_request(environ=env) + context = self._create_context( + request=req, + mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME, + exception_expected=True) + self.assertRaises(exception.UserNotFound, + context.process_request, + req) + + def test_ephemeral_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_NAME'] = self.user_name + req = make_request(environ=env) + self.config_fixture.config(group='tokenless_auth', + protocol='ephemeral') + self.protocol_id = 'ephemeral' + mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping['rules'][0]['local'][0]['group']['id'] = self.group['id'] + context = self._create_context( + request=req, + mapping_ref=mapping) + self._assert_tokenless_auth_context(context, ephemeral_user=True) + + def test_ephemeral_with_default_user_type_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_NAME'] = self.user_name + req = make_request(environ=env) + self.config_fixture.config(group='tokenless_auth', + protocol='ephemeral') + self.protocol_id = 'ephemeral' + # this mapping does not have the user type defined + # and it should defaults to 'ephemeral' which is + # the expected type for the test case. + mapping = mapping_fixtures.MAPPING_FOR_DEFAULT_EPHEMERAL_USER.copy() + mapping['rules'][0]['local'][0]['group']['id'] = self.group['id'] + context = self._create_context( + request=req, + mapping_ref=mapping) + self._assert_tokenless_auth_context(context, ephemeral_user=True) + + def test_ephemeral_any_user_success(self): + """Ephemeral user does not need a specified user + Keystone is not looking to match the user, but a corresponding group. + """ + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_NAME'] = uuid.uuid4().hex + req = make_request(environ=env) + self.config_fixture.config(group='tokenless_auth', + protocol='ephemeral') + self.protocol_id = 'ephemeral' + mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping['rules'][0]['local'][0]['group']['id'] = self.group['id'] + context = self._create_context( + request=req, + mapping_ref=mapping) + self._assert_tokenless_auth_context(context, ephemeral_user=True) + + def test_ephemeral_invalid_scope_fail(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = uuid.uuid4().hex + env['HTTP_X_PROJECT_DOMAIN_NAME'] = uuid.uuid4().hex + env['SSL_CLIENT_USER_NAME'] = self.user_name + req = make_request(environ=env) + self.config_fixture.config(group='tokenless_auth', + protocol='ephemeral') + self.protocol_id = 'ephemeral' + mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping['rules'][0]['local'][0]['group']['id'] = self.group['id'] + context = self._create_context( + request=req, + mapping_ref=mapping, + exception_expected=True) + self.assertRaises(exception.Unauthorized, + context.process_request, + req) + + def test_ephemeral_no_group_found_fail(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_NAME'] = self.user_name + req = make_request(environ=env) + self.config_fixture.config(group='tokenless_auth', + protocol='ephemeral') + self.protocol_id = 'ephemeral' + mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping['rules'][0]['local'][0]['group']['id'] = uuid.uuid4().hex + context = self._create_context( + request=req, + mapping_ref=mapping, + exception_expected=True) + self.assertRaises(exception.MappedGroupNotFound, + context.process_request, + req) + + def test_ephemeral_incorrect_mapping_fail(self): + """Ephemeral user picks up the non-ephemeral user mapping. + Looking up the mapping with protocol Id 'x509' will load up + the non-ephemeral user mapping, results unauthenticated. + """ + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + env['HTTP_X_PROJECT_NAME'] = self.project_name + env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name + env['SSL_CLIENT_USER_NAME'] = self.user_name + req = make_request(environ=env) + # This will pick up the incorrect mapping + self.config_fixture.config(group='tokenless_auth', + protocol='x509') + self.protocol_id = 'x509' + mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping['rules'][0]['local'][0]['group']['id'] = uuid.uuid4().hex + context = self._create_context( + request=req, + mapping_ref=mapping, + exception_expected=True) + self.assertRaises(exception.MappedGroupNotFound, + context.process_request, + req) + + def test_create_idp_id_success(self): + env = {} + env['SSL_CLIENT_I_DN'] = self.client_issuer + auth = tokenless_auth.TokenlessAuthHelper(env) + idp_id = auth._build_idp_id() + self.assertEqual(self.idp_id, idp_id) + + def test_create_idp_id_attri_not_found_fail(self): + env = {} + env[uuid.uuid4().hex] = self.client_issuer + auth = tokenless_auth.TokenlessAuthHelper(env) + expected_msg = ('Could not determine Identity Provider ID. The ' + 'configuration option %s was not found in the ' + 'request environment.' % + CONF.tokenless_auth.issuer_attribute) + # Check the content of the exception message as well + self.assertRaisesRegexp(exception.TokenlessAuthConfigError, + expected_msg, + auth._build_idp_id)