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 <guang.yee@hp.com> SecurityImapct DocImpact implements bp keystone-tokenless-authz-with-x509-ssl-client-cert Change-Id: Icc7305ca9d96f8e9cdc95ccde57de650801c6544
This commit is contained in:
parent
1aa1c061c6
commit
efbc57e593
328
doc/source/configure_tokenless_x509.rst
Normal file
328
doc/source/configure_tokenless_x509.rst
Normal file
@ -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
|
||||
|
||||
<VirtualHost *:443>
|
||||
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
|
||||
</VirtualHost>
|
||||
|
||||
----------------------
|
||||
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: <TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"identity_provider": {"description": "Stores keystone IDP identities.","enabled": true}}' \
|
||||
https://<HOSTNAME>:<PORT>/v3/OS-FEDERATION/identity_providers/<IdP ID>
|
||||
|
||||
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: <TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"mapping": {"rules": [{"local": [{"user": {"name": "{0}","type": "ephemeral"}},{"group": {"id": "<GROUPID>"}}],"remote": [{"type": "SSL_CLIENT_S_DN_CN"}]}]}}' \
|
||||
-X PUT https://<HOSTNAME>:<PORT>/v3/OS-FEDERATION/mappings/<MAPPING ID>
|
||||
|
||||
|
||||
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: <TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"protocol": {"mapping_id": "<MAPPING ID>"}}' \
|
||||
-X PUT https://<HOSTNAME>:<PORT>/v3/OS-FEDERATION/identity_providers/<IdP ID>/protocols/<PROTOCOL NAME>
|
||||
|
||||
-------------------------------
|
||||
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 /<PATH>/x509client.crt \
|
||||
--key /<PATH>/x509client.key \
|
||||
--cacert /<PATH>/ca.crt \
|
||||
-H "X-Project-Name: <PROJECT-NAME>" \
|
||||
-H "X-Project-Domain-Id: <PROJECT-DOMAIN-ID>" \
|
||||
-H "X-Subject-Token: <TOKEN>" \
|
||||
https://<HOST>:<PORT>/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.
|
||||
|
@ -54,6 +54,7 @@ Getting Started
|
||||
configure_federation
|
||||
mapping_combinations
|
||||
mapping_schema
|
||||
configure_tokenless_x509
|
||||
configuringservices
|
||||
extensions
|
||||
key_terms
|
||||
|
@ -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):
|
||||
|
@ -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 '
|
||||
|
193
keystone/common/tokenless_auth.py
Normal file
193
keystone/common/tokenless_auth.py
Normal file
@ -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()
|
@ -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.')
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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 = { |