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 bb65fa135a..511bc89f97 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)